给出下面几个例子,问面试者其中哪些例子有问题。事实上每一个都有 bug,看看面试者能看出几个。
例子 1
func finishReq(timeout time.Duration) ob {
ch := make(chan ob)
go func() {
result := fn()
ch <- result
}()
select {
case result := <-ch:
return result
case <-time.After(timeout):
return nil
}
}
问题:如果超时返回 nil
了,ch
永远无法被写入,造成本函数中起的协程泄露。
解决方法:将 ch
改成 buffered channel;ch := make(chan ob, 1)
。
例子 2
var group sync.WaitGroup
group.Add(len(pm.plugins))
for _, p := range pm.plugins {
go func(p *plugin) {
// do something with p...
defer group.Done()
}
group.Wait()
}
这个很简单,group.Wait()
不应该放在 for
循环中,而是应该挪到最后一行。
例子 3
hctx, hcancel := context.WithCancel(ctx)
if timeout > 0 {
hctx, hcancel = context.WithTimeout(ctx, timeout)
}
这个比较隐晦。如果 timeout > 0
,第一行产生的 hctx
没有被 cancel,会造成协程泄露。
改成:
var (
hctx context.Context
hcancel context.CancelFunc
)
if timeout > 0 {
hctx, hcancel = context.WithTimeout(ctx, timeout)
} else {
hctx, hcancel = context.WithCancel(ctx)
}
例子 4
for i := 1; i <= 5; i++ {
go func() {
fmt.Println(i)
}()
}
会输出什么?
答案是:一般是输出 5 个 5。有可能其中有一个 4,要看调度的情况。但很少会是预计的 1 2 3 4 5。
改成:
for i := 1; i <= 5; i++ {
go func(i int) {
fmt.Println(i)
}(i)
}
例子 5
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
go func() {
wg.Add(1)
// do something...
wg.Done()
}()
}
wg.Wait()
这个例子在于 wg.Add(1)
不能放在新协程中去做。假如 for
循环中的新起的协程一个都还没被调度到,代码就运行到了 wg.Wait()
这一行,那么实际上 wg.Wait()
并没有起到等所有协程结束的作用。改成:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
// do something...
wg.Done()
}()
}
wg.Wait()
例子 6
func (c *Client) Close() {
select {
case <- c.closed:
default:
close(c.closed)
}
}
乍一看这个实现没啥问题:select
语句中有一个 case 是从一个关闭的 channel 中接收消息时,该 case 总是 ready 的。
但是假如有并发调用 Close()
时,如果这两个协程都走入了 default
分支,会造成 channel 被关闭两次,引起 panic。
正确的写法是:
func (c *Client) Close() {
c.closeOnce.Do(func() {
close(c.closed)
})
}
例子 7
ticker := time.NewTicker(interval)
for {
doHeavyWork()
select {
case <- stopCh:
return
case <- ticker.C:
}
}
这段代码的用意是,不停地执行 doHeavyWork()
直至收到 stopCh
消息退出。
它的问题很隐晦。问题出在 select
的机制上:如果有多个 case 是 ready 状态,会随机选择一个 case 去执行。这就导致了当 stopCh
和 ticker.C
都可读时,随机选到了 ticker.C
分支,导致 doHeavyWork()
又被不必要地执行了一次。
解决方法是,在 doHeavyWork()
前再读一次 stopCh
:
ticker := time.NewTicker(interval)
for {
select {
case <- stopCh:
return
default:
}
doHeavyWork()
select {
case <- stopCh:
return
case <- ticker.C:
}
}