看似一道簡(jiǎn)單的 Go 題,考點(diǎn)不少:一道字節(jié)跳動(dòng)面試題解析
我是一只可愛(ài)的土撥鼠,專注于分享 Go 職場(chǎng)、招聘和求職,解 Gopher 之憂!歡迎關(guān)注我。
網(wǎng)上看到有人分享去字節(jié)跳動(dòng)的面試 Go 的經(jīng)驗(yàn)[1],從面試題來(lái)看,應(yīng)該是比較初級(jí)的職位,這方面土撥鼠之前給大家分析過(guò)他家初級(jí)職位的要求。
這份面試經(jīng)驗(yàn)總結(jié)中(其實(shí)談不上總結(jié),只是面試題的記錄,并沒(méi)有總結(jié)分析答案),有一道 Go 相關(guān)的題,也是一個(gè)老生常談的問(wèn)題:以下代碼有什么問(wèn)題,怎么解決?
total,?sum?:=?0,?0
for?i?:=?1;?i?<=?10;?i++?{
????sum?+=?i
????go?func()?{
????????total?+=?i
????}()
}
fmt.Printf("total:%d?sum?%d",?total,?sum)
01 考點(diǎn)一
我相信很多人應(yīng)該一眼看出了其中的一個(gè)問(wèn)題,那就是 i 使用的問(wèn)題。常見(jiàn)的題目是這樣的:以下代碼,輸出什么?
for?i?:=?1;?i?<=?10;?i++?{
??go?func()?{
????fmt.Println(i)
??}()
}
time.Sleep(1e9)
相信很多人知道,輸出 一堆 11,而不是期望的輸出 1 到 10。
怎么改進(jìn)?你應(yīng)該也知曉。
for?i?:=?1;?i?<=?10;?i++?{
??go?func(i?int)?{
????fmt.Println(i)
??}(i)
}
time.Sleep(1e9)
(當(dāng)然這里的輸出順序是亂的,大家應(yīng)該清楚)
02 考點(diǎn)二
該題的第二個(gè)考點(diǎn):data race。因?yàn)榇嬖诙?goroutine 同時(shí)寫(xiě) total 變量的問(wèn)題,所以有數(shù)據(jù)競(jìng)爭(zhēng)。可以加上 -race 參數(shù)驗(yàn)證:
$?go?run?-race?main.go
==================
WARNING:?DATA?RACE
Read?at?0x00c0001b4020?by?goroutine?8:
??main.main.func1()
??????/Users/xuxinhua/main.go:12?+0x57
Previous?write?at?0x00c0001b4020?by?main?goroutine:
??main.main()
??????/Users/xuxinhua/main.go:9?+0x10b
Goroutine?8?(running)?created?at:
??main.main()
??????/Users/xuxinhua/main.go:11?+0xe7
==================
這可以通過(guò)加鎖的方式解決:
var?mutex?sync.Mutex
total,?sum?:=?0,?0
for?i?:=?1;?i?<=?10;?i++?{
??sum?+=?i
??go?func(i?int)?{
????mutex.Lock()
????total?+=?i
????mutex.Unlock()
??}(i)
}
此外,也可以通過(guò) atomic 包解決:(注意 total 的類型,因?yàn)?atomic.AddInt64 需要)
var?total?int64
sum?:=?0
for?i?:=?1;?i?<=?10;?i++?{
??sum?+=?i
??go?func(i?int)?{
????atomic.AddInt64(&total,?int64(i))
??}(i)
}
通過(guò) -race 你驗(yàn)證,發(fā)現(xiàn) data race 沒(méi)了。
細(xì)心的你不知道發(fā)現(xiàn)沒(méi)有,以上代碼我故意把最后的 fmt 輸出那一行去掉了,因?yàn)樗昧?total 變量,避免它導(dǎo)致 data race。這引出考點(diǎn)三。
03 考點(diǎn)三
我上面都沒(méi)有給完整的代碼,因?yàn)榻?jīng)過(guò)上面兩步,最終的結(jié)果還是不對(duì)的。從上面說(shuō)的 fmt 輸出代碼去掉就說(shuō)明還有問(wèn)題。
初學(xué) Go 應(yīng)該遇到類似這樣的問(wèn)題,下面代碼一般沒(méi)有輸出。
package?main
import?"fmt"
func?main()?{
?go?func()?{
??fmt.Println("Hello?World!")
?}()
}
原因是 main 函數(shù)先退出了,開(kāi)啟的 goroutine 根本沒(méi)有機(jī)會(huì)執(zhí)行。所以,常見(jiàn)的解決辦法是在最后加一個(gè) Sleep:
package?main
import?"fmt"
func?main()?{
?go?func()?{
??fmt.Println("Hello?World!")
?}()
??
??time.Sleep(1e9)
}
Sleep 會(huì)讓 main goroutine 休眠,調(diào)度器調(diào)度其他 goroutine 運(yùn)行。
回到開(kāi)頭的題目其實(shí)也存在這個(gè)問(wèn)題,通過(guò)在 fmt 語(yǔ)句之前加上 Sleep,基本能得到正確的結(jié)果:
var?total?int64
sum?:=?0
for?i?:=?1;?i?<=?10;?i++?{
????sum?+=?i
????go?func(i?int)?{
????????atomic.AddInt64(&total,?int64(i))
????}(i)
}
time.Sleep(1e9)
fmt.Printf("total:%d?sum?%d",?total,?sum)
但如果加上 -race 發(fā)現(xiàn)還是有問(wèn)題:
$?go?run?-race?main.go
==================
WARNING:?DATA?RACE
Read?at?0x00c00001c0b0?by?main?goroutine:
??main.main()
??????/Users/xuxinhua/main.go:20?+0xe4
Previous?write?at?0x00c00001c0b0?by?goroutine?7:
??sync/atomic.AddInt64()
??????/Users/xuxinhua/.go/current/src/runtime/race_amd64.s:276?+0xb
??main.main.func1()
??????/Users/xuxinhua/main.go:15?+0x44
Goroutine?7?(finished)?created?at:
??main.main()
??????/Users/xuxinhua/main.go:14?+0xa4
==================
total:55?sum?55Found?1?data?race(s)
所以,這種方式是不靠譜的,這時(shí)正確的方式是使用 sync.WaitGroup。
package?main
import?(
????"sync/atomic"
????"sync"
????"fmt"
)
func?main()?{
????var?wg?sync.WaitGroup
????var?total?int64
????sum?:=?0
????for?i?:=?1;?i?<=?10;?i++?{
????????wg.Add(1)
????????sum?+=?i
????????go?func(i?int)?{
????????????defer?wg.Done()
????????????atomic.AddInt64(&total,?int64(i))
????????}(i)
????}
????wg.Wait()
????fmt.Printf("total:%d?sum?%d",?total,?sum)
}
04 總結(jié)
通過(guò)上面的分析,發(fā)現(xiàn)看起來(lái)是一個(gè)簡(jiǎn)單的題目,其實(shí)考點(diǎn)好幾個(gè)。這個(gè)題目還是挺好的,字節(jié)跳動(dòng)面試官出的這道題還是有點(diǎn)水平。你覺(jué)得呢?
參考資料
面試 Go 的經(jīng)驗(yàn): https://zhuanlan.zhihu.com/p/132813717
推薦閱讀
歡迎搜索或掃碼關(guān)注我!
