golang sync.noCopy 類型 —— 初探 copylocks 與 empty struct

問題引入

學習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 //golang.org/issues/8005#issuecomment-190753527
    // for details.
    // 對應github鏈接://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{} 的使用