在阅读time包源码的时候,遇到了一个特别奇怪的函数定义,类似于C语言中的函数声明一样,只有函数定义,没有函数体,代码如下:
func startTimer(*runtimeTimer)
func stopTimer(*runtimeTimer) bool
我当时非常惊讶,以为自己学的是假的go语法,当问了万能的谷歌之后,我得到的答案是如下,参见 go中的定时器timer
AfterFunc很简单,就是把参数封装为runtimeTimer,然后启动timer(把timer添加到队列中), 这部分代码在runtime/time.go中,注意这里goFunc新启动了一个goroutine来执行用户的任务,这样用户的func就不会堵塞timer
重点在这一句:这部分代码在runtime/time.go中
于是就去runtime/time,go 中寻找这两个函数的详细实现:
// startTimer adds t to the timer heap.
//go:linkname startTimer time.startTimer
func startTimer(t *timer) {
if raceenabled {
racerelease(unsafe.Pointer(t))
}
addtimer(t)
}
// stopTimer removes t from the timer heap if it is there.
// It returns true if t was removed, false if t wasn't even there.
//go:linkname stopTimer time.stopTimer
func stopTimer(t *timer) bool {
return deltimer(t)
}
于是新的疑惑又涌上心头,go:linkname
又是什么东西呢?于是我去查了go的官方文档,得到的是这样的答案:
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)函数或者变量实现导出功能。由于这种方法是破坏了go语言的模块化规则的,所以必须在导入了"unsafe"
包的情况下使用。
我们现在基本清楚了,为什么需要go:linkname
的原因:
由于go语法规则限制,小写字母开头的函数或者变量是本模块私有的,不可被包外的代码访问;但是如果必须要能被外部模块访问到,又要限制为私有方法呢?只能在编译器上做手脚,通过一个特殊的
标记
来实现这种功能
清楚了go:linkname的使用场景,那我们如何使用它呢?下面是个简单的例子:
文件树如下:
Timer
├── controller
│ ├── b.go
│ └── xxx.s
├── main.go
└── view
└── a.go
文件a.go内容:
package view
import (
"fmt"
_ "unsafe"
)
//go:linkname printHelloWorld Timer/controller.printHelloWorld
func printHelloWorld() {
fmt.Println("hello world!")
}
func PrintHelloWorld() {
fmt.Println("hello world!")
}
文件b.go内容:
package controller
import _ "Timer/view"
func printHelloWorld()
func PrintHelloWorld() {
printHelloWorld()
}
文件 main.go 内容:
package main
import (
"Timer/controller"
)
func main() {
controller.PrintHelloWorld()
//view.PrintHelloWorld()
}
但是当我运行代码的时候发现了如下问题:
# Timer/controller
controller/b.go:5:6: missing function body
在经过谷歌之后,才知道是如下原因:
Go在编译的时候会启用
-complete
编译器flag,它要求所有的函数必需包含函数体。创建一个空的汇编语言文件绕过这个限制
于是我在controller目录下创建一个xxx.s的空文件,当存在 .s
文件时,编译器则不会再加 -complete
参数;完整的文件树如下:
Timer
├── controller
│ ├── b.go
│ └── xxx.s
├── main.go
└── view
└── a.go
于是我们运行,终于成功了:
➜ Timer go run main.go
hello world!
以上介绍了go:linkname 的用法,但是这种方法会破坏代码的模块性,所以不建议在业务代码中使用这种方式。