struct 是我們寫 Go 必然會用到的關鍵字, 不過當 struct 遇上一些比較特殊型別的時候, 你注意過你的程式是否正常嗎 ?
一段程式碼
type URL struct {
Ip string
Port string
mux sync.RWMutex
params url.Values
}
func (c *URL) Clone() URL {
newUrl := URL{}
newUrl.Ip = c.Ip
newUrl.params = url.Values{}
return newUrl
}
這段程式碼你能看出來問題所在嗎 ?
A: 程式正常
B: 編譯失敗
C: panic
D: 有可能發生 data race
E: 有可能發生死鎖
如果你看出來問題在哪裡的話, 那我再悄悄告訴你, 這段程式碼是 github 某 3k star Go 框架的底層核心程式碼, 那你是不是就覺得這個話題開始有意思了 ?
先說結論
上面那段程式碼的問題是 sync.RWMutex
引起的. 如果你看過有關 sync 相關型別的介紹或者相關原始碼時, 在 sync
包裡面的所有型別都有句這樣的註釋: must not be copied after first use
, 可能很多人卻並不知道這句話有什麼作用, 頂多看到相關介紹時還記得 sync
相關型別的變數不能複製, 可能真正使用 Mutex, WaitGroup, Cond時, 早把這個註釋忘的一乾二淨.
究其原因, 我覺得有下面兩點原因:
- 不明白什麼叫 sync 型別變數複製
- sync 型別的變數複製了會出現怎樣的結果
下面的例子都以 Mutex 來舉例
- 最容易看出來的情形
func main() {
var amux sync.Mutex
b := amux
b.Lock()
b.Unlock()
}
其實這種情況一般情況下, 沒人這麼用. 問題不大, 略過
- 巢狀在 struct 裡面, struct 變數間的互相賦值
type URL struct {
Ip string
Port string
mux sync.RWMutex
params url.Values
}
func main() {
var url1 URL
url2 := url1
}
當 struct 巢狀 不可複製 型別時, 就需要開始小心了. 當 struct 巢狀層次過深或者 struct 變數隨著值傳遞對外擴散時, 這個時候就會變得不可控了, 就需要特別小心了.
- struct 型別變數的值傳遞作為返回值
type URL struct {
Ip string
mux sync.RWMutex
}
func (c *URL) Clone() URL {
newUrl := URL{}
newUrl.Ip = c.Ip
return newUrl
}
- struct 型別變數的值傳遞作為 receiver
type URL struct {
Ip string
mux sync.RWMutex
}
func (c URL) String() string {
c.paramsLock.Lock()
defer c.paramsLock.Unlock()
buf.WriteString(c.params.Encode())
return buf.String()
}
複製後出現的結果
例子1:
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
var age int
type Person struct {
mux sync.Mutex
}
func (p Person) AddAge() {
defer wg.Done()
p.mux.Lock()
age++
defer p.mux.Unlock()
}
func main() {
p1 := Person{
mux: sync.Mutex{},
}
wg.Add(100)
for i := 0; i < 100; i++ {
go p1.AddAge()
}
wg.Wait()
fmt.Println(age)
}
結果: 結果有可能是 100, 也有可能是99....
例子2:
package main
import (
"fmt"
"sync"
)
type Person struct {
mux sync.Mutex
}
func Reduce(p Person) {
fmt.Println("step...", )
p.mux.Lock()
fmt.Println(p)
defer p.mux.Unlock()
fmt.Println("over...")
}
func main() {
var p Person
p.mux.Lock()
go Reduce(p)
p.mux.Unlock()
fmt.Println(111)
for {
}
}
結果: Reduce 協程會死鎖.
看到這裡我們就能發現, 當 struct 巢狀了 Mutex, 如果以值傳遞的方式使用時, 有可能造成程式死鎖, 有可能需要互斥的變數並不能達到互斥.
所以不管是單獨使用 不能複製 型別的變數, 還是巢狀在 struct 裡面都不能值傳遞的方式使用.
不能複製原因
以 Mutex 為例,
type Mutex struct {
state int32
sema uint32
}
我們使用 Mutex 是為了不同 goroutine 之間共享某個變數, 所以需要讓這個變數做到能夠互斥, 不然該變數就會被互相被覆蓋. Mutex 底層是由 state
sema
控制的, 當 Mutex 變數被複制時, Mutex 的 state, sema 當時的狀態也被複制走了, 但是由於不同 goroutine 之間的 Mutex 已經不是同一個變數了, 這樣就會造成要麼某個 goroutine 死鎖或者不同 goroutine 共享的變數達不到互斥
struct 如何與 不可複製 的型別一塊使用 ?
由上面可以看到不只是 sync 相關型別變數自身不能被複制,而且 sturct 巢狀 不可複製 型別變數時, 同樣也不能被複制. 但是如果我將巢狀的不可複製變數改成指標型別變數呢, 是不是就解決了不能複製的問題 ?
type URL struct {
Ip string
mux *sync.RWMutex
}
這樣確實解決了上述的不能複製問題. 但也引出了另外一個問題. 眾所周知 Go 沒有建構函式, 這就導致我們使用 URL 的時候都需要先去初始化 RWMutex, 不然就會造成同樣很嚴重的空指標問題, 這個問題同樣很棘手,也許哪個位置就忘了初始化這個 RWMutex.
根據 google groups 的討論 How to copy a struct which contains a mutex?, 以及我檢視了Kubernets 的相關原始碼(這裡只是一個例子, 裡面還有很多), 發現大家的觀點基本上都是一致的, 都不會去選用 struct 去巢狀指標型別的變數, 由此不建議 struct 去巢狀 不可複製的 的指標型別變數. 最重要的原因: 沒有一個工具能去準確的檢測空指標.
所以一般情況下, 當 struct 巢狀了 不可複製 型別的變數時, 都需要傳遞的是 struct 型別變數的指標.
如何防止複製了不該複製的變數呢?
由於 Go 並不提供過載
的功能, 所以並不能做到去過載 struct 的相關的被複制的方法. 但是 Go 的槽點就來了, Go 本身還不提供不能被複制的相關的編譯強約束. 這樣就有可能導致出現不能被複制的型別被複制過後矇混過關. 那我們需要怎麼做呢 ?
Go 提供了另外一個工具 go vet
來做補充, 用這個工具是能檢測出來不可複製的型別是否被複制過.
func main() {
var amux sync.Mutex
b := amux
b.Lock()
b.Unlock()
}
$ go vet main.go
# command-line-arguments
./main.go:7:7: assignment copies lock value to b: sync.Mutex
我們怎麼把 go vet 與 日常開發結合起來呢?
- 目前的 Goland, Vscode 都會整合 go vet 的相關功能, 如果你強迫症比較嚴重的話, 你就能發現有相關提示.
- 把 go vet 與 CI 流程結合起來, 其實更推薦使用
golangci-lint
這個 lint 工具來做 CI
Go 還提供一段 noCopy 的程式碼, 當你的 struct 有不能被複制的需求的時候, 可以加入這段程式碼
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*noCopy) Lock() {}
func (*noCopy) Unlock() {}
這段程式碼依然是給 go vet 來使用的.
說到這裡, 禁止複製不能被複制的變數, 這個明明能在 編譯期 就杜絕的事情, 為啥非要搞出來工具來做這個事情呢? 有點想不通.
不可複製的型別有哪些?
Go 提供的不可複製的型別基本上就是 sync 包內的所有型別: atomic.Value
, sync.Mutex
, sync.Cond
, sync.RWMutex
, sync.Map
, sync.Pool
, sync.WaitGroup
.
這些內建的不可被複制的型別當被複制時配合 go vet是能夠發現的. 但是下面這種場景你是否遇見過?
package main
import "fmt"
type Books struct {
someImportantData []int
}
func DoSomething(otherBook Books) Books {
newBook := otherBook
// do something
for k := range newBook.someImportantData {
newBook.someImportantData[k]++ // just like this
}
return otherBook
}
func main() {
oldBook := Books{
someImportantData: make([]int, 0, 100),
}
oldBook.someImportantData = append(oldBook.someImportantData, 1, 2, 3)
fmt.Println("before DoSomething, old book:", oldBook.someImportantData)
DoSomething(oldBook)
fmt.Println("after DoSomething, old book:", oldBook.someImportantData)
// 使用oldBook.someImportantData 繼續做某些事情
}
結果:
before DoSomething, old book: [1 2 3]
after DoSomething, old book: [2 3 4]
這個場景其實我們可能不經意間就會遇到. oldBook 是我們要操作的資料, 但是通過 DoSomething` 後, oldBook.someImportantData 的值可能就被改掉了, 這可能並不是我們所期待的. 由於 DoSomething 是通過複製傳遞的, 可能我們並不能很敏感關注到這個點, 導致程式繼續往下走邏輯可能就錯了. 我們是不是可以設定 Books 為不可複製呢 ? 這樣可以讓 go vet 幫助我們發現這些問題
最後的最後
你是否這樣初始化過 WaitGroup ?
wg := sync.WaitGroup{}
這個算不算是被複制了呢, 歡迎留言討論.