struct 是咱們寫 Go 必然會用到的關鍵字, 不過當 struct 趕上一些比較特殊類型的時候, 你注意過你的程序是否正常嗎 ?git
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 }
這段代碼你能看出來問題所在嗎 ?github
A: 程序正常 B: 編譯失敗 C: panic D: 有可能發生 data race E: 有可能發生死鎖
若是你看出來問題在哪裏的話, 那我再悄悄告訴你, 這段代碼是 github 某 3k star Go 框架的底層核心代碼, 那你是否是就以爲這個話題開始有意思了 ?golang
上面那段代碼的問題是 sync.RWMutex
引發的. 若是你看過有關 sync 相關類型的介紹或者相關源碼時, 在 sync
包裏面的全部類型都有句這樣的註釋: must not be copied after first use
, 可能不少人卻並不知道這句話有什麼做用, 頂多看到相關介紹時還記得 sync
相關類型的變量不能複製, 可能真正使用 Mutex, WaitGroup, Cond時, 早把這個註釋忘的一乾二淨. app
究其緣由, 我以爲有下面兩點緣由:框架
下面的例子都以 Mutex 來舉例函數
func main() { var amux sync.Mutex b := amux b.Lock() b.Unlock() }
其實這種狀況通常狀況下, 沒人這麼用. 問題不大, 略過工具
type URL struct { Ip string Port string mux sync.RWMutex params url.Values } func main() { var url1 URL url2 := url1 }
當 struct 嵌套 不可複製 類型時, 就須要開始當心了. 當 struct 嵌套層次過深或者 struct 變量隨着值傳遞對外擴散時, 這個時候就會變得不可控了, 就須要特別當心了.ui
type URL struct { Ip string mux sync.RWMutex } func (c *URL) Clone() URL { newUrl := URL{} newUrl.Ip = c.Ip return newUrl }
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:this
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....google
例子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 共享的變量達不到互斥
由上面能夠看到不僅是 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 與 平常開發結合起來呢?
golangci-lint
這個 lint 工具來作 CIGo 還提供一段 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{}
這個算不算是被複制了呢, 歡迎留言討論.
歡迎關注個人公衆號: HHFCodeRv, 關注個人最新動態