I write code and talk about writing code

Accessing Private Functions, Methods, Types and Variables in Go

Posted on Aug 26, 2023

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.

Background

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!

//go:linkname

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:

1
2
3
4
func funcWithRandomNumber() {
  randomNumber := rand.Uint32()
  ...
}

We can’t just swap out rand.Uint32 with an internal implementation:

1
2
3
4
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):

1
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

1
//go:linkname <declaration name> <definition package>.<definition name>

In this case, that would look like:

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

Now, we can use runtime_fastrand as we would use runtime.fastrand:

1
2
3
4
5
6
7
//go:linkname runtime_fastrand runtime.fastrand
func runtime_fastrand() uint32

func 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:

1
2
3
4
5
.
├── a/
│   └── a.go
└── a_test/
    └── a_test.go

In a.go there is a private function named foo:

1
2
3
package a

func 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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)
  ...
}

Global Variables

Global variables can be linked in the same way. If our a package had a global variable:

1
2
3
package a

var 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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 {
    ...
  } 
}

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 consts 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:

1
2
3
4
5
6
7
8
9
package a

type 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:

1
2
3
package a_test

func 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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)
}

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:

1
2
3
4
5
6
7
8
9
package a

type 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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)
  ...
}

Private Types

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:

1
2
3
4
5
6
7
8
9
package a

type internalType struct {
  internalField int
}

func GetInternalType() internalType {
  return internalType{internalField: 3}
}

Next, we’ll copy the type in the a_test package:

1
2
3
4
5
package a_test

type 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:

1
2
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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
  ...
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package a

type 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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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
  ...
}

At this point, we can use the linkname directive and pass a_test’s internalType as the first parameter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)
  ...
}

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.

Closing Notes

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!