关于后端:go访问私有变量

3次阅读

共计 5804 个字符,预计需要花费 15 分钟才能阅读完成。

冲破限度, 拜访其它 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.Sleep
func 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 a
import (_ "unsafe")
//go:linkname say a.say
//go:nosplit
func say(name string) string {return "hello," + name}
//go:linkname say2 github.com/smallnest/private/b.Hi
//go:nosplit
func say2(name string) string {return "hi," + name}

它定义了两个办法,符号名别离为 a.say 和 github.com/smallnest/private/b.Hi。

这个不同的符号名的形式会影响 b 中的应用。

b.go

package b
import (
    _ "unsafe"
    _ "github.com/smallnest/private/a"
)
//go:linkname say a.say
func say(name string) string
func 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 main
import (
    "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.go
hello, world
hi, 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 c
type 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 d
import (
    "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 多平台公布

正文完
 0