這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
目錄 [−]
- 將exported類型變為其它package不可訪問
- 訪問其它package中的私人方法
- 訪問其它package中的struct 私人欄位
- 更hack的方法
- 參考文檔
熟悉C++、Java、C#等物件導向的程式設計語言的同學,在學習Go語言的過程中,經常會被存取權限所困擾,逐漸才能瞭解這樣一個事實:
Go語言通過identifier
的首字母是否大寫來決定它是否可以被其它package所訪問。
正式的Go語言規範是這麼規定的:
An identifier may be exported to permit access to it from another package. 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可訪問。
將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.
訪問其它package中的私人方法
如果你查看 Go 標準庫的的代碼, 比如 time/sleep.go 檔案, 你會發現一些奇怪的函數, 如 Sleep
:
這個函數我們經常會用到, 也就是time.Sleep
函數,但是這個函數並沒有函數體,而且同樣的目錄下也沒有組合語言的代碼實現,那麼,這個函數在哪裡定義的?
依照規範,一個只有函式宣告的函數是在Go的外部實現的,我們稱之為external function
。
實際上,這個"外部函數"也是在Go標準庫中實現的,它是 runtime中的一個 unexported的函數:
123456789101112131415 |
//go:linkname timeSleep time.Sleepfunc timeSleep(ns int64) {if ns <= 0 {return}t := getg().timerif 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
這個指令發揮的作用,它的格式如下:
1 |
//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布局如下:
1234567 |
├── 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
1234567891011121314151617 |
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
12345678910111213141516 |
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:
12345678910111213 |
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
,你不會得到正確的結果,而是會出錯:
1234 |
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
:
123 |
main go run main.gohello, worldhi, world |
原因在於Go在編譯的時候會啟用-complete
編譯器flag,它要求所有的函數必需包含函數體。建立一個空的組合語言檔案繞過這個限制。
當然, 一般情況下我們不會用到本文所列出的兩種突破方式,只有在很稀少的情況下,為了更好地組織我們的代碼,我們才會有選擇的採用這兩種方法。至少,作為一個Go開發人員,你會記住有兩種突破方法,可以打破Go語言規範中關於許可權的限制。
訪問其它package中的struct 私人欄位
再額外附送一個技巧, 可以訪問其它package struct的私人欄位。
當然正常情況下struct的私人欄位並沒有export,所以在其它package是不能正常訪問。通過使用refect
,可以訪問struct的私人欄位:
12345678910111213141516171819 |
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
1234567891011121314 |
package ctype Foo struct {x intY 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
12345678910111213141516171819 |
package dimport ("fmt""unsafe""github.com/smallnest/private/c")func ChangeFoo(f *c.Foo) {p := unsafe.Pointer(f)// 事先擷取或者通過 reflect獲得// 本例中是第一個欄位,所以offset=0offset := uintptr(0)ptr2x := (*int)(unsafe.Pointer(uintptr(p) + offset))fmt.Println(*ptr2x)*ptr2x = 100fmt.Println(f.X())} |
更hack的方法
如果你還不滿足,那麼我再贈送一個更hack的方法,但是這個也有點限制,就是你腰調用的方法應該在之前的某處調用過。
這是 Alan Pierce 提供了一個方法。runtime/symtab.go中儲存了符號表,通過一些技巧(go:linkname
),能訪問它的私人方法,尋找到想要調用的函數,然後就可以調用了,Alan將相關的代碼寫成了一個庫,方便調用:go-forceexport。
使用方法如下:
1234567 |
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/issues/15006
- https://siadat.github.io/post/golinkname
- https://sitano.github.io/2016/04/28/golang-private/
- https://golang.org/doc/go1.4#internalpackages
- http://www.alangpierce.com/blog/2016/03/17/adventures-in-go-accessing-unexported-functions/
- https://groups.google.com/forum/#!topic/golang-nuts/ppGGazd9KXI