突破限制,訪問其它Go package中的私人函數

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

目錄 [−]

  1. 將exported類型變為其它package不可訪問
  2. 訪問其它package中的私人方法
  3. 訪問其它package中的struct 私人欄位
  4. 更hack的方法
  5. 參考文檔

熟悉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/httpnet/http/* import.
  • $GOPATH/src/mypkg/internal/foo 只能被$GOPATH/src/mypkg import.

訪問其它package中的私人方法

如果你查看 Go 標準庫的的代碼, 比如 time/sleep.go 檔案, 你會發現一些奇怪的函數, 如 Sleep:

1
func Sleep(d Duration)

這個函數我們經常會用到, 也就是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中定義了很多的函數,比如syncnet中的一些函數,你可以通過命令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.saygithub.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將來的版本是否還能使用,僅供嬉戲之用,慎用在產品代碼中。

參考文檔

  1. https://golang.org/cmd/compile/
  2. https://github.com/golang/go/issues/15006
  3. https://siadat.github.io/post/golinkname
  4. https://sitano.github.io/2016/04/28/golang-private/
  5. https://golang.org/doc/go1.4#internalpackages
  6. http://www.alangpierce.com/blog/2016/03/17/adventures-in-go-accessing-unexported-functions/
  7. https://groups.google.com/forum/#!topic/golang-nuts/ppGGazd9KXI
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.