背景
最近在看go的源码的时候陆续看到很多在注释中使用的go指令,看的让人很是懵逼,本文主要是针对go源码中的这些注释中使用go指令的地方进行详细介绍
常见的注释中使用go指令集合
我在go的源码里搜罗下关键字统计下有以下几类常见的注释中使用的go指令
来源一览
// 来源 src/runtime/internal/atomic/atomic\_wasm.go
// go:linkname Load
// 来源 src/runtime/syscall\_solaris.go
//go:linkname pipe1x runtime.pipe1
// 来源 src/runtime/syscall\_aix.go
//go:cgo\_import\_dynamic libc\_chdir chdir "libc.a/shr\_64.o"
// 来源 src/runtime/sys\_openbsd2.go
//go:build (openbsd && amd64) || (openbsd && arm64)
// 来源 src/runtime/internal/atomic/atomic\_riscv64.go
//go:noescape
// 来源 src/cmd/compile/internal/test/shift\_test.go
//go:noinline
// 来源 src/cmd/go/main.go
//go:generate ./mkalldocs.sh
// 来源 src/embed/internal/embedtest/embed\_test.go
//go:embed testdata/h\*.txt
//go:embed c\*.txt testdata/g\*.txt
var global embed.FS
// 来源 src/runtime/internal/atomic/atomic\_wasm.go
//go:nosplit
//go:noinline
func Load(ptr \*uint32) uint32 {
return \*ptr
}
// 来源 src/syscall/syscall\_linux.go
//go:nosplit
//go:norace
func (pc \*allThreadsCaller) doSyscall(initial bool) bool {
r1, r2, err := RawSyscall(pc.trap, pc.a1, pc.a2, pc.a3)
常见的注释中使用的go指令预览
指令名称
使用方法
备注
linkname
//go:linkname local remote
remote 可以没有,此时 remote 使用 local 的值,效果就是 local 被导出
build
//go:build (openbsd && amd64) || (openbsd && arm64)
generate
//go:generate ./mkalldocs.sh
当运行go generate时,它将扫描与当前包相关的源代码文件,找出所有包含"//go:generate"的特殊注释,提取并执行该特殊注释后面的命令,命令为可执行程序,形同shell下面执行
cgo\_import\_dynamic
//go:cgo\_import\_dynamic libc\_res\_search res\_search "/usr/lib/libSystem.B.dylib"
这是一个链接器指令,告诉可以从动态库(LIBC)中加载某个功能和能力
当链接器在编译后运行时,它将执行此指令。链接器res\_search从libSystem动态库(在给定路径中找到)中提取例程。然后,该例程将名称命名为在Go的汇编代码中可引用的例程libc\_res\_search。
noinline
//go:noinline
字面意思“不要内联”
InLine,是在编译期间发生的,将函数调用调用处替换为被调用函数主体的一种编译器优化手段。
nosplit
//go:nosplit
跳过栈溢出检测
noescape
//go:noescape
禁止逃逸,而且它必须指示一个只有声明没有主体的函数
norace
//go:norace
跳过竞态检测
embed
//go:embed
静态资源嵌入功能 go 1.16支持的新功能
注释中使用go指令方法
// 后不能有空格。有些人可能习惯 // 后不加空格。goland的IDE会在注释后面自动加上空格,这个坑还是有恶心
go:linkname
//go:linkname local remote
- local remote 可以是占位符和函数
- remote如果缺省的话表示 local 被导出
- local 作为占位符,remote 作为实现者
local 作为实现者,remote 作为占位符
- 也存在 一个占位符+一个汇编函数
go:embed
使用正则规则描述
静态资源嵌入功能,详情引入规则如下
//go:embed images
这是匹配所有位于/tmp/proj/images及其子目录中的文件
//go:embed images/jpg/a.jpg
匹配/tmp/proj/images/jpg/a.jpg这一个文件
//go:embed a.txt
匹配/tmp/proj/a.txt
//go:embed images/jpg/\*.jpg
匹配/tmp/proj/images/jpg下所有.jpg文件
//go:embed images/jpg/a?.jpg
匹配/tmp/proj/images/jpg下的a1.jpg a2.jpg ab.jpg等
//go:embed images/??g/\*.\*
匹配/tmp/proj/images下的jpg和png文件夹里的所有有后缀名的文件,例如png/123.png jpg/a.jpeg
//go:embed \*
直接匹配整个/tmp/proj
//go:embed a.txt
//go:embed \*.png \*.jpg
//go:embed aa.jpg
可以指定多个//go:embed指令行,之间不能有空行,也可以用空格在一行里写上对个模式匹配,表示匹配所有这些文件,相当于并集操作
可以包含重复的文件或是模式串,golang对于相同的文件只会嵌入一次,很智能
如何使用embed
//go:embed imgs/screenrecord.gif
var gif \[\]byte
//go:embed imgs/png/a.png
var png \[\]byte
func main() {
fmt.Println("gif size:", len(gif)) // gif size: 81100466
fmt.Println("png size:", len(png)) // png size: 4958264
}
go:generate
go generate命令是go 1.4版本里面新添加的一个命令,当运行go generate时,它将扫描与当前包相关的源代码文件,找出所有包含"//go:generate"的特殊注释,提取并执行该特殊注释后面的命令,命令为可执行程序,形同shell下面执行
使用场景
在有些场景下,我们会使用go generate:
在build之前生成一些特定文件(下文介绍)
yacc:从 .y 文件生成 .go 文件.
protobufs:从 protocol buffer 定义文件(.proto)生成 .pb.go 文件。
Unicode:从 UnicodeData.txt 生成 Unicode 表.
注意事项
必须在.go源码文件中
每个源码文件可以包含多个generate特殊注释时
显示运行go generate命令时,才会执行特殊注释后面的命令
如果前面的注释执行出错,则终止执行
go:noinline 不要内联
首先搞清楚什么是内联:内联是在编译期间发生的,将函数调用调用处替换为被调用函数主体的一种编译器优化手段
使用 InLine 有一些优势,同样也有一些问题。
使用优势:
减少函数调用的开销,提高执行速度。
复制后的更大函数体为其他编译优化带来可能性,如过程间优化
消除分支,并改善空间局部性和指令顺序性,同样可以提高性能。
使用问题:
代码复制带来的空间增长。
如果有大量重复代码,反而会降低缓存命中率,尤其对 CPU 缓存是致命的。
所以,在实际使用中,对于是否使用内联,要谨慎考虑,并做好平衡,以使它发挥最大的作用。
简单来说,对于短小而且工作较少的函数,使用内联是有效益的。
go:nosplit 跳过栈溢出检测
栈溢出是什么?
正是因为一个 Goroutine 的起始栈大小是有限制的,且比较小的,才可以做到支持并发很多 Goroutine,并高效调度。
stack.go 源码中可以看到,\_StackMin 是 2048 字节,也就是 2k,它不是一成不变的,当不够用时,它会动态地增长。
那么,必然有一个检测的机制,来保证可以及时地知道栈不够用了,然后再去增长。
回到话题,nosplit 就是将这个跳过这个机制。
优劣
显然地,不执行栈溢出检查,可以提高性能,但同时也有可能发生 stack overflow 而导致编译失败。
go:noescape 禁止逃逸
禁止逃逸,而且它必须指示一个只有声明没有主体的函数
逃逸是什么?
Go 相比 C、C++ 是内存更为安全的语言,主要一个点就体现在它可以自动地将超出自身生命周期的变量,从函数栈转移到堆中,逃逸就是指这种行为
优劣
最显而易见的好处是,GC 压力变小了。
因为它已经告诉编译器,下面的函数无论如何都不会逃逸,那么当函数返回时,其中的资源也会一并都被销毁。不过,这么做代表会绕过编译器的逃逸检查,一旦进入运行时,就有可能导致严重的错误及后果。
go:norace 跳过竞态检测
在多线程程序中,难免会出现数据竞争,正常情况下,当编译器检测到有数据竞争,就会给出提示
执行 go run -race main.go 利用 -race 来使编译器报告数据竞争问题。
优劣
使用 norace 除了减少编译时间,我想不到有其他的优点了。但缺点却很明显,那就是数据竞争会导致程序的不确定性。
go:build
在源代码里添加标注,通常称之为编译标签( build tag)
编译标签是在尽量靠近源代码文件顶部的地方用注释的方式添加
go build在构建一个包的时候会读取这个包里的每个源文件并且分析编译便签,这些标签决定了这个源文件是否参与本次编译
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
…
//go:build !linux
go: cgo\_import\_dynamic
//go:cgo_import_dynamic libc_res_search res_search "/lib/libSystem.B.dylib"
当编辑器在编译后运行时,它将执行此指令。编辑器res\_search从libSystem动态库(在给定路径中找到)中提取列程。然后,该进程将名称命名为在Go的汇编代码中可引用的例程libc\_res\_search
我们就能够使用go tool cgo -dynimport命令查看所有从libc中的导入的功能信息
参考文章
https://www.grant.pizza/blog/...
https://blog.csdn.net/weixin\_44264674/article/details/107780595
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。