//go:linkname

由于go是按照首字母大小写决定是否可以被外部包引用的。所以,如果我们想方位某个包中的私有成员,就需要用到go:linkname了,也就是说我们可以通过 //go:linkname localname linkname 这种方式将本地的私有函数/变量提供给外部使用。但是经过试验,常量是不可以的。同时还有一个 //go:nosplit 在源码中也是经常出现的,其实就是告诉编译器,下面的函数不会产生堆栈溢出,不需要插入堆栈溢出检查。

具体的使用有大致有两种
1. 通过 //go:linkname localname linkname 将函数 localname 连接到 当前包(a),然后在使用的时候(b)同样需要 通过此命令连接到之前的符号。也就是 a.go 在函数上加 //go:linkname say a.say 此时无论使用方采用首字母大写关联还是小写进行管理,均不能直接使用。 之后 b.go 中

  1. ----------------------------定义处----------------------------
  2. //go:linkname say a.say
  3. //go:nosplit
  4. func say(name string) string {
  5. return "say:hello," + name
  6. }
  7. ----------------------------使用处----------------------------
  8. //go:linkname sayTest a.say
  9. func sayTest(name string) string
  10. func Greet(name string) string {
  11. return sayTest(name)
  12. }
  1. 不需要在使用的时候在用 go:linkname 去关联,只需要在定义处直接 关联到目标包的函数即可 ```go //go:linkname aaaStr commons/go-linkname-test/b.AAAStr var aaaStr = “1111”

//go:linkname name commons/go-linkname-test/b.Name const name = “33333”

//将 say2 连接到 对应的域名下的函数 //go:linkname say2 commons/go-linkname-test/b.Hi //go:nosplit 不得包含堆栈溢出检查 因为只是一个简单的字符串拼合,不需要溢出检测 func say2(name string) string { return “say2:hi,” + name }

// b.go // 如果不写此函数,a.go中的 //go:linkname say2 SourceCodeTest/go-linkname-test/b.Hi 也不会报错 func Hi(name string) string

  1. 这种方式有个弊端,就是我们在使用时,不知道实现在哪,但可以规范函数名来确定,例如把上面的代码改为
  2. ```go
  3. //将 say2 连接到 对应的域名下的函数
  4. //go:linkname say2 commons/go-linkname-test/b.A_Hi
  5. //go:nosplit 不得包含堆栈溢出检查 因为只是一个简单的字符串拼合,不需要溢出检测
  6. func say2(name string) string {
  7. return "say2:hi," + name
  8. }
  9. // b.go
  10. // 通过A_表明这个函数的实现是在A包中
  11. func A_Hi(name string) string

我们先看一下代码的结构
image.png
a.go

  1. package a
  2. import _ "unsafe"
  3. //go:linkname say a.say
  4. //go:nosplit
  5. func say(name string) string {
  6. return "say:hello," + name
  7. }
  8. //go:linkname say1 a.say1
  9. //go:nosplit
  10. func say1(name string) string {
  11. return "say1:hello," + name
  12. }
  13. //将 say2 连接到 对应的域名下的函数
  14. //go:linkname say2 commons/go-linkname-test/b.Hi
  15. //go:nosplit 不得包含堆栈溢出检查 因为只是一个简单的字符串拼合,不需要溢出检测
  16. func say2(name string) string {
  17. return "say2:hi," + name
  18. }
  19. //go:linkname aaaStr commons/go-linkname-test/b.AAAStr
  20. var aaaStr = "1111"
  21. //go:linkname name commons/go-linkname-test/b.Name
  22. const name = "33333"

b.go

  1. package b
  2. import (
  3. _ "commons/go-linkname-test/a"
  4. _ "unsafe"
  5. )
  6. var AAAStr = ""
  7. const Name = "222"
  8. //go:linkname sayTest a.say
  9. func sayTest(name string) string
  10. //即使是 大写的 但是在main包和其他任何包中,是不能直接调用的,因为
  11. //go:linkname sayHi a.say1
  12. func sayHi(name string) string
  13. func SayHii(name string) string {
  14. return sayHi(name)
  15. }
  16. func Greet(name string) string {
  17. return sayTest(name)
  18. }
  19. // 如果不写此函数,a.go中的 //go:linkname say2 SourceCodeTest/go-linkname-test/b.Hi 也不会报错
  20. func Hi(name string) string

c.go

  1. package c
  2. import (
  3. "commons/go-linkname-test/b"
  4. )
  5. func SayHii(name string) string {
  6. return b.Hi(name)
  7. }

main.go

  1. package main
  2. import (
  3. "commons/go-linkname-test/b"
  4. "commons/go-linkname-test/c"
  5. "fmt"
  6. )
  7. func main() {
  8. s := b.Greet("world")
  9. fmt.Println(s)
  10. s = b.Hi("world")
  11. fmt.Println(s)
  12. s = b.SayHii("world")
  13. fmt.Println(s)
  14. s = c.SayHii("world")
  15. fmt.Println(s)
  16. fmt.Println(b.AAAStr)
  17. fmt.Println(b.Name)
  18. }
  19. output:
  20. say:hello,world
  21. say2:hi,world
  22. say1:hello,world
  23. say2:hi,world
  24. 1111
  25. 222

编译运行报错:missing function body
在b目录下添加一个空的汇编文件 b.s 标识即可

//go:noinline

noinline 顾名思义,不要内联。
Inline,是在编译期间发生的,将函数调用调用处替换为被调用函数主体的一种编译器优化手段。
使用 Inline 有一些优势,同样也有一些问题。

优势:
  • 减少函数调用的开销,提高执行速度。
  • 复制后的更大函数体为其他编译优化带来可能性,如 过程间优化
  • 消除分支,并改善空间局部性和指令顺序性,同样可以提高性能。

    问题:
  • 代码复制带来的空间增长。

  • 如果有大量重复代码,反而会降低缓存命中率,尤其对 CPU 缓存是致命的。

所以,在实际使用中,对于是否使用内联,要谨慎考虑,并做好平衡,以使它发挥最大的作用。
简单来说,对于短小而且工作较少的函数,使用内联是有效益的。

  1. func appendStr(word string) string {
  2. return "new " + word
  3. }

执行 GOOS=linux GOARCH=386 go tool compile -S main.go > main.S
我截取有区别的部分展出它编译后的样子:

  1. 0x0015 00021 (main.go:4) LEAL ""..autotmp_3+28(SP), AX
  2. 0x0019 00025 (main.go:4) PCDATA $2, $0
  3. 0x0019 00025 (main.go:4) MOVL AX, (SP)
  4. 0x001c 00028 (main.go:4) PCDATA $2, $1
  5. 0x001c 00028 (main.go:4) LEAL go.string."new "(SB), AX
  6. 0x0022 00034 (main.go:4) PCDATA $2, $0
  7. 0x0022 00034 (main.go:4) MOVL AX, 4(SP)
  8. 0x0026 00038 (main.go:4) MOVL $4, 8(SP)
  9. 0x002e 00046 (main.go:4) PCDATA $2, $1
  10. 0x002e 00046 (main.go:4) LEAL go.string."hello"(SB), AX
  11. 0x0034 00052 (main.go:4) PCDATA $2, $0
  12. 0x0034 00052 (main.go:4) MOVL AX, 12(SP)
  13. 0x0038 00056 (main.go:4) MOVL $5, 16(SP)
  14. 0x0040 00064 (main.go:4) CALL runtime.concatstring2(SB)

可以看到,它并没有调用 appendStr 函数,而是直接把这个函数体的功能内联了。
那么话说回来,如果你不想被内联,怎么办呢?此时就该使用 go//:noinline 了,像下面这样写:

  1. //go:noinline
  2. func appendStr(word string) string {
  3. return "new " + word
  4. }

编译后是:

  1. 0x0015 00021 (main.go:4) LEAL go.string."hello"(SB), AX
  2. 0x001b 00027 (main.go:4) PCDATA $2, $0
  3. 0x001b 00027 (main.go:4) MOVL AX, (SP)
  4. 0x001e 00030 (main.go:4) MOVL $5, 4(SP)
  5. 0x0026 00038 (main.go:4) CALL "".appendStr(SB)

//go:nosplit

nosplit 的作用是:跳过栈溢出检测。
一个 Goroutine 的起始栈大小是有限制的,且比较小的,才可以做到支持并发很多 Goroutine,并高效调度。
stack.go 源码中可以看到,_StackMin 是 2048 字节,也就是 2k,它不是一成不变的,当不够用时,它会动态地增长。
那么,必然有一个检测的机制,来保证可以及时地知道栈不够用了,然后再去增长。
回到话题,nosplit 就是将这个跳过这个机制。

//go:noescape

noescape 的作用是:禁止逃逸,而且它必须指示一个只有声明没有主体的函数。

优劣
最显而易见的好处是,GC 压力变小了。
因为它已经告诉编译器,下面的函数无论如何都不会逃逸,那么当函数返回时,其中的资源也会一并都被销毁。
不过,这么做代表会绕过编译器的逃逸检查,一旦进入运行时,就有可能导致严重的错误及后果。

//go:norace

norace 的作用是:跳过竞态检测

执行 go run -race main.go 可以利用 -race 来使编译器报告数据竞争问题,而通过该执行可以跳过

//go:notinheap

一般情况下,runtime 会尝试使用普通的方法来申请内存(堆上内存,gc 管理的),然而在某些情况 runtime 必须申请一些不被 gc 所管理的堆外内存(unmanaged memory)。这是很必要的,因为有可能该片内存就是内存管理器自身,或者说调用者没有一个 P(译者注:比如在调度器初始化之前,是不存在 P 的)。

在运行时中常用其来做较低层次的内部结构,避免调度器和内存分配中的写屏障,能够提高性能。

go:notinheap 适用于类型声明,表明了一个类型必须不被分配在 GC 堆上。特别的,指向该类型的指针总是应当在 runtime.inheap 判断中失败。这个类型可能被用于全局变量、栈上变量,或者堆外内存上的对象(比如通过 sysAlloc、persistentalloc、fixalloc 或者其它手动管理的 span 进行分配)。特别的:

  1. new(T)、make([]T)、append([]T, …) 和隐式的对于 T 的堆上分配是不允许的(尽管隐式的分配在 runtime 中是从来不被允许的)。
  2. 一个指向普通类型的指针(除了 unsafe.Pointer)不能被转换成一个指向 go:notinheap 类型的指针,就算它们有相同的底层类型(underlying type)。
  3. 任何一个包含了 go:notinheap 类型的类型自身也是 go:notinheap 的。如果结构体和数组包含 go:notinheap 的元素,那么它们自身也是 go:notinheap 类型。map 和 channel 不允许有 go:notinheap 类型。为了使得事情更加清晰,任何隐式的 go:notinheap 类型都应该显式地标明 go:notinheap。
  4. 指向 go:notinheap 类型的指针的写屏障可以被忽略。

最后一点是 go:notinheap 类型真正的好处。runtime 在底层结构中使用这个来避免调度器和内存分配器的内存屏障以避免非法检查或者单纯提高性能。这种方法是适度的安全(reasonably safe)的并且不会使得 runtime 的可读性降低。

// +build

当我们编写的go代码依赖特定平台或者cpu架构的时候,我们需要给出不同的实现
C语言有预处理器,可以通过宏或者#define包含特定平台指定的代码进行编译
但是Go没有预处理器,他是通过 go/build包 里定义的tags和命名约定来让Go的包可以管理不同平台的代码

注意:编译标签要放在源文件顶部

  1. // +build darwin freebsd netbsd openbsd

这个将会让这个源文件只能在支持kqueue的BSD系统里编译

一个源文件里可以有多个编译标签,多个编译标签之间是逻辑”与”的关系

  1. // +build linux darwin
  2. // +build 386

这个将限制此源文件只能在 linux/386或者darwin/386平台下编译