TranslateProject/sources/tech/20170124 Compile-time assertions in Go.md

142 lines
5.0 KiB
Markdown
Raw Normal View History

2017-02-11 15:25:42 +08:00
Compile-time assertions in Go
============================================================
This post is about a little-known way to make compile-time assertions in Go. You probably shouldnt use it, but it is interesting to know about.
As a warm-up, heres a fairly well-known form of compile-time assertions in Go: Interface satisfaction checks.
In this code ([playground][1]), the `var _ =` line ensures that type `W` is a `stringWriter`, as checked for by [`io.WriteString`][2].
```
package main
import "io"
type W struct{}
func (w W) Write(b []byte) (int, error) { 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")
}
```
If you comment out `W`s `WriteString` method, the code will not compile:
```
main.go:14: cannot use W literal (type W) as type stringWriter in assignment:
W does not implement stringWriter (missing WriteString method)
```
This is useful. For most types that satisfy both `io.Writer` and `stringWriter`, if you eliminate the `WriteString` method, everything will continue to work as it did before, but with worse performance.
Rather than trying to write a fragile test for a performance regression using [`testing.T.AllocsPerRun`][3], you can simply protect your code with a compile-time assertion.
Heres [a real world example of this technique from package io][4].
* * *
OK, onward to obscurity!
Interface satisfaction checks are great. But what if you wanted to check a plain old boolean expression, like `1+1==2`?
Consider this code ([playground][5]):
```
package main
import "crypto/md5"
type Hash [16]byte
func init() {
if len(Hash{}) < md5.Size {
panic("Hash is too small")
}
}
func main() {
// ...
}
```
`Hash` is perhaps some kind of abstracted hash result. The `init` function ensures that it will work with [crypto/md5][6]. If you change `Hash` to be (say) `[8]byte`, itll panic when the process starts. However, this is a run-time check. What if we wanted it to fail earlier?
Heres how. (Theres no playground link, because this doesnt work on the playground.)
```
package main
import "C"
import "crypto/md5"
type Hash [16]byte
func hashIsTooSmall()
func init() {
if len(Hash{}) < md5.Size {
hashIsTooSmall()
}
}
func main() {
// ...
}
```
Now if you change `Hash` to be `[8]byte`, it will fail during compilation. (Actually, it fails during linking. Close enough for our purposes.)
```
$ go build .
# demo
main.hashIsTooSmall: call to external function
main.init.1: relocation target main.hashIsTooSmall not defined
main.init.1: undefined: "main.hashIsTooSmall"
```
Whats going on here?
`hashIsTooSmall` is [declared without a function body][7]. The compiler assumes that someone else will provide an implementation, perhaps an assembly routine.
When the compiler can prove that `len(Hash{}) < md5.Size`, it eliminates the code inside the if statement. As a result, no one uses the function `hashIsTooSmall`, so the linker eliminates it. No harm done. As soon as the assertion fails, the code inside the if statement is preserved.`hashIsTooSmall` cant be eliminated. The linker then notices that no one else has provided an implementation for the function and fails with an error, which was the goal.
One last oddity: Why `import "C"`? The go tool knows that in normal Go code, all functions must have bodies, and instructs the compiler to enforce that. By switching to cgo, we remove that check. (If you run `go build -x` on the code above, without the `import "C"` line, you will see that the compiler is invoked with the `-complete` flag.) An alternative to adding `import "C"` is to [add an empty file called `foo.s` to the package][8].
I know of only one use of this technique, in the [compiler test suite][9]. There are other [imaginable places to apply it][10], but no one has bothered.
And thats probably how it should be. :)
--------------------------------------------------------------------------------
via: http://commaok.xyz/post/compile-time-assertions
作者:[Josh Bleecher Snyder][a]
译者:[译者ID](https://github.com/译者ID)
校对:[校对者ID](https://github.com/校对者ID)
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
[a]:https://twitter.com/commaok
[1]:https://play.golang.org/p/MJ6zF1oNsX
[2]:https://golang.org/pkg/io/#WriteString
[3]:https://golang.org/pkg/testing/#AllocsPerRun
[4]:https://github.com/golang/go/blob/go1.8rc2/src/io/multi.go#L72
[5]:https://play.golang.org/p/mjIMWsWu4V
[6]:https://golang.org/pkg/crypto/md5/
[7]:https://golang.org/ref/spec#Function_declarations
[8]:https://github.com/golang/go/blob/go1.8rc2/src/os/signal/sig.s
[9]:https://github.com/golang/go/blob/go1.8rc2/test/fixedbugs/issue9608.dir/issue9608.go
[10]:https://github.com/golang/go/blob/go1.8rc2/src/runtime/hashmap.go#L261