美文网首页
go-linkname

go-linkname

作者: hsiaojun | 来源:发表于2022-02-20 18:09 被阅读0次

    假设有一个内部包,它提供一个方法如下:

    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代码的底层逻辑。

    相关文章

      网友评论

          本文标题:go-linkname

          本文链接:https://www.haomeiwen.com/subject/gusvlrtx.html