問題引入

學習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點比較特別:

  1. noCopy 型別是空 struct
  2. 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型別,實現了LockUnlock方法,使得 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{} 的使用