Go 编译时断言

GO 刘宇帅 4年前 阅读量: 2434

原文地址:Go 编译时断言

这篇文章介绍一个鲜为人知的在Go中编译时断言的方法。你可能不会用到它,但是了解下也是非常有趣的。
作为热身,这是一个相当知名的编译时断言的方法:接口满足性检查。 在下面的代码中(playgroud),var _ stringWriter = W{}这句表明wstringWriter,当作io.WriteString来做检查。

package main

import "io"
type w struct{}

func (w W) Write(b []byte)(int, err) {
    return len(b), nil
}

func (w W) WriteString(s string) (int, error) {
    return len(s), nil
}

type stringWriter interface {
    WriteString(string)(int, error)
}

var _ stringWriter = W{}

func main() {
    var w W
    io.WriteString(w, "very long string")
}

如果你注释掉W''sWriteString的方法,那么这里将无法编译通过:

main.go:14: cannot use W literal (type W) as type stringWriter in assignment:
    W does not implement stringWriter (missing WriteString method)

这是非常有用的,对于其他更多满足io.WriterstringWriter的类型如果删除WriteString方法,那么其他一切会像以前一样运行,但是性能会下降。
宁愿用testing.T.AllocsPerRun写一个简易的测试来测试性能,还不如在你的代码里添加一个编译时断言。
这里是现实中的一个例子

好,现在正式开始!
接口满足性检查是很好的。但是如果你想检查一个普通的布尔表达式呢,例如1 + 1 = 2
思考下下面的代码(playground):

package main

import "crypto/md5"

type Hash [16]byte

func init() {
    if len(Hash{}) < md5.Size {
        panic("Hash is too small")
    }
}

func main() {
    // ...
}

Hash用来表示抽象的hash结果。init函数确保他可以被crypto/md5使用。如果你把Hash修改为[8]byte,运行的时候就会 panic。但是这是运行时检查,我们怎么能够提前发现他呢?
下面是方法。

package main

import "C"

import "crypto/md5"

type Hash [16]byte

func hashIsTooSmall()

func init() {
    if len(Hash{}) < md5.Size {
        hashIsTooSmall()
    }
}

func main() {
    // ...
}

现在如果你把Hash改成[8]byte,那么编译的时候就会报错。(事实上,程序会在链接的时候失败。接近我们的目的)

$ go build .
# demo
main.hashIsTooSmall: call to external function
main.init.1: relocation target main.hashIsTooSmall not defined
main.init.1: undefined: "main.hashIsTooSmall"

发生了什么呢?
hashIsTooSmall被声明了但是却没有函数体。编译器假定其他编译程序等会提供该函数的实现。
如果编译器可以证明len(Hash{}) < md5.Size,那么编译器会消除掉if声明里面的语句,因此,没有人会去调用hashIsToolSmall,所以链接器会消除掉。如果现在无法证明,那么if中的语句将会保留下来,hashIsToolSmall不会被消除。链接器然后会发现并没有人实现相应的函数然后就会失败报错。
最后一个奇怪的事:为什么引入了C包?Go语言工具知道在正常的代码里任何函数都应该有函数体,并指示编译器去执行。通过切换到 cgo可以避免这种检查。(如果你删除掉上面的import "C"并使用go build -x编译上面的代码,你会看到编译器被调用的时候包含-complete参数)还有一个替代import "C"的可选方案是在包中添加一个foo.s文件。

我知道在go的一个编译器测试里有使用这个方案,还有一些其他可以想象到的在使用它的地方,但是没有人烦恼(but no one has bothered.)

提示

功能待开通!


暂无评论~