代碼包的開發(fā)者可以在一個結構體類型定義里放置一個非導出的零尺寸的字段,這樣編譯器將會禁止代碼包的使用者使用含有一些字段但卻不含有字段名字的組合字面量來創(chuàng)建此結構體類型的值。
例如:
// foo.go
package foo
type Config struct {
_ [0]int
Name string
Size int
}
// main.go
package main
import "foo"
func main() {
//_ = foo.Config{[0]int{}, "bar", 123} // 編譯不通過
_ = foo.Config{Name: "bar", Size: 123} // 編譯沒問題
}
請盡量不要把零尺寸的非導出字段用做結構體的最后一個字段,因為這樣做會有可能會增大結構體類型的尺寸而導致一些內存浪費。
有時候,我們想要避免一個自定義的結構體類型被用做一個映射的鍵值類型,那么我們可以放置一個非導出的零尺寸的不可比較類型的字段在結構體類型中以使此結構體類型不可比較。 例如:
package main
type T struct {
dummy [0]func()
AnotherField int
}
var x map[T]int // 編譯錯誤:非法的鍵值類型
func main() {
var a, b T
_ = a == b // 編譯錯誤:非法的比較
}
目前(Go 1.19),在一些多值賦值中有一些表達式估值順序是未指定的。 因此,如果一個多值賦值語句中涉及的表達式會相互干涉,或者不太容易確定是否會相互干涉,我們應該將此多值賦值語句分拆成多個單值賦值語句。
事實上,在一些寫得很糟糕的代碼中,單值賦值中的表達式求值順序也有可能是有歧義的。 例如,下面的程序可能會打印[7 0 9]
、[0 8 9]
或者[7 8 9]
,依賴于具體編譯器實現。
package main
import "fmt"
var a = &[]int{1, 2, 3}
var i int
func f() int {
i = 1
a = &[]int{7, 8, 9}
return 0
}
func main() {
// 表達式"a"、"i"和"f()"的估值順序未定義。
(*a)[i] = f()
fmt.Println(*a)
}
我們可以通過遍歷一個元素尺寸為零的數組或者一個空數組指針來模擬這樣的循環(huán)。 例如:
package main
import "fmt"
func main() {
const N = 5
for i := range [N]struct{}{} {
fmt.Println(i)
}
for i := range [N][0]int{} {
fmt.Println(i)
}
for i := range (*[N]int)(nil) {
fmt.Println(i)
}
}
關于細節(jié),請閱讀如何刪除切片元素和因為未重置丟失的切片元素中的指針而造成的臨時性內存泄露。
bytes.Buffer
類型、strings.Builder
類型以及在sync
標準庫包里的類型的值不推薦被復制。 (它們確實不應該被復制,盡管在某些特定情形下復制它們或許是沒有問題的。)
strings.Builder
的實現會在運行時刻探測到非法的strings.Builder
值復制。 一旦這樣的復制被發(fā)現,就會產生恐慌。例如:
package main
import "strings"
func main() {
var b strings.Builder
b.WriteString("hello ")
var b2 = b
b2.WriteString("world!") // 一個恐慌將在這里產生
}
復制標準庫包sync
中類型的值會被Go官方工具鏈提供的go vet
命令檢測到并被警告。
// demo.go
package demo
import "sync"
func f(m sync.Mutex) { // warning: f passes lock by value: sync.Mutex
m.Lock()
defer m.Unlock()
// do something ...
}
$ go vet demo.go
./demo.go:5: f passes lock by value: sync.Mutex
復制bytes.Buffer
的值不會在運行時被檢查到,也不會被go vet
命令所檢測到。 千萬要小心不要隨意這樣做。
關于細節(jié),請閱讀memclr
優(yōu)化。
可以使用下面的例子中的方法。 (假設需要被檢查的方法的描述是M(int) string
。)
package main
import "fmt"
type A int
type B int
func (b B) M(x int) string {
return fmt.Sprint(b, ": ", x)
}
func check(v interface{}) bool {
_, has := v.(interface{M(int) string})
return has
}
func main() {
var a A = 123
var b B = 789
fmt.Println(check(a)) // false
fmt.Println(check(b)) // true
}
關于細節(jié)請閱讀這篇wiki文章和這篇wiki文章。
假設一個包提供了一個func NewX(...Option) *X
函數,并且這個函數的實現將輸入選項與一些內部默認選項合并,那么下面的實現是不推薦的。
func NewX(opts ...Option) *X {
options := append(opts, defaultOpts...)
// 使用合并后選項來創(chuàng)建一個X值并返回其指針。
// ...
}
上述實現不被推薦的原因是append
函數調用可能會修改輸入實參opts
的底層潛在Option
元素序列。 對大多數場景,這可能是沒問題的。但是對某些特殊場景,這有可能會導致后續(xù)代碼執(zhí)行產生不期望的結果。
為了避免輸入實參的底層Option
元素序列被修改,我們應該使用下面的實現方法:
func NewX(opts ...Option) *X {
// 改用三下標子切片格式。
opts = append(opts[:len(opts):len(opts)], defaultOpts...)
// 使用合并后選項來創(chuàng)建一個X值并返回其指針。
// ...
}
另一方面,對于NewX
函數的調用者來說,不應該依賴于此函數的具體實現,所以最好使用三下標子切片形式options[:len(options):cap(options)]
來傳遞實參。
另外一個需要使用三下標子切片格式的場景在這篇wiki文章中被提及。
三下標子切片格式的一個缺點是它們有些冗長。 事實上,我曾經提了一個建議來讓三下標格式看上起簡潔得多。 但是此建議被否決了。
關于細節(jié),請閱讀這篇文章。
我們可以將一個自定義類型的一個值賦給指定接口類型的一個變量來確保此自定義類型實現了指定接口類型。 更重要的是,這樣可以表明此自定義類型實現了指定接口類型。 使用自解釋的代碼編寫文檔比使用注釋來編寫文檔要自然得多。
package myreader
import "io"
type MyReader uint16
func NewMyReader() *MyReader {
var mr MyReader
return &mr
}
func (mr *MyReader) Read(data []byte) (int, error) {
switch len(data) {
default:
*mr = MyReader(data[0]) << 8 | MyReader(data[1])
return 2, nil
case 2:
*mr = MyReader(data[0]) << 8 | MyReader(data[1])
case 1:
*mr = MyReader(data[0])
case 0:
}
return len(data), io.EOF
}
// 下面三行中的任一行都可以保證類型*MyReader實現
// 了接口io.Reader。
var _ io.Reader = NewMyReader()
var _ io.Reader = (*MyReader)(nil)
func _() {_ = io.Reader(nil).(*MyReader)}
除了上一個技巧中提到過的編譯時刻斷言技巧,下面將要介紹更多編譯時刻斷言技巧。
下面是一些方法用來在編譯時刻保證常量N
不小于另一個常量M
:
// 下面任一行均可保證N >= M
func _(x []int) {_ = x[N-M]}
func _(){_ = []int{N-M: 0}}
func _([N-M]int){}
var _ [N-M]int
const _ uint = N-M
type _ [N-M]int
// 如果M和N都是正整數常量,則我們也可以使用下一行所示的方法。
var _ uint = N/M - 1
另一個方法是借鑒@lukechampine的一個點子。 此點子利用了容器組合字面量中不能出現重復的常量鍵值這一規(guī)則。
var _ = map[bool]struct{}{false: struct{}{}, N>=M: struct{}{}}
此方法看上去有些冗長,但是它更加通用。它可以用來斷言任何條件。 其實,它也可以不必很冗長,但需要多消耗一點(完全可以忽略的)內存,如下面所示:
var _ = map[bool]int{false: 0, N>=M: 1}
類似地,下面是斷言兩個整數常量相等的方法:
var _ [N-M]int; var _ [M-N]int
type _ [N-M]int; type _ [M-N]int
const _, _ uint = N-M, M-N
func _([N-M]int, [M-N]int) {}
var _ = map[bool]int{false: 0, M==N: 1}
var _ = [1]int{M-N: 0} // 唯一被允許的元素索引下標為0
var _ = [1]int{}[M-N] // 唯一被允許的元素索引下標為0
var _ [N-M]int = [M-N]int{}
最后一行的靈感同樣來自于Luke Champine的一條tweet。
下面是一些用來斷言一個常量字符串是不是一個空串的方法。
type _ [len(aStringConstant)-1]int
var _ = map[bool]int{false: 0, aStringConstant != "": 1}
var _ = aStringConstant[:1]
var _ = aStringConstant[0]
const _ = 1/len(aStringConstant)
最后一行借鑒自Jan Mercl的一個點子。
有時候,為了避免包級變量消耗太多的內存,我們可以把斷言代碼放在一個名為空標識符的函數體中。 例如:
func _() {
var _ = map[bool]int{false: 0, N>=M: 1}
var _ [N-M]int
}
const MaxUint = ^uint(0)
const MaxInt = int(^uint(0) >> 1)
這個技巧和Go無關。
const Is64bitArch = ^uint(0) >> 63 == 1
const Is32bitArch = ^uint(0) >> 63 == 0
const WordBits = 32 << (^uint(0) >> 63) // 64或32
關于細節(jié),請閱讀關于Go值的內存布局一文。
當一個非接口值被賦值給一個接口值時,此非接口值的一個副本將被包裹到此接口值中。 副本復制的開銷和非接口值的尺寸成正比。尺寸越大,復制開銷越大。 所以請盡量避免將大尺寸的值包裹到接口值中。
在下面的例子中,后兩個打印調用的成本要比前兩個低得多。
package main
import "fmt"
func main() {
var a [1000]int
// 這兩行的開銷相對較大,因為數組a中的元素都將被復制。
fmt.Println(a)
fmt.Printf("Type of a: %T\n", a)
// 這兩行的開銷較小,數組a中的元素沒有被復制。
fmt.Printf("%v\n", a[:])
fmt.Println("Type of a:", fmt.Sprintf("%T", &a)[1:])
}
關于不同種類的類型的尺寸,請閱讀值復制成本一文。
請閱讀此文來獲知什么是邊界檢查消除(BCE)以及目前的標準編譯器對BCE的支持程度。
下面是一個利用了BCE進行性能優(yōu)化的例子:
package main
import (
"strings"
"testing"
)
func NumSameBytes_1(x, y string) int {
if len(x) > len(y) {
x, y = y, x
}
for i := 0; i < len(x); i++ {
if x[i] != y[i] {
return i
}
}
return len(x)
}
func NumSameBytes_2(x, y string) int {
if len(x) > len(y) {
x, y = y, x
}
if len(x) <= len(y) { // 雖然代碼多了,但是效率提高了
for i := 0; i < len(x); i++ {
if x[i] != y[i] { // 邊界檢查被消除了
return i
}
}
}
return len(x)
}
var x = strings.Repeat("hello", 100) + " world!"
var y = strings.Repeat("hello", 99) + " world!"
func BenchmarkNumSameBytes_1(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = NumSameBytes_1(x, y)
}
}
func BenchmarkNumSameBytes_2(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = NumSameBytes_2(x, y)
}
}
從下面所示的基準測試結果來看,函數NumSameBytes_2
比函數NumSameBytes_1
效率更高。
BenchmarkNumSameBytes_1-4 10000000 669 ns/op
BenchmarkNumSameBytes_2-4 20000000 450 ns/op
請注意:標準編譯器(gc)的每個新的主版本都會有很多小的改進。 上例中所示的優(yōu)化從gc 1.11開始才有效。 未來的gc版本可能會變得更加智能,以使函數NumSameBytes_2
中使用技巧變得不再必要。 事實上,從gc 1.11開始,如果x
和y
是兩個切片,即使上例中使用小技巧沒有被使用,y[i]
中的邊界檢查也已經被消除了。
更多建議: