假设有一个内部包,它提供一个方法如下:
package internal
import "fmt"
func print(msg string) {
fmt.Println("[internal]", msg)
}
这个方法是内部使用的,它没有导出属性,因此它无法被其他外部包import,那既然如此,那有没有办法能在包外部去调用这个方法呢?答案是肯定的,只不过这个黑科技屏蔽了至少80%的Gopher的认知,它就是go:linkname。
01 go:linkname基础
在理解go:linkname之前,需要先理解Golang中独有的内部包internal。在Go1.14中加入了Go 1.4 “Internal” Packages
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包只能被特定外部包导入:
包/a/b/c/internal/d/e/f只能被/a/b/c导入,而不能被/a/b/d导入。
包·$GOROOT/src/pkg/internal/xxx
,只能被$GOROOT/src/
导入。
包$GOROOT/src/pkg/net/http/internal
只能被net/http和net/http/*
导入。
包$GOPATH/src/mypkg/internal/foo
只能被$GOPATH/src/mypkg
导入。
在不违反这个原则的情况下,如何直接引用internal.print这个方法呢?
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".
//go:linkname指令指示编译器使用importpath.name作为源代码中声明为localname的变量或函数的对象文件符号名。由于该指令可以破坏类型系统和包模块化,因此仅在导入了unsafe的文件中启用该指令。如下:
package main
import (
_ "demo/internal"
_ "unsafe"
)
//go:linkname Print demo/internal.print
func Print(data string)
func main() {
Print("hello world")
}
这样就完成了go:linkname将方法实现指向一个外部包未导出的方法实现了。简单理解,就是通过go:linkname [local] [target]为当前这个local方法绑定其具体实现target。当直接运行时会提示missing body的错误。这是因为go build会增加-complete参数检查完整性,显然这个Print方法是没有包体的。因此,需要告知编译器绕过这个限制,在调用目录下增加xxx.s文件即可。最后整个文件目录如下:
.
├── go.mod
├── internal
│ └── internal.go
└── main.go
运行后输出:
# go run *.go
[internal] hello world
02 go:linkname进阶1:随机数
//go:linkname FastRand runtime.fastrand
func FastRand() uint32
runtime.fastrand和math.Rand都是伪随机数生成器,但不同的是runtime.fastrand是在当前goroutine上下文环境下的,因此在频繁调用过程中不需要加锁,所以,它的性能要比math.Rand要好得多。以下是两者的性能测试:
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()
}
}
执行得到基准性能数据,可见runtime.Rand在性能上碾压math/rand:
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
03 go:linkname进阶2:时间戳
//go:linkname nanotime1 runtime.nanotime1
func nanotime1() int64
time.Now()和runtime.nanotime1()两者都是获取时间戳,但是time.Now()其底层调用了runtime.walltime1和runtime.nanotime,分别获取时间戳和程序运行时间。而后者只需要单独获取时间戳。因此,在某些场景下,比如统计耗时,那么就可以直接通过nanotime1()获得更好的性能。以下为基准测试代码:
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()
}
}
执行得到基准性能数据,可见runtime.nanotime1()在性能上碾压time.Now():
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
04 总结
在理解go:linkname的原理后,我们就可以举一反三,针对特定场景来优化代码,提高性能瓶颈。这个指令虽然可以绕过限制访问到其他包的私有方法,但不失为一种巧技。在阅读golang的源码时也可以看到大量的go:linkname指令,因此有助于我们更好理解golang代码的底层逻辑。