Yarden Laifenfeld · Follow
6 min read · Just now
Have you ever found yourself cursing at your computer because you needed to access a private function, method, global variable or type in Go? Did it seem practically impossible? Well — it's not, and this blog post is for you. Everything written below should NOT be considered a recommendation or an example of good Go code design.
The way to distinguish between private and public in Go is by the first letter of the name. If the letter is lowercase, it is private. If it is uppercase, it is public. The scope of a private function/method/type is the package in which it is defined.
It's possible to write large Go programs without resorting to using a hack to access private information. In fact, it's advisable to avoid doing so. That said, there are extreme circumstances that require such a hack. That, along with pure curiosity, led me to write this blog post. Let’s begin!
To access private functions, global variables and methods, you can use the linkname compiler directive. In a sentence, this directive allows you to link a declaration in one place to a definition in another. When the compiler sees the linkname comment, it makes the declaration a “link” (hence the name) to the original definition.
The solution that allows access to private types is slightly less conventional, so we'll dive into that later.
Go runtime
Let's start with the most common use case: running a private function from the Go runtime. Many public functions in the standard library have private alternatives in the runtime that perform better but have certain limitations. An example of such a function is rand.Uint32
and its private runtime counterpart runtime.fastrand
. runtime.fastrand
does not allow seeding, but it performs much better than rand.Uint32
.
Let's say we have a function that generates and uses a random number:
func funcWithRandomNumber() {
randomNumber := rand.Uint32()
...
}
We can't just swap out rand.Uint32
with an internal implementation:
func funcWithRandomNumber() {
randomNumber := runtime.fastrand() // Doesn't work
...
}
Instead, we'll copy the runtime.fastrand
function signature (the names here don't matter, only the types):
func runtime_fastrand() uint32
To "link" our function declaration with the definition of runtime.fastrand
, we'll add the linkname compiler directive in the format
//go:linkname <declaration name> <definition package>.<definition name>
In this case, that would look like:
//go:linkname runtime_fastrand runtime.fastrand
func runtime_fastrand() uint32
Now, we can use runtime_fastrand
as we would use runtime.fastrand
:
//go:linkname runtime_fastrand runtime.fastrand
func runtime_fastrand() uint32func funcWithRandomNumber() {
randomNumber := runtime_fastrand()
...
}
User package
Similarly, we can link a private function from a user-written package to a function definition.
For this example, our module name will be github.com/YardenLaif/example. Under example
, we have the following directories and files:
.
├── a/
│ └── a.go
└── a_test/
└── a_test.go
In a.go
there is a private function named foo
:
package afunc foo(i int) bool { ... }
We'd like to test this function in a_test.go
, so we'll use the same format for the linkname directive and add the full path for the a
package:
package a_testimport (
"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)
...
}
Global Variables
Global variables can be linked in the same way. If our a
package had a global variable:
package avar globalVar = 55
We can access globalVar
from the a_test
package using the linkname directive. I’ve found that importing the package the global variable is defined in is necessary when working with global variables.
package a_testimport (
"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 {
...
}
}
This might look confusing, but the globalVar
defined in a
and the a_globalVar
defined in a_test
are the same variable. That means that its initial value is 55
and any changes in one will affect the other.
Please note: it's not possible to link to const
s or non-global variables.
Pointer Receiver Methods
We’re differentiating between pointer receiver methods: func (t *Type) foo()
, and value receiver methods: func (t Type) foo()
. For the latter, skip to the next section.
Methods deserve their own section since they require linking a function to a method. We'll define a private method with a pointer receiver in the a
package:
package atype IntHolder struct {
i int
}
func (i *IntHolder) setInt(newInt int) {
i.i = newInt
}
In a_test
, instead of copying the method signature as-is, we'll change it to a function signature and have the method's receiver be the first parameter:
package a_testfunc setInt(*IntHolder, int)
To specify the method we're linking to, we'll use the method receiver type along with the method's name in the following format:<method package>.(<method receiver type>).<method name>
We'll put that in the linkname directive:
package a_testimport (
"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)
}
Value Receiver Methods
The way to link to value receiver methods is counterintuitive because it’s the same as linking to a pointer receiver. The function will have a pointer parameter even though the method has a value receiver. So, for this private method:
package atype IntHolder struct {
i int
}
func (i IntHolder) getInt() int {
return i.i
}
We’ll pass a pointer (not a value, as is passed in the original method!) as the first parameter:
package a_testimport (
"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)
...
}
There is no compiler trick to access private types, but this trick will allow you to access private fields on private (or public) types. Combined with the linkname directive, this will enable you to access private methods on private types.
Private Fields
We'll start by defining a private field in a type in the a
package, and a function that returns that type:
package atype internalType struct {
internalField int
}
func GetInternalType() internalType {
return internalType{internalField: 3}
}
Next, we'll copy the type in the a_test
package:
package a_testtype a_internalType struct {
internalField int
}
Finally, we'll convert a
's internalType
to a_test
's a_internalType
wherever necessary. We’ll do this by playing with pointers and unsafe.Pointer
in the following way:
i := a.GetInternalType()
convertedI := *(*a_internalType)(unsafe.Pointer(&i))
Now, we can use convertedI
and access internalField
. Once again, this is not a recommendation and must be used carefully.
package a_testimport (
"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
...
}
Private Methods
Last but certainly not least is the ability to access private methods on private types.
We’ll define a private method of a private type in the a
package, and a function that returns that type:
package atype internalType struct {
internalField int
}
func (i *internalType) internalMethod() { ... }
func GetInternalType() internalType {
return internalType{internalField: 3}
}
Once again, we’ll copy the internal type and use pointers to convert an object of a
's internalType
to a_test
's internalType
:
package a_testimport (
"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
...
}
At this point, we can use the linkname directive and pass a_test
's internalType
as the first parameter:
package a_testimport (
"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)
...
}
This works because the go compiler doesn’t check that the function declaration matches the function definition. In this case, that works in our favor, but it’s also another reason using linkname is dangerous and can cause runtime crashes.
There is a way to do almost anything in Go if you try hard enough and are willing to risk your code's safety. Using the "hacks" written here lets you access practically anything regardless of the creator's intentions.
On a personal note, a small part of me hopes this doesn't help you, as these practices genuinely aren't recommended. However, I do hope this piqued your interest!