之前写过一篇文章:为什么Go标准库中有些函数只有签名没有函数体?其中之一是//go:linkname指令。Go中有很多类似的指令,比如Go1.16中的//go:embed。前几天有人问我,为什么用//go:embed不行?我看到是这样写的://go:embed不知道你看出问题了吗?是的,命令是通过Comment方法,但是有三个要求,特别注意://后面不能有空格。有些人可能习惯在//后面不加空格。但是一般认为//后面应该加一个空格。但是,go命令不需要空格。这是一个小“坑”,要注意。所以上面这位朋友只是加了一个空格,结果就出问题了。(程序不报错,只是得不到你想要的结果)代码和指令之间不能有空行或其他注释。这个应该没问题,很多人不会用错;一般来说,需要导入相应的包才能使用该命令。比如//go:linkname命令需要导入unsafe包,通常是import_"unsafe",//go:embed命令需要导入embed包。另一位围棋小伙伴“橘子小姐姐”微信私聊我:大哥你好,能不能写篇linkname的文章。已经有了一些初步的概念,但是还有一些疑惑不是特别清楚。//go:linknamelocalnameremotename,其中local作为占位符,remote作为实现者,或者local作为实现者,remote作为占位符。目前的理解是给Symbol加上一个Linkname,在寻找Symboll的时候使用remote。比如//go:linknameruntimeNanoruntime.nanotime,runtimeNano提供了作为占位符runtime.nanotime的实现,凡是调用runtimeNano的地方实际上都被调用runtime.nanotime代替,这种场景更容易接受。比如//go:linknameruntime_cmpstringruntime.cmpstring,runtime_cmpstring提供了runtime.cmpstring的实现作为占位符,是不是意味着此时符号表中没有runtime_cmpstring,只有runtime.cmpstring?经过简单的沟通,他写了一篇文章来解决自己的困惑。希望能帮到你。这是他写的关于//go:linkname的内容(我做了一些调整)。01格式//go:linknamelocalremoteremote可以省略。这时候remote使用的是local的值,效果是local被导出了。02local和remote都是函数local的占位符,remote作为实现者标准库中的例子://来自时间包//go:linknameruntimeNanoruntime.nanotimefuncruntimeNano()int64//来自运行时包//go:nosplitfuncnanotime()int64{returnnanotime1()}此时二进制文件中没有runtimeNano,直接转化为调用runtime.nanotime。作为实现者的本地和作为占位符的远程也来自标准库。存在一个没有函数体但被反向引用的函数。//在标准库的内部//go:linknameruntime_cmpstringruntime.cmpstringfuncruntime_cmpstring(a,bstring)int{l:=len(a)iflen(b)c2{return+1}}iflen(a)len(b){return+1}return0}//来自runtimefunccmpstring(string,string)int此时二进制文件中没有runtime_cmpstring,对应的函数已经命名为runtime.cmpstring。即实现在内部包中,但最终是通过runtime.cmpstring来引用的。一个占位符+一个汇编函数//在标准库的内部//go:linknameabigen_runtime_memequalruntime.memequalfuncabigen_runtime_memequal(a,bunsafe.Pointer,sizeuintptr)bool注意runtime.memequal的实现不在runtime包中,如果是implementedusingassembly不需要在对应的包中。#memequal(a,bunsafe.Pointer,sizeuintptr)boolTEXTRuntimememequal(SB),NOSPLIT,$0-25MOVQa+0(FP),SIMOVQb+8(FP),DICMPQSI,DIJEQeqMOVQsize+16(FP),BXLEAQret+24(FP),AXJMPmemeqbody<>(SB)eq:MOVB$1,ret+24(FP)RET03本地和远程都是正则变量zeroValvarzeroVal[maxZero]byte//go:linkname_iscgoruntime.iscgovar_iscgobool=true//go:cgo_import_staticx_cgo_setenv//go:linknamex_cgo_setenvx_cgo_setenv//go:linkname_cgo_setenvruntime._cgo_setenvvarx_cgo_setenvbytevar_cgo_setenv=&x_cgo_setenv//go:cgo_import_staticx_cgo_unsetenv//go:linknamex_cgo_unsetenvx_cgo_unsetenv//go:linkname_cgo_unsetenvruntime._cgo_unsetenvvarx_cgo_unsetenvbytevar_cgo_unsetenv=&x_cgo_unsetenv占位符+虚拟符号//go:linknameruntime_inittaskruntime..inittaskvarruntime_inittaskinitTask//go:linknamemain_inittaskmain..inittaskvarmain_inIttaskinitTask注意..inittask不是.inittask,.inittask只存在于编译阶段,不能在任何包中声明这个变量。这是为什么..inittask有两个点的额外解释。第一点是正常的运行时。在这个调用方法中,第二个点和inittask组成一个符号(变量)。注意Go中的变量不允许以.开头,所以这叫做伪符号,只存在于非编译阶段。04一个例子学习//go:linkname是因为有以下背景:Java有InheritableThreadLocal,SpringWeb在ServletActionContext中使用它,可以方便的在任何地方获取HttpServletRequest。Go没有提供类似的机制。即使通过stack找到goroutineid(99%的文章都是这样介绍的),结合sync.Map也只是实现了一个比较粗略的ThreadLocal,在子协程中是获取不到的。父协程的内容。g.label虽然不是为这种场景准备的,但是它具备了InheritableThreadLocal的所有需求。只要我们可以访问标签的私有字段,我们就有了完整版的InheritableThreadLocal。下面的例子是在笔者的真实项目中使用的。runtime和runtime/pprof包中有两个函数:runtime_setProfLabel和runtime_getProfLabel。其中,提供了runtime包中的实现,没有提供pprof中的实现。如果基于它们创建额外的函数,如下:/运行。runtime_getProfLabel提供了一个函数实现,但是它并没有出现在二进制文件中(见下面的代码)。这时候如果要调用它,就必须通过runtime/pprof.runtime_setProfLabel/runtime/pprof.runtime_getProfLabel,这也是上面linkname给pprof的root而不是runtime的原因。//来自运行时包//go:linknameruntime_setProfLabelruntime/pprof.runtime_setProfLabelfuncruntime_setProfLabel(labelsunsafe.Pointer){ifraceenabled{racereleasemerge(unsafe.Pointer(&labelSync))}getg().labels=labels}//来自运行时/pprof包funcruntime_setProfLabel(labelsun.Pointer)//来自runtime包//go:linknameruntime_getProfLabelruntime/pprof.runtime_getProfLabelfuncruntime_getProfLabel()unsafe.Pointer{returngetg().labels}//来自runtime/pprof包funcruntime_getProfLabel()unsafe.Pointer05总结一下有很多指令在Go中,你可能不需要关心一些指令,也不会使用它们。不过,有些指令是明白什么意思的,阅读相关代码还是有帮助的。本文全面介绍了//go:linkname命令。不知道有没有彻底解决你的疑惑呢?欢迎留言交流!本文转载自微信公众号「polarisxu」,可通过以下二维码关注。转载本文请联系polarisxu公众号。