Suppose there is an internal package that provides a method as follows.

1
2
3
4
5
6
7
package internal

import "fmt"

func print(msg string) {
    fmt.Println("[internal]", msg)
}

This method is used internally, it has no export properties, so it can’t be imported by other external packages, so since this is the case, is there any way to call this method outside the package? The answer is yes, except that this hack blocks at least 80% of Gopher’s knowledge, and it is go:linkname.

1. go:linkname Basics

Before understanding go:linkname, it is necessary to understand the internal package internal, which is unique to Golang. go 1.4 “Internal” Packages were added in Go 1.14.

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.

The simple understanding is that this particular internal package can only be imported by a specific external package.

  • Package /a/b/c/internal/d/e/f can only be imported by /a/b/c, not by /a/b/d.
  • Package $GOROOT/src/pkg/internal/xxx, can only be imported by $GOROOT/src/.
  • Package $GOROOT/src/pkg/net/http/internal can only be imported by net/http and net/http/*.
  • Package $GOPATH/src/mypkg/internal/foo can only be imported by $GOPATH/src/mypkg.

How can I directly reference the internal.print method without violating this principle?

The //go:linkname directive instructs the compiler to use importpath.name as the object file symbolic name of a variable or function declared as localname in the source code. Since this directive can break the type system and package modularity, it is only enabled in files that have unsafe imported. As follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import (
    _ "demo/internal"
    _ "unsafe"
)

//go:linkname Print demo/internal.print
func Print(data string)

func main() {
    Print("hello world")
}

This completes the process of go:linkname pointing the method implementation to an unexported method implementation of an external package. In simple terms, this means that go:linkname [local] [target] binds the specific implementation target to the current local method. When run directly, it prompts a missing body error. This is because go build adds the -complete parameter to check for completeness, and apparently this Print method has no body. Therefore, you need to tell the compiler to bypass this restriction by adding the xxx.s file to the calling directory. Finally the whole file directory is as follows.

1
2
3
4
5
.
├── go.mod
├── internal
│   └── internal.go
└── main.go

The output after running is as follows.

1
2
# go run *.go
[internal] hello world

2. go:linkname advanced 1: random numbers

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

Both runtime.fastrand and math.Rand are pseudo-random number generators. But the difference is that runtime.fastrand is in the context of the current goroutine. Therefore, it does not require locking during frequent calls, so its performance is much better than that of `math. Here are the performance tests of both.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
    "math/rand"
    "testing"
)

func BenchmarkMathRand(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = rand.Int()
    }
}

func BenchmarkRuntimeRand(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = FastRand()
    }
}

The performance data obtained from benchmarking shows that runtime.Rand crushes math/rand in terms of performance.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Running tool: /usr/bin/go test -benchmem -run=^$ -coverprofile=/tmp/vscode-goVPugfM/go-code-cover -bench . demo

goos: linux
goarch: amd64
pkg: demo
BenchmarkMathRand    	91929873	        12.8 ns/op	       0 B/op	       0 allocs/op
BenchmarkRuntimeRand 	316043065	         3.71 ns/op	       0 B/op	       0 allocs/op
PASS
coverage: 0.0% of statements
ok  	demo	2.750s

3. go:linkname Advanced 2: Timestamp

1
2
//go:linkname nanotime1 runtime.nanotime1
func nanotime1() int64

time.Now() and runtime.nanotime1() both get timestamps, but time.Now() has underlying calls to runtime.walltime1 and runtime.nanotime to get the timestamp and program runtime respectively. And the latter only needs to get the timestamp separately. Therefore, in some scenarios, such as statistical time consumption, then you can directly get better performance with nanotime1(). The following is the benchmarking code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
    "testing"
    "time"
)

func BenchmarkTimeNow(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = time.Now()
    }
}

func BenchmarkRuntimeNanotime(b *testing.B) {
    for i := 0; i < b.N; i++ {
        _ = nanotime1()
    }
}

As you can see, runtime.nanotime1() crushes time.Now() in terms of performance.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Running tool: /usr/bin/go test -benchmem -run=^$ -coverprofile=/tmp/vscode-goVPugfM/go-code-cover -bench . demo

goos: linux
goarch: amd64
pkg: demo
BenchmarkTimeNow         	10140447	       117 ns/op	       0 B/op	       0 allocs/op
BenchmarkRuntimeNanotime 	22762699	        55.7 ns/op	       0 B/op	       0 allocs/op
PASS
coverage: 0.0% of statements
ok  	demo	2.635s

4. Summary

With an understanding of how go:linkname works, we can optimize our code for specific scenarios and improve performance bottlenecks. When reading the golang source code, you can also see a lot of go:linkname directives, understanding this directive helps us better understand the underlying logic of golang code.