本文已参与「新人创作礼」活动,一起开启掘金创作之路。
先看一下下面这段代码,我们的 Echo 方法通过锁去控制并发访问,期望 type 和 type___ 各自连续输出 10 次,但是运行结果真的符合我们预期么?
package main
import (
"fmt"
"sync"
"time"
)
type Data struct {
sync.Mutex
}
func (d Data) Echo(t string) {
d.Lock()
for i := 1; i <= 10; i++ {
fmt.Printf("%s: %d\n", t, i)
time.Sleep(time.Second * 1)
}
d.Unlock()
}
func main() {
d := Data{}
var wg sync.WaitGroup
wg.Add(2)
go func() {
d.Echo("type")
wg.Done()
}()
go func() {
d.Echo("type___")
wg.Done()
}()
wg.Wait()
}
我们可以看到结果并不符合预期,最终会交叉输出:
$ go run main.go
type___: 1
type: 1
type___: 2
type: 2
type___: 3
type: 3
type: 4
type___: 4
type: 5
type___: 5
type___: 6
type: 6
type___: 7
type: 7
type: 8
type___: 8
type___: 9
type: 9
type: 10
type___: 10
我们明明加了互斥锁,为什么没有限制并发访问,看起来锁 “失效” 了,造成这个现象的原因是什么呢?我们先看下 Mutex 的结构:
type Mutex struct {
state int32 //记录锁状态
sema uint32 //控制锁状态信号量
}
可以看到 state 和 sema 都是值类型,大家想到原因了吗?再给大家一个提示:
GoLang 中定义结构体成员方法时,底层实际上是把接收者作为第一个参数传递进去了,本质还是一个普通方法。
有思路了吗?没错,既然把接收者作为参数传递给方法了,那么当我们像上面代码一样定义值接收者时,结构体会 copy 一个新变量传递给函数,实际上此时用到的 Mutex 不是同一个变量,可以打印指针看一下:
func (d Data) Echo(t string) {
fmt.Printf("%p\n", &d.Mutex)
d.Lock()
for i := 1; i <= 10; i++ {
fmt.Printf("%s: %d\n", t, i)
time.Sleep(time.Second * 1)
}
d.Unlock()
}
$ go run main.go
0xc0000ba008
0xc000100000
type___: 1
type: 1
所以解决的思路就是将方法接收者改成指针类型,这样在 copy 结构体的时候,实际上是复制的锁指针,就可以保证用的是同一个锁结构,像下面这样:
func (d *Data) Echo(t string) {
fmt.Printf("%p\n", &d.Mutex)
d.Lock()
for i := 1; i <= 10; i++ {
fmt.Printf("%s: %d\n", t, i)
time.Sleep(time.Second * 1)
}
d.Unlock()
}
看下结果,地址是同一个,并且打印顺序符合预期:
$ go run main.go
0xc00001c0b0
0xc00001c0b0
type___: 1
type___: 2
type___: 3
type___: 4
type___: 5
type___: 6
type___: 7
type___: 8
type___: 9
type___: 10
type: 1
type: 2
type: 3
type: 4
type: 5
type: 6
type: 7
type: 8
type: 9
type: 10
既然编译的时候不会报错,那么我们该如何及时发现代码问题呢,可以用两个工具去检测,go vet 和 golangci-lint:
$ go vet main.go
# command-line-arguments
./main.go:13:9: Echo passes lock by value: command-line-arguments.Data
$ golangci-lint main.go
Error: unknown command "main.go" for "golangci-lint"
Run 'golangci-lint --help' for usage.
failed executing command with error unknown command "main.go" for "golangci-lint"