WIP:《100 Go Mistakes and How to Avoid Them》之并发实践篇
礼物说9.1 传递了不合适的context
我们在代码中经常会向下传递context,但是有时会产生bug。比如我们在httpserver中想要异步传输一条topic信息。
func handler(w http.ResponseWriter, r *http.Request) {
, err := doSomeTask(r.Context(), r)
responseif err != nil {
.Error(w, err.Error(), http.StatusInternalServerError)
httpreturn
}
go func() {
:= publish(r.Context(), response)
err // Do something with err
}()
(response)
writeResponse}
当客户端关闭连接或者返回response给客户端时,ctx会canceled。这就有可能导致异步消息提前终止。所以这样情况,我们不能无脑传递ctx,我们需要:
:= publish(context.Background(), response) err
9.2 创建了一个协程却并不知道什么时候关闭
在Go中,创建协程非常简单,但如果我们不设定什么时候停止这个协程,就会造成资源泄漏。协程的初始内存是2KB,可以缩扩容。如果当一个协程持有了数据库资源或者网络socket,那么它的泄漏同样会造成资源泄漏。我们可以看以下的一个例子:
func main() {
()
newWatcher// Run the application
}
type watcher struct { /* Some resources */ }
func newWatcher() {
:= watcher{}
w go w.watch()
}
我们需要给watcher
增加关闭资源的方法,然后在程序退出前执行:
:= newWatcher()
w defer w.close()
func (w watcher) close() {
// Close the resources
}
总而言之,我们开启一个协程就应该知道什么情况下去关闭。
9.3 协程使用迭代变量不仔细
错误操作协程和迭代变量非常常见:
:= []int{1, 2, 3}
s for _, i := range s {
go func() {
.Print(i)
fmt}()
}
比如以上的例子,你可能认为输出的结果是123
,当实际是223
或者有时是333
。闭包创建协程,变量读的是地址,可能内容已经改变了。我们可以改为:
for _, i := range s {
:= i
val go func() {
.Print(val)
fmt}() }
或者:
for _, i := range s {
go func(val int) {
.Print(val)
fmt}(i)
}
9.4 使用select和channels时认为确定行为
我们可以看下例子:
for {
select {
case v := <-messageCh:
.Println(v)
fmtcase <-disconnectCh:
.Println("disconnection, return")
fmtreturn
}
}
for i := 0; i < 10; i++ {
<- i
messageCh }
<- struct{}{} disconnectCh
我们得到的结果是:
0
1
2
3
4
, return disconnection
为什么不是10条?我们可以看下go对于select语句的文档:
If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection.
当case语句同时满足时,select
会随机选择分支。如果我们想要满足上述的实现,我们可以:
messageCh
改为不带缓冲区的channel。这样就会变成同步堵塞操作。- 两者使用同一个channel。同一个channel是顺序的。
9.5 不使用通知类型的channel
channel
通常作为协程中传递信号的一种通讯方式。那我们仅仅是为了通知而非传递数据时,我们应该使用chan struct{}
,并不占用存储空间。
var s struct{}
fmt.Println(unsafe.Sizeof(s))
0
这种类型可以明确的让接收者清楚不需要理会message的内容。 ### 9.6 不使用nil channel 值为nil的管道其实很有用,我们可以了解下什么是nil channel
,什么时候我们可以使用。 ### 9.7 不理解channel长度我们使用channel的时候需要考虑2个问题:
- 是否需要带缓冲区
- 缓冲区大小为多少
带缓冲区代表了异步操作,数据会放入缓冲区。而不带缓冲区或者缓冲区已满,则会变成同步操作,如果没有接收/发送方则会堵塞。当使用带缓冲区的时候,我们应该如何设置缓冲区大小呢?
- 当多个协程往同一个共享的channel发送数据的时候
- 当channel用来做限流的时候
- 其他情况都应该使用默认值:1.
如何设置缓冲区大小本身就是CPU与Memory之间的平衡点。
9.8 忘记字符串格式化时的边界情况
9.9 append的时候产生 data races
我们看下在什么情况下append
会产生数据竞争。
:= make([]int, 1)
s
go func() {
:= append(s, 1)
s1 .Println(s1)
fmt}()
go func() {
:= append(s, 1)
s2 .Println(s2)
fmt}()
你认为上述例子有么?答案是没有。因为在上述情况下,slice的length
和capacity
都是1,append操作会申请新的内存块。如果改成s := make([]int, 0, 1)
就会产生数据竞争了。这种情况要避免就需要使用副本copy
去操作:
:= make([]int, 0, 1)
s
go func() {
:= make([]int, len(s), cap(s))
sCopy copy(sCopy, s)
:= append(sCopy, 1)
s1 .Println(s1)
fmt}()
go func() {
:= make([]int, len(s), cap(s))
sCopy copy(sCopy, s)
:= append(sCopy, 1)
s2 .Println(s2)
fmt}()
9.10 在slices和maps中错误的使用锁
9.11 错用sync.WaitGroup
sync.WaitGroup
是用来等待X个操作完成的机制。配合Add(int)
和Done()
去增减内部计数器,Wait()
会阻塞直到计数器归零。下面有一个例子,大家可以看看有什么问题:
:= sync.WaitGroup{}
wg var v uint64
for i := 0; i < 3; i++ {
go func() {
.Add(1)
wg.AddUint64(&v, 1)
atomic.Done()
wg}()
}
.Wait()
wg.Println(v) fmt
结果是个不确定的值,原因是我们调用wg.Add(1)
不应该在协程内部,这样会导致wg.Wait()
直接非堵塞,然后读取了v.正确处理方式应该是:
for i := 0; i < 3; i++ {
.Add(1)
wggo func() {
// ...
}()
}
9.12 不知道sync.Cond
9.13 不使用errgroup
Go提供了errgroup
包,可以执行多个协程,从多个协程中聚合错误。使用方式:
$ go get golang.org/x/sync/errgroup
func handler(ctx context.Context, circles []Circle) ([]Result, error) {
:= make([]Result, len(circles))
results , ctx := errgroup.WithContext(ctx)
g
for i, circle := range circles {
:= i
i := circle
circle .Go(func() error {
g, err := foo(ctx, circle)
resultif err != nil {
return err
}
[i] = result
resultsreturn nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
g.Go
接收一个闭包函数func() error
. g.Wait
当遇到error会抛出,否则就会等待所有协程完成。另一个优势就是当一个协程抛错,errgroup.WithContext
生成的ctx就会接收到canceled,其他协程也会结束,无需继续等待。
9.14 copy了一个sync类型
sync包提供了一些基础的同步工具比如:mutexes
和wait groups
。他们都有一个铁律想要遵守:不能copy. 比较常见的错误就是方法用了值接收,比如:
type Counter struct {
.Mutex
mu syncmap[string]int
counters }
func NewCounter() Counter {
return Counter{counters: map[string]int{}}
}
func (c Counter) Increment(name string) {
.mu.Lock()
cdefer c.mu.Unlock()
.counters[name]++
c}
解决方案就是改成指针接收:
func (c *Counter) Increment(name string) {
// Same code
}
或者把锁改成指针:
type Counter struct {
*sync.Mutex
mu map[string]int
counters }