冲破限度,拜访其它Go package中的公有函数
原文地址 https://colobu.com/2017/05/12...
Go语言通过identifier的首字母是否大写来决定它是否能够被其它package所拜访。
正式的Go语言标准是这么规定的:
An identifier may be exported to permit access to it from another pack>age. An identifier is exported if both:
the first character of the identifier's name is a Unicode upper case letter (Unicode class "Lu"); and
the identifier is declared in the package block or it is a field name or method name.All other identifiers are not exported.
这个Go语言标准定义的拜访权限管制办法。
然而有没有方法冲破这个限度呢?
冲破能够从两个方向来探讨: 将exported类型变为其它package不可拜访;将unexported的类型变为其它package可拜访。
1. 将exported类型变为其它package不可拜访
至多有一个方法能够将package中 exported的函数、类型变为其它package不可拜访, 那就是定义一个internal package,将这些package放在internal package之下。
Go语言自身没有这个限度,这是通过go命令实现的。最早这个个性是在 go 1.4版本中引入的,相干的细节能够查看文档: design document
这个规定是这样的:
An import of a path containing the element “internal” is disallowed if the importing code is outside the tree rooted at the parent of the “internal” directory.
也就是internal包下的 exported 类型只能由internal所在的package (internal的parent)为root的package所拜访。
举例来说:
- /a/b/c/internal/d/e/f 能够被/a/b/c import, 不能被 /a/b/g import.
- $GOROOT/src/pkg/internal/xxx只能够被规范库import ($GOROOT/src/).
- $GOROOT/src/pkg/net/http/internal 只能够被 net/http 和 net/http/* import.
- $GOPATH/src/mypkg/internal/foo 只能被$GOPATH/src/mypkg import.
2. 拜访其它package中的公有办法
如果你查看 Go 规范库的的代码, 比方 time/sleep.go 文件, 你会发现一些奇怪的函数, 如 Sleep:
func Sleep(d Duration)
这个函数咱们常常会用到, 也就是time.Sleep函数,然而这个函数并没有函数体,而且同样的目录下也没有汇编语言的代码实现,那么,这个函数在哪里定义的?
按照标准,一个只有函数申明的函数是在Go的内部实现的,咱们称之为external function。
实际上,这个"内部函数"也是在Go规范库中实现的,它是 runtime中的一个 unexported的函数:
//go:linkname timeSleep time.Sleepfunc timeSleep(ns int64) { if ns <= 0 { return } t := getg().timer if t == nil { t = new(timer) getg().timer = t } ......}
事实上,runtime为其它 package中定义了很多的函数,比方sync、net中的一些函数,你能够通过命令grep linkname /usr/local/go/src/runtime/*.go查找这些函数。
咱们会有两个疑难:一是为什么这些函数要定义在 runtime package中,而是这个机制到底是怎么实现的?
将相干的函数定义在runtime中的益处是, 它们能够拜访 runtime package中 unexported的类型, 比方getp函数等,相当于往 runtime package打入一个"叛徒",通过"叛徒"能够拜访 runtime package 的公有对象。同时,这些"叛徒"函数只管被申明为unexported,还是能够在其它package中拜访。
第二个问题,其实是Go的go:linkname这个指令施展的作用,它的格局如下:
//go:linkname localname importpath.name
Go文档阐明了这个指令的作用:
The //go:linkname directive instructs the compiler to use “importpath.name” as the object file symbol name for the variable or function declared as “localname” in the source code. Because this directive can subvert the type system and package modularity, it is only enabled in files that have imported "unsafe".
这个指令通知编译器为函数或者变量localname应用importpath.name作为指标文件的符号名。因为这个指令毁坏了类型零碎和包的模块化,所以它只能在 import "unsafe" 的状况下能力应用。
importpath.name能够是这种格局:a/b/c/d/apkg.foo,这样在package a/b/c/d/apkg中就能够应用这个函数foo了。
举个例子,假如咱们的package布局如下:
├── a│ └── a.go├── b│ ├── b.go│ └── internal.s└── main └── main.go
package a 定义了公有的办法,并加上 go:linkname指令, package b 能够调用 package a的公有办法。 main.go 测试拜访 b中的函数。
首先看看a.go中的实现:
a.go
package aimport ( _ "unsafe")//go:linkname say a.say//go:nosplitfunc say(name string) string { return "hello, " + name}//go:linkname say2 github.com/smallnest/private/b.Hi//go:nosplitfunc say2(name string) string { return "hi, " + name}
它定义了两个办法,符号名别离为a.say和github.com/smallnest/private/b.Hi。
这个不同的符号名的形式会影响b中的应用。
b.go
package bimport ( _ "unsafe" _ "github.com/smallnest/private/a")//go:linkname say a.sayfunc say(name string) stringfunc Greet(name string) string { return say(name)}func Hi(name string) string
在b 中,如果想应用符号a.say,你还是须要go:linkname,通知编译器这个函数的符号为a.say。对于Hi函数, 咱们不须要go:linkname指令,因为在a.go中咱们定义的符号名称凑巧就是这个package.funcname。
留神,你须要引入package unsafe,并且在b.go还须要import package a.
你能够在main.go中调用b:
package mainimport ( "fmt" "github.com/smallnest/private/b")func main() { s := b.Greet("world") fmt.Println(s) s = b.Hi("world") fmt.Println(s)}
然而,如果你go run main.go,你不会失去正确的后果,而是会出错:
main go run main.go# github.com/smallnest/private/b../b/b.go:10: missing function body for "say"../b/b.go:16: missing function body for "Hi"
难道咱们后面讲的都是错的吗?
这里有一个技巧,你在 package b下创立一个空的文件, w文件名随便,只有文件后缀为.s,再运行一下go run main.go:
main go run main.gohello, worldhi, world
起因在于Go在编译的时候会启用-complete编译器flag,它要求所有的函数必须蕴含函数体。创立一个空的汇编语言文件绕过这个限度。
当然, 个别状况下咱们不会用到本文所列出的两种冲破形式,只有在很稀少的状况下,为了更好地组织咱们的代码,咱们才会有抉择的采纳这两种办法。至多,作为一个Go开发者,你会记住有两种冲破办法,能够突破Go语言标准中对于权限的限度。
一个利用的例子是能够在代码中拜访sync.runtime_registerPoolCleanup,因为它有明确的linkname
> //go:linkname runtime_registerPoolCleanup> sync.runtime_registerPoolCleanup> func runtime_registerPoolCleanup(cleanup func())>
3. 拜访其它package中的struct 公有字段
再额定附送一个技巧, 能够拜访其它package struct的公有字段。
当然失常状况下struct的公有字段并没有export,所以在其它package是不能失常拜访。通过应用refect,能够拜访struct的公有字段:
import ( "fmt" "reflect" "github.com/smallnest/private/c")func ChangeFoo(f *c.Foo) { v := reflect.ValueOf(f) x := v.Elem().FieldByName("x") fmt.Println(x.Int()) //panic: reflect: reflect.Value.SetInt using value obtained using unexported field //x.SetInt(100) fmt.Println(x.Int()) y := v.Elem().FieldByName("Y") y.SetString("world") fmt.Println(f.Y)}
然而你不能设置公有字段的值,否则会panic,这是因为SetXXX会首先应用v.mustBeAssignable()查看字段是否是exported的。
当然,还能够通过"指针"的形式获取字段的地址,通过地址获取数据或者设置数据。
还是用雷同的例子:
c.go
package ctype Foo struct { x int Y string}func (f Foo) X() int { return f.x}func New(x int, y string) *Foo { return &Foo{x: x, Y: y}}
在package d中拜访:
d.go
package dimport ( "fmt" "unsafe" "github.com/smallnest/private/c")func ChangeFoo(f *c.Foo) { p := unsafe.Pointer(f) // 当时获取或者通过 reflect取得 // 本例中是第一个字段,所以offset=0 offset := uintptr(0) ptr2x := (*int)(unsafe.Pointer(uintptr(p) + offset)) fmt.Println(*ptr2x) *ptr2x = 100 fmt.Println(f.X())}
4. 更hack的办法
如果你还不满足,那么我再赠送一个更hack的办法,然而这个也有点限度,就是你腰调用的办法应该在之前的某处调用过。
这是 Alan Pierce 提供了一个办法。runtime/symtab.go中保留了符号表,通过一些技巧(go:linkname),能拜访它的公有办法,查找到想要调用的函数,而后就能够调用了,Alan将相干的代码写成了一个库,不便调用:go-forceexport。
应用办法如下:
var timeNow func() (int64, int32)err := forceexport.GetFunc(&timeNow, "time.now")if err != nil { // Handle errors if you care about name possibly being invalid.}// Calls the actual time.now function.sec, nsec := timeNow()
我在应用的过程中发现只有相应的办法在某处调用过, 符号表中才有这个函数的信息, forceexport.GetFunc才会返回对应的函数。
另外,这是一个十分hack的形式,不保障Go未来的版本是否还能应用,仅供嬉戏之用,慎用在产品代码中。
参考文档
https://golang.org/cmd/compile/
https://github.com/golang/go/...
https://siadat.github.io/post...
https://sitano.github.io/2016...
https://golang.org/doc/go1.4#...
http://www.alangpierce.com/bl...
https://groups.google.com/for...!topic/golang-nuts/ppGGazd9KXI
原文地址 https://colobu.com/2017/05/12...
关注 golang技术实验室 获取更多好文
本文由mdnice多平台公布