Go語言中的并發(fā)模式你了解了嗎
工作中查看項(xiàng)目代碼,發(fā)現(xiàn)會存在使用 GO 語言做并發(fā)的時(shí)候出現(xiàn)各種各樣的異常情況,有的輸出結(jié)果和自己期望和設(shè)計(jì)的不一致,有的是程序直接阻塞住,更有甚者直接是程序 crash 掉。
實(shí)際上,出現(xiàn)上述的情況,還是因?yàn)槲覀儗τ?GO 語言的并發(fā)模型和涉及的 GO 語言基礎(chǔ)不夠扎實(shí),誤解了語言的用法。
那么,對于 GO 語言的并發(fā)模式,我們一起來梳理一波。 GO 語言常見的并發(fā)模式有這些:
- 創(chuàng)建模式
- 退出模式
- 管道模式
- 超時(shí)模式和取消模式
在 GO 語言里面,咱們使用使用并發(fā),自然離不開使用 GO 語言的協(xié)程 goroutine,通道 channel 和 多路復(fù)用 select,接下來就來看看各種模式都是如何去搭配使用這三個(gè)關(guān)鍵原語的

創(chuàng)建模式
使用過通道和協(xié)程的朋友對于創(chuàng)建模式肯定不會模式,這是一個(gè)非常常用的方式,也是一個(gè)非常簡單的使用方式:
- 主協(xié)程中調(diào)用 help 函數(shù),返回一個(gè)通道 ch 變量
- 通道 ch 用于主協(xié)程和 子協(xié)程之間的通信,其中通道的數(shù)據(jù)類型完全可以自行定義
type XXX struct{...}
func help(fn func()) chan XXX {
ch := make(chan XXX)
// 開啟一個(gè)協(xié)程
go func(){
// 此處的協(xié)程可以控制和外部的 主協(xié)程 通過 ch 來進(jìn)行通信,達(dá)到一定邏輯便可以執(zhí)行自己的 fn 函數(shù)
fn()
ch <- XXX
}()
}
func main(){
ch := help(func(){
fmt.Println("這是GO 語言 并發(fā)模式之 創(chuàng)建模式")
})
<- ch
}退出模式
程序的退出我們應(yīng)該也不會陌生,對于一些常駐的服務(wù),如果是要退出程序,自然是不能直接就斷掉,此時(shí)會有一些連接和業(yè)務(wù)并沒有關(guān)閉,直接關(guān)閉程序會導(dǎo)致業(yè)務(wù)異常,例如在關(guān)閉過程中最后一個(gè) http 請求沒有正常響應(yīng)等等等
此時(shí),就需要做優(yōu)雅關(guān)閉了,對于協(xié)程 goroutine 退出有 3 種模式
- 分離模式
- join 模式
- notify-and-wait 模式
分離模式
此處的分離模式,分離這個(gè)術(shù)語實(shí)際上是線程中的術(shù)語,pthread detached
分離模式可以理解為,咱們創(chuàng)建的協(xié)程 goroutine,直接分離,創(chuàng)建子協(xié)程的父協(xié)程不用關(guān)心子協(xié)程是如何退出的,子協(xié)程的生命周期主要與它執(zhí)行的主函數(shù)有關(guān),咱們 return 之后,子協(xié)程也就結(jié)束了
對于這類分離模式的協(xié)程,咱們需要關(guān)注兩類,一種是一次性的任務(wù),咱們 go 出來后,執(zhí)行簡單任務(wù)完畢后直接退出,一種是常駐程序,需要優(yōu)雅退出,處理一些垃圾回收的事情
例如這樣:
- 主程序中設(shè)置一個(gè)通道變量 ch ,類型為 os.Signal
- 然后主程序就開始各種創(chuàng)建協(xié)程執(zhí)行自己的各種業(yè)務(wù)
- 直到程序收到了 syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT 任意一個(gè)信號的時(shí)候,則會開始進(jìn)行垃圾回收等清理工作,執(zhí)行完畢后,程序再進(jìn)行退出
func main(){
ch := make(chan os.Signal)
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
// ...
// go 程序執(zhí)行其他業(yè)務(wù)
// ...
for i := range ch {
switch i {
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
// 做一些清理工作
os.Exit(0)
}
}
}join 模式
看到這個(gè)關(guān)鍵字,是不是也似曾相識,和線程貌似很像,例如 線程中 父線程可以通過 pthread_join 來等待子線程結(jié)束,并且還可以獲取子線程的結(jié)束狀態(tài)
GO 語言中等待子協(xié)程退出并且獲取子協(xié)程的退出狀態(tài),咱們就可以使用通道 channel 的方式來進(jìn)行處理
例子1
等待一個(gè)子協(xié)程退出,并獲取退出狀態(tài)
- 主協(xié)程中調(diào)用 help 方法得到一個(gè) ch 通道變量,主協(xié)程阻塞著讀 ch
- help 中開辟一個(gè)子協(xié)程去執(zhí)行傳入的 fn 回調(diào)函數(shù),并傳參為 ok bool
- 實(shí)際 fn 函數(shù)判斷傳參 ok 是否是 true,若不是則返回具體的錯(cuò)誤信息,若是 true 則返回 nil
func help(f func(bool) error, ok bool) <-chan error {
ch := make(chan error)
go func() {
ch <- f(ok)
}()
return ch
}
func fn(ok bool) error {
if !ok {
return errors.New("not ok ... ")
}
return nil
}
func main() {
ch := help(fn, true)
fmt.Println("help 111")
err := <-ch
fmt.Println("help 111 done ", err)
ch = help(fn, false)
fmt.Println("help 222")
err = <-ch
fmt.Println("help 222 done ", err)
}看上如上程序,我們就可以知道,第一次調(diào)用 help(fn , true) ,主協(xié)程等待子協(xié)程退出的時(shí)候,會得到一個(gè)錯(cuò)誤信息,為 not ok ... , 第二次調(diào)用 help(fn , false) 的時(shí)候,返回的 err 是一個(gè) nil
通過上述這種方式,主協(xié)程不僅可以輕易的等待一個(gè)子協(xié)程退出,還可以獲取到子協(xié)程退出的狀態(tài)
那么,主協(xié)程如果是等待多個(gè)協(xié)程退出呢?需要如何處理?
例子2
主協(xié)程等待多個(gè)協(xié)程退出咱們就需要使用到 GO 中的 sync.WaitGroup
- 使用 help 函數(shù),傳入回調(diào)函數(shù),參數(shù)1 bool,參數(shù)2 int ,其中參數(shù) 2 表示開辟子協(xié)程的個(gè)數(shù),返回值為一個(gè)無緩沖的 channel 變量,數(shù)據(jù)類型是 struct{}
- 使用 var wg sync.WaitGroup ,開辟子協(xié)程的時(shí)候記錄一次 wg.Add(1),當(dāng)子協(xié)程退出時(shí) ,記錄退出 wg.Done()
- help 中再另起一個(gè)協(xié)程 wg.Wait() 等待所有子協(xié)程退出,并將 ch 變量寫入值
- 主協(xié)程阻塞讀取 ch 變量的值,待所有子協(xié)程都退出之后,help 中寫入到 ch 中的數(shù)據(jù),主協(xié)程就能馬上收到 ch 中的數(shù)據(jù),并退出程序
func help(f func(bool)error, ok bool, num int)chan struct{}{
ch := make(chan struct{})
var wg sync.WaitGroup
for i:=0; i<num; i++ {
wg.Add(1)
go func(){
f(ok)
fmt.Println(" f done ")
wg.Done()
}()
}
go func(){
// 等待所有子協(xié)程退出
wg.Wait()
ch <- struct{}{}
}()
return ch
}
func fn(ok bool) error{
time.Sleep(time.Second * 1)
if !ok{
return errors.New("not ok ... ")
}
return nil
}
func main(){
ch := help(fn , true)
fmt.Println("help 111")
<- ch
fmt.Println("help 111 done ",err)
}notify-and-wait 模式
可以看到上述模式,都是主協(xié)程等待一個(gè)子協(xié)程,或者多個(gè)子協(xié)程結(jié)束后,主協(xié)程再進(jìn)行退出,或者處理完垃圾回收后退出
那么如果主協(xié)程要主動通知子協(xié)程退出,我們應(yīng)該要如何處理呢?
同樣的問題,如果主協(xié)程自己退出了,而沒有通知其他子協(xié)程退出,這是會導(dǎo)致業(yè)務(wù)數(shù)據(jù)異?;蛘邅G失的,那么此刻我們就可以使用到 notify-and-wait 模式 來進(jìn)行處理
我們就直接來寫一個(gè)主協(xié)程通知并等待多個(gè)子協(xié)程退出的 demo:
- 主協(xié)程調(diào)用 help 函數(shù),得到一個(gè) quit chan struct{} 類型的通道變量,主協(xié)程阻塞讀取 quit 的值
- help 函數(shù)根據(jù)傳入的參數(shù) num 來創(chuàng)建 num 個(gè)子協(xié)程,并且使用 sync.WaitGroup 來控制
- 當(dāng)主協(xié)程在 quit 通道中寫入數(shù)據(jù)時(shí),主動通知所有子協(xié)程退出
- help 中的另外一個(gè)協(xié)程讀取到 quit 通道中的數(shù)據(jù),便 close 掉 j 通道,觸發(fā)所有的子協(xié)程讀取 j 通道值的時(shí)候,得到的 ok 為 false,進(jìn)而所有子協(xié)程退出
- wg.Wait() 等待所有子協(xié)程退出后,再在 quit 中寫入數(shù)據(jù)
- 主協(xié)程此時(shí)從 quit 中讀取到數(shù)據(jù),則知道所有子協(xié)程全部退出,自己的主協(xié)程即刻退出
func fn(){
// 模擬在處理業(yè)務(wù)
time.Sleep(time.Second * 1)
}
func help(num int, f func()) chan struct{}{
quit := make(chan struct{})
j := make(chan int)
var wg sync.WaitGroup
// 創(chuàng)建子協(xié)程處理業(yè)務(wù)
for i:=0;i<num;i++{
wg.Add(1)
go func(){
defer wg.Done()
_,ok:=<-j
if !ok{
fmt.Println("exit child goroutine .")
return
}
// 子協(xié)程 正常執(zhí)行業(yè)務(wù)
f()
}()
}
go func(){
<-quit
close(j)
// 等待子協(xié)程全部退出
wg.Wait()
quit <- struct{}{}
}()
return quit
}
func main(){
quit := help(10, fn)
// 模擬主程序處理在處理其他事項(xiàng)
// ...
time.Sleep(time.Second * 10)
quit <- struct{}{}
// 此處等待所有子程序退出
select{
case <- quit:
fmt.Println(" programs exit. ")
}
}上述程序執(zhí)行結(jié)果如下,可以看到 help 函數(shù)創(chuàng)建了 10 個(gè)子協(xié)程,主協(xié)程主動通知子協(xié)程全部退出,退出的時(shí)候也是 10 個(gè)子協(xié)程退出了,主協(xié)程才退出

上述程序,如果某一個(gè)子協(xié)程出現(xiàn)了問題,導(dǎo)致子協(xié)程不能完全退出,也就是說某些子協(xié)程在 f 函數(shù)中阻塞住了,那么這個(gè)時(shí)候主協(xié)程豈不是一直無法退出???
那么此時(shí),在主協(xié)程通知子協(xié)程退出的時(shí)候,我們加上一個(gè)超時(shí)時(shí)間,表達(dá)意思為,超過某個(gè)時(shí)間,如果子協(xié)程還沒有全部退出完畢,那么主協(xié)程仍然主動關(guān)閉程序,可以這樣寫:
設(shè)定一個(gè)定時(shí)器, 3 秒后會觸發(fā),即可以從 t.C 中讀取到數(shù)據(jù)
t := time.NewTimer(time.Second * 3)
defer t.Stop()
// 此處等待所有子程序退出
select{
case <-t.C:
fmt.Println("timeout programs exit. ")
case <- quit:
fmt.Println(" 111 programs exit. ")
}管道模式
說到管理,或許大家對 linux 里面的管道更加熟悉吧,例如使用 linux 命令找到文件中的 golang 這個(gè)字符串
cat xxx.txt |grep "golang"
那么對于 GO 語言并發(fā)模式中的管道模式也是類似的效果,我們就可以用這個(gè)管道模式來過濾數(shù)據(jù)
例如我們可以設(shè)計(jì)這樣一個(gè)程序,兄弟們可以動起手來寫一寫,評論區(qū)見哦:
- 整個(gè)程序總共使用 2 個(gè)通道
- help 函數(shù)中傳輸數(shù)據(jù)量 50 ,邏輯計(jì)算能夠被 5 整除的數(shù)據(jù)寫到第一個(gè)通道 ch1 中
- 另一個(gè)協(xié)程阻塞讀取 ch1 中的內(nèi)容,并將取出的數(shù)據(jù)乘以 3 ,將結(jié)果寫入到 ch2 中
- 主協(xié)程就阻塞讀取 ch2 的內(nèi)容,讀取到內(nèi)容后,挨個(gè)打印出來
管道模式有兩種模式,扇出模式 和 扇入模式,這個(gè)比較好理解
- 扇出模式:多種類型的數(shù)據(jù)從同一個(gè)通道 channel 中讀取數(shù)據(jù),直到通道關(guān)閉
- 扇入模式:輸入的時(shí)候有多個(gè)通道channel,程序?qū)⑺械耐ǖ纼?nèi)數(shù)據(jù)匯聚,統(tǒng)一輸入到另外一個(gè)通道channel A 里面,另外一個(gè)程序則從這個(gè)通道channel A 中讀取數(shù)據(jù),直到這個(gè)通道A關(guān)閉為止
超時(shí)模式和取消模式化
超時(shí)模式
上述例子中有專門說到如何去使用他,實(shí)際上我們還可以這樣用:
select{
case <- time.Afer(time.Second * 2):
fmt.Println("timeout programs exit. ")
case <- quit:
fmt.Println(" 111 programs exit. ")
}取消模式
則是使用了 GO 語言的 context 包中的提供了上下文機(jī)制,可以在協(xié)程 goroutine 之間傳遞 deadline,取消等信號
我們使用的時(shí)候例如可以這樣:
- 使用 context.WithCancel 創(chuàng)建一個(gè)可以被取消的上下文,啟動一個(gè)協(xié)程 在 3 秒后關(guān)閉上下文
- 使用 for 循環(huán)模擬處理業(yè)務(wù),默認(rèn)會走 select 的 default 分支
- 3 秒后 走到 select 的 ctx.Done(),則進(jìn)入到了取消模式,程序退出
ctx, cancelFunc := context.WithCancel(context.Background())
go func() {
time.Sleep(time.Second * 3)
cancelFunc()
}()
for {
select {
case <-ctx.Done():
fmt.Println("program exit .")
return
default:
fmt.Println("I'm still here.")
time.Sleep(time.Second)
}
}總的來說,今天分享了 GO 語言中常見的幾種并發(fā)模式:創(chuàng)建模式,退出模式,管道模式,超時(shí)模式和取消模式,更多的,還是要我們要思考其原理和應(yīng)用起來,學(xué)習(xí)他們才能更加的有效
以上就是Go語言中的并發(fā)模式你了解了嗎的詳細(xì)內(nèi)容,更多關(guān)于go并發(fā)模式的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang中map的三種聲明定義方式實(shí)現(xiàn)
本文主要介紹了Golang中map的三種聲明定義方式實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02
Go語言并發(fā)定時(shí)任務(wù)之從Sleep到Context的8種寫法全解析
這篇文章主要為大家詳細(xì)介紹了Go語言并發(fā)定時(shí)任務(wù)之從Sleep到Context的8種寫法的相關(guān)知識,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解下2025-08-08
Go語言防范SQL注入CSRF及XSS攻擊實(shí)例探究
在本文中,我們將會介紹幾種最常見的攻擊類型,并且介紹如何使用Golang來防范這些攻擊,本文會涉及XSS攻擊、CSRF攻擊、SQL注入等,如果你想學(xué)習(xí)Golang和網(wǎng)絡(luò)安全的相關(guān)知識,那么這篇文章會是一個(gè)很好的開始2024-01-01
Go操作Kafka的實(shí)現(xiàn)示例(kafka-go)
本文介紹了使用kafka-go庫在Go語言中與Kafka進(jìn)行交互,涵蓋了kafka-go的安裝、API使用、消息發(fā)送與消費(fèi)方法,以及如何通過DockerCompose快速搭建Kafka環(huán)境,文章還比較了其他兩個(gè)常用的Kafka客戶端庫,感興趣的可以了解一下2024-10-10
go語言通過odbc操作Access數(shù)據(jù)庫的方法
這篇文章主要介紹了go語言通過odbc操作Access數(shù)據(jù)庫的方法,實(shí)例分析了Go語言通過odbc連接、查詢與關(guān)閉access數(shù)據(jù)庫的技巧,需要的朋友可以參考下2015-03-03

