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{}
这个算不算是被复制了呢, 欢送留言探讨.
欢送关注我的公众号: HHFCodeRv, 关注我的最新动静