訪問 Golang 私有函數、方法、類型和變量

本文譯自 Yarden Laifenfeld 的 Accessing Private Functions, Methods, Types and Variables in Go

https://medium.com/@yardenlaif/accessing-private-functions-methods-types-and-variables-in-go-951acccc05a6

你有沒有發現自己因爲需要訪問 Go 中的私有函數、方法、全局變量或類型而對電腦大罵不已? 這似乎是不可能的? 事實並非如此, 這篇博文就是爲你而寫的。下面寫的所有內容都不應該被視爲推薦或良好的 Go 代碼設計的例子。

照片由 Call Me Fred 在 Unsplash 上提供

背景

Go 中區分私有和公有的方式是通過名稱的第一個字母。如果字母是小寫, 則是私有的。如果是大寫, 則是公有的。私有函數 / 方法 / 類型的作用域是定義它們的包。

編寫大型 Go 程序時, 不需要使用黑客技術來訪問私有信息。事實上, 最好避免這樣做。但是, 在某些極端情況下, 確實需要使用這種黑客技術。這種需求, 加上純粹的好奇心, 促使我寫下這篇博文。讓我們開始吧!

//go:linkname

要訪問私有函數、全局變量和方法, 可以使用 linkname 編譯器指令。簡單地說, 這個指令允許將一個聲明鏈接到另一個定義。當編譯器看到 linkname 註釋時, 它會將聲明變成一個 "鏈接"(因此得名) 到原始定義。

訪問私有類型的解決方案稍微不那麼常規, 所以我們稍後會深入探討。

Go 運行時

讓我們從最常見的用例開始: 從 Go 運行時運行私有函數。標準庫中的許多公共函數在運行時都有私有替代品, 這些替代品性能更好, 但也有一些限制。一個例子是 rand.Uint32 和它的私有運行時對應物 runtime.fastrandruntime.fastrand 不允許設置種子, 但它的性能要遠遠好於 rand.Uint32

假設我們有一個生成和使用隨機數的函數:

func funcWithRandomNumber() {
  randomNumber := rand.Uint32()
  ...
}

我們不能簡單地用內部實現替換 rand.Uint32:

func funcWithRandomNumber() {
  randomNumber := runtime.fastrand() 
  ...
}

相反, 我們將複製 runtime.fastrand 函數簽名 (這裏的名稱並不重要, 只有類型):

func runtime_fastrand
()
 uint32

爲了 "鏈接" 我們的函數聲明和 runtime.fastrand 的定義, 我們將添加 linkname 編譯器指令, 格式如下:

在這種情況下, 它看起來會像這樣:

//go:linkname runtime_fastrand runtime.fastrand
func runtime_fastrand() uint32

現在, 我們可以像使用 runtime.fastrand 一樣使用 runtime_fastrand:

func runtime_fastrand() uint32
func funcWithRandomNumber() {
  randomNumber := runtime_fastrand()
  ...
}

用戶包

同樣, 我們可以將用戶編寫的包中的私有函數鏈接到函數定義。

對於這個例子, 我們的模塊名稱將是 github.com/YardenLaif/example。在 example 下, 我們有以下目錄和文件:

.
├── a/
│   └── a.go
└── a_test/
    └── a_test.go

a.go 中, 有一個名爲 foo 的私有函數:

package a
func foo(i int) bool { ... }

我們想在 a_test.go 中測試這個函數, 所以我們將使用相同的 linkname 指令格式, 並添加 a 包的完整路徑:

package a_test
import (
  "testing"
  _ "unsafe"
)
//go:linkname a_foo github.com/YardenLaif/example/a.foo
func a_foo(int) bool
func TestFoo(t *testing.T) {
  b := a_foo(1)
  ...
}

全局變量

全局變量可以以同樣的方式進行鏈接。如果我們的 a 包有一個全局變量:

package a
var globalVar = 55

我們可以使用 linkname 指令從 a_test 包訪問 globalVar。我發現在處理全局變量時, 需要導入定義全局變量的包。

package a_test
import (
  "testing"
  _ "unsafe"
  _ "github.com/YardenLaif/example/a"
)
//go:linkname a_globalVar github.com/YardenLaif/example/a.globalVar
var a_globalVar int
func TestGlobalVar(t *testing.T) {
  if a_globalVar != 55 {
    ...
  } 
}

這可能看起來很混亂, 但 a 中定義的 globalVara_test 中定義的 a_globalVar 是同一個變量。這意味着它的初始值爲 55, 在一箇中進行的任何更改都會影響另一個。

請注意: 無法鏈接 const 或非全局變量。

指針接收器方法

我們區分指針接收器方法: _func(t*Type)foo()_ , 和值接收器方法: _func(tType)foo()_。對於後者, 請跳到下一節。

方法值得單獨一節, 因爲它們需要將一個函數鏈接到一個方法。我們將在 a 包中定義一個帶有指針接收器的私有方法:

package a
type IntHolder struct {
  i int
}
func (i *IntHolder) setInt(newInt int) {
  i.i = newInt
}

a_test 中, 我們不會像之前那樣複製方法簽名, 而是將其改爲函數簽名, 並將方法的接收器作爲第一個參數:

package a_test
func setInt(*IntHolder, int)

爲了指定我們要鏈接的方法, 我們將使用方法接收器類型和方法名稱, 格式如下:
<methodpackage>.(<methodreceivertype>).<methodname>

我們將把它放在 linkname 指令中:

package a_test
import (
  "testing"
  _ "unsafe"
  "github.com/YardenLaif/example/a"
)
//go:linkname setInt github.com/YardenLaif/example/a.(*IntHolder).setInt
func setInt(*a.IntHolder, int)
func TestSetInt(t *testing.T) {
  holder := &a.IntHolder{}
  setInt(holder, 2)
}

值接收器方法

鏈接到值接收器方法的方式是反直覺的, 因爲它與鏈接到指針接收器是一樣的。該函數將有一個指針參數, 即使該方法有一個值接收器。因此, 對於這個私有方法:

package a
type IntHolder struct {
  i int
}
func (i IntHolder) getInt() int {
  return i.i
}

我們將傳遞一個指針 (而不是原始方法中傳遞的值!) 作爲第一個參數:

package a_test
import (
  "testing"
  _ "unsafe"
  "github.com/YardenLaif/example/a"
)
//go:linkname getInt github.com/YardenLaif/example/a.(*IntHolder).getInt
func getInt(*a.IntHolder) int
func TestGetInt(t *testing.T) {
  holder := &a.IntHolder{}
  i := getInt(holder)
  ...
}

私有類型

沒有編譯器技巧可以訪問私有類型, 但這個技巧將允許您訪問私有 (或公共) 類型上的私有字段。結合鏈接名指令, 這將使您能夠訪問私有類型上的私有方法。

私有字段

我們首先在 a 包中定義一個私有字段, 並定義一個返回該類型的函數:

package a
type internalType struct {
  internalField int
}
func GetInternalType() internalType {
  return internalType{internalField: 3}
}

接下來, 我們將在 a_test 包中複製該類型:

package a_test
type a_internalType struct {
  internalField int
}

最後, 我們將在需要的地方將 ainternalType 轉換爲 a_testa_internalType。我們將通過以下方式使用指針和 unsafe.Pointer 來實現這一點:

i := a.GetInternalType()
convertedI := *(*a_internalType)(unsafe.Pointer(&i))

現在, 我們可以使用 convertedI 並訪問 internalField。再次強調, 這不是一個推薦做法, 必須小心使用。

package a_test
import (
  "testing"
  "unsafe"
  "github.com/YardenLaif/example/a"
)
type a_internalType struct {
  internalField int
}
func TestInternalType(t *testing.T) {
  i := a.GetInternalType()
  convertedI := *(*a_internalType)(unsafe.Pointer(&i))
  internalField := convertedI.internalField
  ...
}

私有方法

最後但並非最不重要的是訪問私有類型上的私有方法的能力。

我們將在 a 包中定義一個私有類型的私有方法, 並定義一個返回該類型的函數:

package a
type internalType struct {
  internalField int
}
func (i *internalType) internalMethod() { ... }
func GetInternalType() internalType {
  return internalType{internalField: 3}
}

再次, 我們將複製內部類型並使用指針將 ainternalType 對象轉換爲 a_testinternalType:

package a_test
import (
  "testing"
  "unsafe"
  "github.com/YardenLaif/example/a"
)
type a_internalType struct {
  internalField int
}
func TestInternalType(t *testing.T) {
  i := a.GetInternalType()
  convertedI := *(*a_internalType)(unsafe.Pointer(&i))
  convertedI.internalMethod() // Doesn't work
  ...
}

此時, 我們可以使用鏈接名指令並將 a_testinternalType 作爲第一個參數傳遞:

package a_test
import (
  "testing"
  "unsafe"
  "github.com/YardenLaif/example/a"
)
type a_internalType struct {
  internalField int
}
//go:linkname internalMethod github.com/YardenLaif/example/a.(*internalType).internalMethod
func internalMethod(a_internalType)
func TestInternalType(t *testing.T) {
  i := a.GetInternalType()
  convertedI := *(*a_internalType)(unsafe.Pointer(&i))
  internalMethod(&convertedI)
  ...
}

這樣做是因爲 go 編譯器不檢查函數聲明是否與函數定義匹配。在這種情況下, 這對我們有利, 但這也是使用鏈接名很危險並可能導致運行時崩潰的另一個原因。

結語

如果你努力嘗試並願意冒代碼安全的風險, 在 Go 中幾乎可以做任何事情。使用這裏編寫的 "黑客" 技術, 您可以訪問幾乎任何內容, 而不管創建者的本意如何。 就個人而言, 我希望這不會對你有所幫助, 因爲這些做法確實不被推薦。但是, 我確實希望這引起了你的興趣!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/2JceVPLAeJYb6-nxIhB0bw