問題引入
學習golang(v1.16)的 WaitGroup 程式碼時,看到了一處奇怪的用法,見下方型別定義:
type WaitGroup struct {
noCopy noCopy
...
}
這裡,有個奇怪的“noCopy
”型別,顧名思義,這個應該是某種“不可複製”的意思。下邊是noCopy型別的定義:
// noCopy may be embedded into structs which must not be copied
// after the first use.
//
// See https://golang.org/issues/8005#issuecomment-190753527
// for details.
// 對應github連結:https://github.com/golang/go/issues/8005#issuecomment-190753527
type noCopy struct {}
// Lock is a no-op used by -copylocks checker from `go vet`
func (*noCopy) Lock{}
func (*noCopy) Unlock{}
// 以上 Lock 和 Unlock 方法屬於 Locker 介面型別的方法集,見 sync/mutex.go
這裡有2點比較特別:
- noCopy 型別是空 struct
- noCopy 型別實現了兩個方法: Lock 和 Unlock,而且都是空方法(no-op)。註釋中有說,這倆方法是給 go vet 的 copylocks 檢測器用的
也就是說,這個 noCopy 型別和它的方法集,沒有任何實質的功能屬性。那麼它是用來做什麼的呢?
動手試試
從型別定義,以及實現的lock方法的註釋可以看出,noCopy 是為了實現對不可複製型別的限制。這個限制如何起作用呢?參考註釋中給出的 issuecomment 連結,在Russ Cox 的評論中,看的這麼一句:
A package can define:
type noCopy struct{}
func (*noCopy) Lock() {}
and then put a
noCopy noCopy
into any struct that must be flagged by vet.
原來這個noCopy
的用處,是為了讓被嵌入的container型別,在用go vet
工具進行copylock check時,能被檢測到。
我寫了一段程式碼試了下:
// file: main.go
package main
import "fmt"
type noCopy struct{}
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
type cool struct {
Val int32
noCopy
}
func main() {
c1 := cool{Val:10,}
c2 := c1 // <- 賦值拷貝
c2.Val = 20
fmt.Println(c1, c2) // <- 傳參拷貝
}
然後,我先用vet工具檢查了一下:
leo@leo-MBP % go vet main.go
# command-line-arguments
./main.go:14:8: assignment copies lock value to c2: command-line-arguments.cool
./main.go:16:14: call of fmt.Println copies lock value: command-line-arguments.cool
./main.go:16:18: call of fmt.Println copies lock value: command-line-arguments.cool
上邊的輸出可以看到,在程式碼標記出來的兩處位置,vet列印了“copy lock value”的提示。
查詢資料
試著查了一下這個提示的相關資訊,發現這一篇博文:Detect locks passed by value in Go
同時,用go tool vet help copylocks
命令可以檢視 vet 對 copylocs 分析器的介紹:
copylocks: check for locks erroneously passed by value
Inadvertently copying a value containing a lock, such as sync.Mutex or
sync.WaitGroup, may cause both copies to malfunction. Generally such
values should be referred to through a pointer.
原來,vet 工具的 copylocks 檢測器有這麼一個功能:檢測帶鎖型別(如 sync.Mutex
) 的錯誤複製使用,這種不當的複製,會引發死鎖。
其實不僅僅是sync.Mutex
型別會這樣,所有需要用到Lock和Unlock方法的型別,即 lock type,都有這種 “錯誤複製引發死鎖” 的隱患。
所以,我們在上邊測試的程式碼中定義的noCopy
型別,實現了Lock
和Unlock
方法,使得 noCopy 成了一個 lock type,目的就是為了能利用 vet 的 copylocks 分析器對 copy value 的檢測能力。
岔個題
雖然上邊的測試程式碼,在用 go vet 檢測時給出了提示資訊,但是這並不是警告,相應程式碼沒有語法錯誤,仍然是可執行的,run 一下試試:leo@leo-MBP % go run main.go
{10 {}} {20 {}}
嵌入了 noCopy 型別的 cool 型別,在被強行復制之後,依然可以執行。noCopy 這種設計的意義,在於防範不當的 copylocks 發生,且這種防範不是強制的,依靠開發者自行檢測。
空 struct
好,明白了 noCopy
的存在的意義,接下來探究一下 noCopy
為什麼要設計成空 struct 型別。
先上結論:使用空 struct 是出於效能考慮。
package main
import (
"fmt"
"unsafe"
)
type cool struct{}
func main() {
c := cool{}
fmt.Println(unsafe.Sizeof(c)) // -> print 0
}
如上所示,空 struct 型別的值不佔用記憶體空間,所以在效能上更有優勢。
總結
綜合來看,noCopy 空 struct 型別,結合了 vet 工具對 copylocks 檢測的支援,以及空 struct 對效能的優化,用在 “標記不可複製型別” 的場景下,是比較巧妙的設計。
參考
Detect locks passed by value in Go
The empty struct
Go 空結構體 struct{} 的使用