Go語言協(xié)程通道使用的問題小結(jié)
關(guān)于Go語言中通道(channel)使用的一些重要問題:
1. 為什么用完通道要關(guān)閉?
- 資源管理:關(guān)閉通道可以釋放與之相關(guān)的資源,包括內(nèi)存和goroutine。
- 通知接收方:關(guān)閉通道是一種向接收方發(fā)出"不再有數(shù)據(jù)"信號(hào)的方式。
- 避免死鎖:如果接收方在等待已經(jīng)不再使用的通道,可能會(huì)導(dǎo)致程序死鎖。
2. 不關(guān)閉通道的風(fēng)險(xiǎn):
- 資源泄露:未關(guān)閉的通道可能導(dǎo)致相關(guān)的goroutine無法退出,造成資源泄露。
- 潛在的死鎖:如果有g(shù)oroutine在等待接收數(shù)據(jù),而發(fā)送方已經(jīng)不再發(fā)送,可能導(dǎo)致程序死鎖。
- 難以調(diào)試:未關(guān)閉的通道可能導(dǎo)致程序行為不可預(yù)測,增加調(diào)試難度。
3.怎么優(yōu)雅地關(guān)閉?
優(yōu)雅地關(guān)閉通道的方法:
a. 使用defer關(guān)鍵字:
ch := make(chan int) defer close(ch) // 使用通道...
b. 使用sync.Once確保只關(guān)閉一次:
var once sync.Once close := func() { once.Do(func() { close(ch) }) } // 在適當(dāng)?shù)臅r(shí)候調(diào)用close()
c. 使用專門的停止通道:
done := make(chan struct{}) go func() { // 執(zhí)行操作... close(done) }() // 等待操作完成 <-done
d. 使用context包來管理取消操作:
ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 使用ctx來控制goroutine的生命周期
記住,只有發(fā)送方應(yīng)該關(guān)閉通道,接收方不應(yīng)該關(guān)閉通道。同時(shí),確保不要多次關(guān)閉同一個(gè)通道,這會(huì)導(dǎo)致panic。
4.有緩存和無緩存的通道有什么區(qū)別?
有緩存和無緩存的通道(channel)在 Go 語言中有一些關(guān)鍵的區(qū)別,兩者的詳細(xì)比較如下:
定義方式:
- 無緩存通道:
ch := make(chan int)
- 有緩存通道:
ch := make(chan int, capacity)
(capacity > 0)
- 無緩存通道:
容量:
- 無緩存通道:容量為 0
- 有緩存通道:容量大于 0,由創(chuàng)建時(shí)指定
阻塞行為:
- 無緩存通道:
- 發(fā)送操作會(huì)阻塞,直到有接收者準(zhǔn)備好接收數(shù)據(jù)
- 接收操作會(huì)阻塞,直到有發(fā)送者發(fā)送數(shù)據(jù)
- 有緩存通道:
- 只有當(dāng)緩沖區(qū)滿時(shí),發(fā)送操作才會(huì)阻塞
- 只有當(dāng)緩沖區(qū)空時(shí),接收操作才會(huì)阻塞
- 無緩存通道:
同步特性:
- 無緩存通道:提供了強(qiáng)同步保證,發(fā)送和接收操作同時(shí)發(fā)生
- 有緩存通道:提供了一定程度的異步性,發(fā)送和接收可以在不同時(shí)間發(fā)生
使用場景:
- 無緩存通道:適用于需要即時(shí)響應(yīng)或嚴(yán)格同步的場景
- 有緩存通道:適用于需要一定程度解耦或緩沖的場景
性能考慮:
- 無緩存通道:可能導(dǎo)致更多的上下文切換
- 有緩存通道:可以減少goroutine的阻塞,潛在地提高性能
關(guān)閉行為:
- 兩種通道關(guān)閉后的行為相同:
- 可以繼續(xù)從關(guān)閉的通道接收數(shù)據(jù),直到通道為空
- 向關(guān)閉的通道發(fā)送數(shù)據(jù)會(huì)導(dǎo)致 panic
- 兩種通道關(guān)閉后的行為相同:
容量檢查:
- 無緩存通道:
len(ch)
始終為 0,cap(ch)
始終為 0 - 有緩存通道:
len(ch)
返回當(dāng)前在緩沖區(qū)中的元素?cái)?shù)量,cap(ch)
返回緩沖區(qū)的容量
- 無緩存通道:
內(nèi)存使用:
- 無緩存通道:內(nèi)存占用較小
- 有緩存通道:根據(jù)容量的大小,可能占用更多內(nèi)存
死鎖風(fēng)險(xiǎn):
- 無緩存通道:如果沒有相應(yīng)的接收操作,單獨(dú)的發(fā)送操作更容易導(dǎo)致死鎖
- 有緩存通道:提供了一定的緩沖,減少了即時(shí)死鎖的風(fēng)險(xiǎn),但仍需注意緩沖區(qū)滿時(shí)的情況
用于范圍循環(huán):
- 兩種通道都可以用于 range 循環(huán),但有緩存通道可能更適合批處理場景
選擇使用哪種類型的通道取決于具體的應(yīng)用場景、同步需求、性能考慮和代碼的復(fù)雜性。無緩存通道提供了更強(qiáng)的同步保證,而有緩存通道則提供了更大的靈活性和潛在的性能優(yōu)勢。
5.代碼詳解:
var once sync.Once close := func() { once.Do(func() { close(ch) }) }
var once sync.Once
:- 這里聲明了一個(gè)
sync.Once
類型的變量once
。 sync.Once
是 Go 標(biāo)準(zhǔn)庫中的一個(gè)同步原語,用于確保某個(gè)函數(shù)只被執(zhí)行一次。
- 這里聲明了一個(gè)
close := func() { ... }
:- 這定義了一個(gè)匿名函數(shù),并將其賦值給變量
close
。 - 這個(gè)函數(shù)可以在之后被多次調(diào)用。
- 這定義了一個(gè)匿名函數(shù),并將其賦值給變量
once.Do(func() { close(ch) })
:once.Do()
方法接受一個(gè)函數(shù)作為參數(shù)。- 這個(gè)函數(shù)只會(huì)在第一次調(diào)用
once.Do()
時(shí)執(zhí)行,后續(xù)的調(diào)用將不會(huì)執(zhí)行這個(gè)函數(shù)。 - 在這里,傳遞給
once.Do()
的函數(shù)是func() { close(ch) }
,這個(gè)函數(shù)會(huì)關(guān)閉通道ch
。
整體功能:
- 這段代碼創(chuàng)建了一個(gè)安全的關(guān)閉通道的機(jī)制。
- 即使
close
函數(shù)被多次調(diào)用,ch
通道也只會(huì)被關(guān)閉一次。 - 這避免了因多次關(guān)閉同一個(gè)通道而導(dǎo)致的 panic。
使用場景:
- 在并發(fā)環(huán)境中,當(dāng)多個(gè) goroutine 可能會(huì)嘗試關(guān)閉同一個(gè)通道時(shí),這種方法特別有用。
- 它確保了通道只會(huì)被關(guān)閉一次,無論
close
函數(shù)被調(diào)用多少次。
優(yōu)點(diǎn):
- 線程安全:
sync.Once
保證了在并發(fā)環(huán)境中的安全性。 - 避免 panic:防止了多次關(guān)閉通道導(dǎo)致的 panic。
- 簡潔:提供了一種簡潔的方式來處理"只執(zhí)行一次"的邏輯。
- 線程安全:
注意事項(xiàng):
- 雖然這個(gè)方法可以防止多次關(guān)閉通道,但仍然需要確保不會(huì)向已關(guān)閉的通道發(fā)送數(shù)據(jù)。
- 這個(gè)模式主要用于在不確定通道是否已關(guān)閉的情況下安全地關(guān)閉通道。
這種模式在處理通道關(guān)閉時(shí)非常有用,特別是在復(fù)雜的并發(fā)場景中,可以有效地防止由于重復(fù)關(guān)閉通道而導(dǎo)致的錯(cuò)誤。
done := make(chan struct{}) go func() { // 執(zhí)行操作... close(done) }() // 等待操作完成 <-done
done := make(chan struct{})
- 創(chuàng)建一個(gè)無緩沖的通道
done
。 - 使用
struct{}
類型是因?yàn)槲覀冎魂P(guān)心通道的關(guān)閉狀態(tài),不需要傳遞實(shí)際的數(shù)據(jù)。 struct{}
是一個(gè)空結(jié)構(gòu)體,不占用任何內(nèi)存,是信號(hào)通道的理想選擇。
- 創(chuàng)建一個(gè)無緩沖的通道
go func() { ... }()
- 啟動(dòng)一個(gè)新的 goroutine 來執(zhí)行匿名函數(shù)。
- 這允許主 goroutine 繼續(xù)執(zhí)行,而不會(huì)被阻塞。
// 執(zhí)行操作...
- 這里代表在新啟動(dòng)的 goroutine 中執(zhí)行的實(shí)際操作。
- 可能是一些耗時(shí)的任務(wù),如 I/O 操作、計(jì)算等。
close(done)
- 當(dāng)操作完成時(shí),關(guān)閉
done
通道。 - 關(guān)閉通道會(huì)向所有正在等待該通道的 goroutine 發(fā)送一個(gè)信號(hào)。
- 當(dāng)操作完成時(shí),關(guān)閉
<-done
- 主 goroutine 在這里等待
done
通道的關(guān)閉。 - 這行代碼會(huì)阻塞,直到
done
通道被關(guān)閉。 - 一旦
done
通道關(guān)閉,這個(gè)接收操作就會(huì)立即完成,允許程序繼續(xù)執(zhí)行。
- 主 goroutine 在這里等待
這種模式的主要優(yōu)點(diǎn)和使用場景:
同步:提供了一種簡單的方式來同步主 goroutine 和后臺(tái) goroutine,確保主 goroutine 等待某個(gè) goroutine 完成實(shí)際工作后再繼續(xù)執(zhí)行。
非阻塞操作:允許后臺(tái)任務(wù)非阻塞地執(zhí)行,同時(shí)主 goroutine 可以等待它完成。
超時(shí)處理:可以很容易地添加超時(shí)邏輯,例如:
select { case <-done: // 操作完成 case <-time.After(5 * time.Second): // 超時(shí)處理 }
取消操作:可以擴(kuò)展這個(gè)模式來支持取消操作,例如使用 context 包。
無數(shù)據(jù)傳遞:使用
struct{}
類型的通道強(qiáng)調(diào)了這是一個(gè)純粹的信號(hào)機(jī)制,不涉及數(shù)據(jù)傳輸。資源管理:一旦操作完成并且通道關(guān)閉,相關(guān)的 goroutine 就可以退出,防止資源泄露。
這種模式在 Go 的并發(fā)編程中非常常見,特別是當(dāng)你需要等待一個(gè)或多個(gè)后臺(tái)任務(wù)完成時(shí)。它提供了一種簡潔、高效的方式來協(xié)調(diào)不同的 goroutine,
ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 使用ctx來控制goroutine的生命周期
context.Background()
- 這是一個(gè)空的 Context,通常用作根 Context。
- 它永遠(yuǎn)不會(huì)被取消,沒有值,也沒有截止時(shí)間。
context.WithCancel(context.Background())
- 基于 Background Context 創(chuàng)建一個(gè)新的可取消的 Context。
- 返回新創(chuàng)建的 Context 和一個(gè)取消函數(shù)。
ctx, cancel := ...
ctx
是新創(chuàng)建的可取消 Context。cancel
是用于取消這個(gè) Context 的函數(shù)。
defer cancel()
- 確保在函數(shù)退出時(shí)調(diào)用 cancel 函數(shù)。
- 這是一個(gè)好習(xí)慣,可以防止資源泄露。
使用 ctx
來控制 goroutine 的生命周期的詳細(xì)解讀:
啟動(dòng) goroutine:
go func() { for { select { case <-ctx.Done(): // Context被取消,退出goroutine return default: // 執(zhí)行實(shí)際工作 // ... } } }()
傳遞 Context:
- 將
ctx
傳遞給需要控制的函數(shù)或方法。
go doSomething(ctx)
- 將
在函數(shù)中使用 Context:
func doSomething(ctx context.Context) { for { select { case <-ctx.Done(): // 處理取消邏輯 return case <-time.After(1 * time.Second): // 執(zhí)行定期任務(wù) } } }
取消操作:
- 當(dāng)需要停止所有使用這個(gè) Context 的操作時(shí),調(diào)用
cancel()
函數(shù)。
- 當(dāng)需要停止所有使用這個(gè) Context 的操作時(shí),調(diào)用
處理取消:
- 在 goroutine 中,可以通過檢查
ctx.Done()
通道來判斷是否應(yīng)該退出。 ctx.Err()
可以提供額外的錯(cuò)誤信息(如超時(shí)或取消)。
- 在 goroutine 中,可以通過檢查
超時(shí)控制:
- 可以使用
context.WithTimeout
或context.WithDeadline
來添加超時(shí)控制。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()
- 可以使用
值傳遞:
- Context 還可以用來傳遞請求范圍的值。
ctx = context.WithValue(ctx, key, value)
優(yōu)點(diǎn):
- 優(yōu)雅地控制 goroutine 的生命周期。
- 避免 goroutine 泄露。
- 在整個(gè)調(diào)用鏈中傳播取消信號(hào)。
- 可以輕松實(shí)現(xiàn)超時(shí)控制。
- 提供了一種標(biāo)準(zhǔn)的方式來傳遞請求范圍的值。
注意事項(xiàng):
- 不要將 Context 存儲(chǔ)在結(jié)構(gòu)體中,而應(yīng)該將其作為第一個(gè)參數(shù)傳遞給需要它的函數(shù)。
- Context 應(yīng)該及時(shí)取消,以釋放資源。
- 不要傳遞 nil Context,如果不確定使用什么 Context,可以使用
context.TODO()
。
6.可能疑惑的問題:
done := make(chan struct{}) go func() { // 執(zhí)行操作... close(done) }() // 等待操作完成 <-done
在該代碼中,啟動(dòng)的協(xié)程下面緊跟著的代碼就是<-done,如果done通道中無數(shù)據(jù),那么主goroutine不應(yīng)該會(huì)阻塞在這里嗎,為什么在回答的第2點(diǎn)中卻說 這允許主 goroutine 繼續(xù)執(zhí)行,而不會(huì)被阻塞 呢?
解答:
先梳理一下代碼的執(zhí)行過程 :
done := make(chan struct{})
- 創(chuàng)建一個(gè)無緩沖的未關(guān)閉的通道
done
。 - 這個(gè)通道用來通知主 goroutine 某個(gè)操作已完成。
- 創(chuàng)建一個(gè)無緩沖的未關(guān)閉的通道
go func() { ... }()
- 啟動(dòng)一個(gè)新的 goroutine,并在這個(gè) goroutine 中執(zhí)行匿名函數(shù)。
- 匿名函數(shù)會(huì)執(zhí)行具體的操作,然后關(guān)閉
done
通道。
close(done)
- 在匿名函數(shù)中,當(dāng)操作完成時(shí),關(guān)閉
done
通道。 - 關(guān)閉通道會(huì)通知所有等待該通道的接收者。
- 在匿名函數(shù)中,當(dāng)操作完成時(shí),關(guān)閉
<-done
- 主 goroutine 在這里等待
done
通道的關(guān)閉。 - 這行代碼會(huì)阻塞當(dāng)前的主 goroutine,直到
done
通道被關(guān)閉。 - 一旦
done
通道關(guān)閉,主 goroutine 將繼續(xù)執(zhí)行后續(xù)代碼。
- 主 goroutine 在這里等待
進(jìn)一步解釋阻塞與非阻塞
啟動(dòng) goroutine 是非阻塞操作:當(dāng)
go func() { ... }()
這行代碼執(zhí)行時(shí),主 goroutine 會(huì)立即啟動(dòng)匿名函數(shù)做并行處理,然后繼續(xù)執(zhí)行主 goroutine 后面的代碼。也就是說,啟動(dòng) goroutine 本身不會(huì)阻塞主 goroutine。等待通道結(jié)果是阻塞操作:主 goroutine 接著執(zhí)行的
<-done
是一個(gè)阻塞操作。主 goroutine 將在<-done
這一行阻塞,直到done
通道被關(guān)閉。
為什么說“這允許主 goroutine 繼續(xù)執(zhí)行,而不會(huì)被阻塞”
前面的那句話實(shí)際是解釋在啟動(dòng) goroutine 時(shí),主 goroutine 是不會(huì)受阻塞的,這意味著它會(huì)繼續(xù)往下執(zhí)行 <-done
這一行代碼。但當(dāng)執(zhí)行到 <-done
時(shí),確實(shí)會(huì)阻塞,等待 done
通道被關(guān)閉。
這樣做的關(guān)鍵目的是為了同步 goroutine 的執(zhí)行:
- 啟動(dòng) goroutine 讓它去做一些獨(dú)立的工作。
- 主 goroutine 在啟動(dòng) goroutine 后等待它完成,通過
<-done
實(shí)現(xiàn)這一點(diǎn)。
這種模式非常適合在并發(fā)編程中進(jìn)行同步,確保主 goroutine 等待某個(gè) goroutine 完成實(shí)際工作后再繼續(xù)執(zhí)行。這種方法保證了并發(fā)程序的正確性和同步,并且避免了 goroutine 泄露(即 goroutine 執(zhí)行完任務(wù)后實(shí)際退出)。
7.解釋一下通道的selcet語句:
select是Go語言中的一個(gè)控制結(jié)構(gòu),專門用于處理多個(gè)通道操作。它的作用類似于switch語句,但是專門針對通道操作設(shè)計(jì)。
以下是select語句的詳細(xì)解析:
- 基本語法:
select { case <-ch1: // 如果可以從ch1接收數(shù)據(jù),執(zhí)行這里的代碼 case x := <-ch2: // 如果可以從ch2接收數(shù)據(jù),將數(shù)據(jù)賦值給x,然后執(zhí)行這里的代碼 case ch3 <- y: // 如果可以向ch3發(fā)送數(shù)據(jù)y,執(zhí)行這里的代碼 default: // 如果上面的case都沒有準(zhǔn)備好,執(zhí)行這里的代碼 }
- 工作原理:
- select會(huì)同時(shí)監(jiān)聽所有case語句中的通道操作。
- 如果多個(gè)通道同時(shí)準(zhǔn)備就緒,select會(huì)隨機(jī)選擇一個(gè)case執(zhí)行。
- 如果沒有任何通道準(zhǔn)備就緒,且有default語句,就執(zhí)行default語句。
- 如果沒有default語句,select將阻塞,直到某個(gè)通道可以操作。
- 常見用法:
a. 非阻塞通道操作:
select { case msg := <-ch: fmt.Println("Received:", msg) default: fmt.Println("No message received") }
b. 超時(shí)處理:
select { case res := <-ch: fmt.Println("Received:", res) case <-time.After(1 * time.Second): fmt.Println("Timeout") }
c. 多通道監(jiān)聽:
for { select { case msg1 := <-ch1: fmt.Println("ch1 received:", msg1) case msg2 := <-ch2: fmt.Println("ch2 received:", msg2) case <-done: return } }
- 特殊情況:
- 空select(沒有任何case)會(huì)永遠(yuǎn)阻塞:
select {}
- 當(dāng)多個(gè)case同時(shí)就緒時(shí),Go運(yùn)行時(shí)會(huì)偽隨機(jī)地選擇一個(gè)case執(zhí)行。
8.超時(shí)處理分析:
select { case res := <-ch: fmt.Println("Received:", res) case <-time.After(1 * time.Second): fmt.Println("Timeout") }
select 語句:
- select 是Go語言的一個(gè)控制結(jié)構(gòu),用于同時(shí)監(jiān)聽多個(gè)通道操作。
- 它會(huì)阻塞,直到其中一個(gè)case可以執(zhí)行。
- 如果多個(gè)case同時(shí)就緒,會(huì)隨機(jī)選擇一個(gè)執(zhí)行。
第一個(gè) case:
case res := <-ch:
- 這個(gè)case嘗試從通道
ch
接收數(shù)據(jù)。 - 如果
ch
中有數(shù)據(jù)可讀,這個(gè)case會(huì)被選中執(zhí)行。 res
變量會(huì)被賦值為從通道接收到的數(shù)據(jù)。
- 這個(gè)case嘗試從通道
第一個(gè) case 的執(zhí)行體:
fmt.Println("Received:", res)
- 如果從
ch
成功接收到數(shù)據(jù),將打印 “Received:” 以及接收到的數(shù)據(jù)。
- 如果從
第二個(gè) case:
case <-time.After(1 * time.Second):
time.After(1 * time.Second)
返回一個(gè)通道,這個(gè)通道將在1秒后發(fā)送一個(gè)值。- 這個(gè)case會(huì)在1秒后變?yōu)榭蓤?zhí)行狀態(tài)。
- 如果在1秒內(nèi)第一個(gè)case沒有被觸發(fā),這個(gè)case就會(huì)被選中。
第二個(gè) case 的執(zhí)行體:
fmt.Println("Timeout")
- 如果超過1秒還沒有從
ch
接收到數(shù)據(jù),將打印 “Timeout”。
- 如果超過1秒還沒有從
整體邏輯:
- 這個(gè)select語句同時(shí)等待兩個(gè)事件:從
ch
接收數(shù)據(jù)或1秒超時(shí)。 - 如果在1秒內(nèi)從
ch
接收到數(shù)據(jù),程序會(huì)打印接收到的數(shù)據(jù)。 - 如果1秒過去了還沒有從
ch
接收到數(shù)據(jù),就會(huì)觸發(fā)超時(shí),程序會(huì)打印超時(shí)消息。
使用場景:
- 這種模式常用于需要設(shè)置超時(shí)的操作,例如:
等待異步操作的結(jié)果
網(wǎng)絡(luò)請求
用戶輸入
資源獲?。ㄈ鐢?shù)據(jù)庫查詢)
注意事項(xiàng):
ch
應(yīng)該是在其他地方定義的一個(gè)通道,可能由另一個(gè)goroutine寫入數(shù)據(jù)。- 1秒的超時(shí)時(shí)間是一個(gè)示例,實(shí)際使用時(shí)應(yīng)根據(jù)具體需求調(diào)整。
- 這種模式不會(huì)取消正在進(jìn)行的操作。如果需要取消操作,應(yīng)該考慮使用context包。
- 如果
ch
是一個(gè)無緩沖通道,確保有其他goroutine在寫入數(shù)據(jù),否則可能導(dǎo)致goroutine泄漏。
優(yōu)點(diǎn):
- 簡潔而強(qiáng)大,能夠優(yōu)雅地處理超時(shí)情況。
- 避免程序因等待某個(gè)可能永遠(yuǎn)不會(huì)完成的操作而無限阻塞。
這種模式展示了Go語言在處理并發(fā)和超時(shí)問題時(shí)的優(yōu)雅和高效。它允許開發(fā)者輕松地實(shí)現(xiàn)非阻塞操作和超時(shí)控制,這在構(gòu)建可靠的并發(fā)系統(tǒng)時(shí)非常有用。
9.對注意事項(xiàng)中的第3點(diǎn)進(jìn)行分析:
這句話指出了使用 select 和 time.After 實(shí)現(xiàn)的超時(shí)模式的一個(gè)局限性,同時(shí)提供了一個(gè)更完善的解決方案。詳細(xì)解釋如下:
局限性:
使用 select 和 time.After 的超時(shí)模式只能檢測到超時(shí),但不能主動(dòng)取消或停止正在進(jìn)行的操作。如果超時(shí)發(fā)生,代碼會(huì)執(zhí)行超時(shí)分支,但原來的操作可能仍在后臺(tái)繼續(xù)執(zhí)行,這可能導(dǎo)致資源浪費(fèi)或其他問題。context 包的作用:
Go 的 context 包提供了一種優(yōu)雅的方式來傳遞截止時(shí)間、取消信號(hào)或其他請求范圍的值across API邊界和進(jìn)程。使用 context,你可以:- 設(shè)置超時(shí)
- 取消操作
- 傳遞請求范圍的值
使用 context 的優(yōu)勢:
- 可以在超時(shí)發(fā)生時(shí)主動(dòng)取消正在進(jìn)行的操作
- 可以在多個(gè) goroutine 之間協(xié)調(diào)取消操作
- 提供了一種標(biāo)準(zhǔn)的方式來處理取消和超時(shí)
示例對比:
不使用 context 的版本(無法取消操作):
func doOperation() <-chan int { resultChan := make(chan int) go func() { // 假設(shè)這是一個(gè)耗時(shí)操作 time.Sleep(2 * time.Second) resultChan <- 42 }() return resultChan } func main() { select { case result := <-doOperation(): fmt.Println("Result:", result) case <-time.After(1 * time.Second): fmt.Println("Timeout") // 操作仍在后臺(tái)繼續(xù)執(zhí)行 } }
使用 context 的版本(可以取消操作):
func doOperation(ctx context.Context) <-chan int { resultChan := make(chan int) go func() { select { case <-time.After(2 * time.Second): resultChan <- 42 case <-ctx.Done(): fmt.Println("Operation cancelled") return } }() return resultChan } func main() { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() select { case result := <-doOperation(ctx): fmt.Println("Result:", result) case <-ctx.Done(): fmt.Println("Timeout") // 操作被取消,不會(huì)繼續(xù)執(zhí)行 } }
在使用 context 的版本中,如果超時(shí)發(fā)生,操作會(huì)被主動(dòng)取消,避免了資源浪費(fèi)。這種方法更加靈活和強(qiáng)大,特別是在處理復(fù)雜的并發(fā)場景時(shí)。
總之,雖然 select 和 time.After 的模式簡單直接,但在需要真正取消操作或在多個(gè) goroutine 間協(xié)調(diào)的場景中,使用 context 包是更好的選擇。
10.對使用 context 的版本的代碼進(jìn)行分析:
1.context是什么
Context 是 Go 語言中用于跨 API 邊界和進(jìn)程間傳遞截止時(shí)間、取消信號(hào)以及其他請求作用域值的一個(gè)標(biāo)準(zhǔn)包。它是 Go 1.7 版本引入的,主要用于解決 goroutine 管理和請求取消的問題。以下是關(guān)于 context 的幾個(gè)關(guān)鍵點(diǎn):
主要用途:
- 傳遞取消信號(hào)
- 傳遞截止時(shí)間
- 傳遞請求作用域的值
- 跨 API 邊界的數(shù)據(jù)傳遞
核心接口:
Context 接口定義如下:
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
主要功能:
- 取消操作:通過 Done() 方法返回一個(gè) channel,當(dāng) context 被取消時(shí),這個(gè) channel 會(huì)被關(guān)閉。
- 設(shè)置截止時(shí)間:可以為操作設(shè)置一個(gè)截止時(shí)間。
- 傳值:可以攜帶請求作用域的值,用于跨函數(shù)、跨 API 調(diào)用傳遞數(shù)據(jù)。
常用函數(shù):
- context.Background():返回一個(gè)空的 Context,通常用作頂層 Context。
- context.TODO():當(dāng)不確定使用哪種 Context 時(shí)使用。
- context.WithCancel(parent Context):創(chuàng)建一個(gè)可取消的子 Context。
- context.WithDeadline(parent Context, deadline time.Time):創(chuàng)建一個(gè)具有截止時(shí)間的子 Context。
- context.WithTimeout(parent Context, timeout time.Duration):創(chuàng)建一個(gè)具有超時(shí)時(shí)間的子 Context。
- context.WithValue(parent Context, key, val interface{}):創(chuàng)建一個(gè)攜帶鍵值對的子 Context。
使用場景:
- HTTP 請求的上下文傳遞
- 數(shù)據(jù)庫查詢的超時(shí)控制
- 多個(gè) goroutine 之間的協(xié)調(diào)和取消
- 跨服務(wù)調(diào)用的上下文傳遞
最佳實(shí)踐:
- 將 Context 作為函數(shù)的第一個(gè)參數(shù)傳遞。
- 不要將 nil 作為 Context 類型的參數(shù)值,如果不確定使用什么,就使用 context.TODO()。
- Context 應(yīng)該是只讀的,不要修改它。
- 同一個(gè) Context 可以傳遞給在不同 goroutine 中運(yùn)行的函數(shù)。
優(yōu)點(diǎn):
- 提供了一種標(biāo)準(zhǔn)的方式來處理取消和超時(shí)。
- 使得跨 API 和進(jìn)程邊界的請求作用域數(shù)據(jù)傳遞變得簡單。
- 有助于防止資源泄漏。
注意事項(xiàng):
- Context 應(yīng)該貫穿整個(gè)請求的生命周期。
- 不要將 Context 存儲(chǔ)在結(jié)構(gòu)體中,而應(yīng)該顯式地傳遞。
- 取消操作是建議性的,不是強(qiáng)制性的。被取消的函數(shù)應(yīng)該盡快返回,但可能需要一些清理工作。
Context 包的引入大大簡化了在 Go 程序中處理取消、超時(shí)和跨調(diào)用邊界傳值的復(fù)雜性,是構(gòu)建健壯的并發(fā)和分布式系統(tǒng)的重要工具。
2.對ctx.Done()的分析:
ctx.Done()
是 Context 接口中的一個(gè)重要方法,它返回一個(gè)只讀的 channel(<-chan struct{}
)。這個(gè)方法在處理 context 的取消和超時(shí)時(shí)非常關(guān)鍵。以下是關(guān)于 ctx.Done()
的詳細(xì)解釋:
功能:
- 返回一個(gè) channel,當(dāng) context 被取消或到達(dá)截止時(shí)間時(shí),這個(gè) channel 會(huì)被關(guān)閉。
- 用于通知相關(guān)的 goroutine context 已經(jīng)結(jié)束,應(yīng)該停止當(dāng)前操作。
返回值:
- 類型為
<-chan struct{}
,這是一個(gè)只讀的 channel。 - 當(dāng) channel 關(guān)閉時(shí),讀取操作會(huì)立即返回一個(gè)零值(對于 struct{} 類型來說就是空結(jié)構(gòu)體)。
使用場景:
- 在 select 語句中監(jiān)聽 context 的取消信號(hào)。
- 在長時(shí)間運(yùn)行的操作中周期性檢查是否應(yīng)該停止。
- 協(xié)調(diào)多個(gè) goroutine 的取消操作。
典型用法:
select { case <-ctx.Done(): // Context 已被取消,執(zhí)行清理操作 return ctx.Err() case <-someOtherChannel: // 處理其他情況 }
工作原理:
- 當(dāng) context 被取消(通過調(diào)用 cancel 函數(shù))或達(dá)到截止時(shí)間時(shí),Done() 返回的 channel 會(huì)被關(guān)閉。
- channel 的關(guān)閉會(huì)立即解除所有等待在這個(gè) channel 上的 goroutine 的阻塞狀態(tài)。
注意事項(xiàng):
ctx.Done()
本身不會(huì)阻塞,它只是返回一個(gè) channel。- 讀取這個(gè) channel(如
<-ctx.Done()
)會(huì)阻塞,直到 channel 被關(guān)閉。 - 如果 context 永遠(yuǎn)不會(huì)被取消(如使用 context.Background()),則 Done() 返回的 channel 永遠(yuǎn)不會(huì)被關(guān)閉。
與 ctx.Err() 的關(guān)系:
- 當(dāng)
ctx.Done()
返回的 channel 被關(guān)閉時(shí),ctx.Err()
會(huì)返回一個(gè)非空錯(cuò)誤,表明 context 被取消的原因(如 “context canceled” 或 “context deadline exceeded”)。
最佳實(shí)踐:
- 在處理長時(shí)間運(yùn)行的操作時(shí),定期檢查
ctx.Done()
。 - 結(jié)合使用
select
語句和ctx.Done()
來實(shí)現(xiàn)可取消的操作。 - 在返回時(shí),通常應(yīng)該檢查并返回
ctx.Err()
,以傳播取消的原因。
ctx.Done()
是實(shí)現(xiàn)優(yōu)雅取消和超時(shí)處理的關(guān)鍵機(jī)制,它允許 Go 程序以非阻塞的方式響應(yīng)取消信號(hào),從而編寫出更加健壯和響應(yīng)式的并發(fā)代碼。
3.代碼分析:
- doOperation 函數(shù):
func doOperation(ctx context.Context) <-chan int { resultChan := make(chan int) go func() { select { case <-time.After(2 * time.Second): resultChan <- 42 case <-ctx.Done(): fmt.Println("Operation cancelled") return } }() return resultChan }
函數(shù)接收一個(gè) context.Context 參數(shù),返回一個(gè)只讀的整數(shù)通道。
創(chuàng)建一個(gè) resultChan 通道用于返回結(jié)果。
啟動(dòng)一個(gè) goroutine 執(zhí)行實(shí)際的操作:
- 使用 select 語句同時(shí)等待兩個(gè)事件:
2秒后的定時(shí)器觸發(fā)(模擬耗時(shí)操作)
context 被取消
- 如果 2 秒后定時(shí)器先觸發(fā),將 42 發(fā)送到 resultChan。
- 如果 context 被取消,打印取消消息并返回,不發(fā)送結(jié)果。
- 立即返回 resultChan,不等待 goroutine 完成。
main 函數(shù):
func main() { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() select { case result := <-doOperation(ctx): fmt.Println("Result:", result) case <-ctx.Done(): fmt.Println("Timeout") } }
創(chuàng)建一個(gè)帶有 1 秒超時(shí)的 context:
context.Background()
創(chuàng)建一個(gè)空的 context。context.WithTimeout
基于背景 context 創(chuàng)建一個(gè) 1 秒后會(huì)自動(dòng)取消的新 context。cancel
是一個(gè)函數(shù),可以手動(dòng)取消 context(這里通過 defer 確保在函數(shù)結(jié)束時(shí)調(diào)用)。
使用 select 語句等待兩個(gè)可能的結(jié)果:
從 doOperation 返回的通道接收結(jié)果。
context 超時(shí)或被取消(通過 ctx.Done()
通道)。
執(zhí)行流程:
- main 函數(shù)創(chuàng)建一個(gè) 1 秒超時(shí)的 context。
- 調(diào)用 doOperation,傳入這個(gè) context。
- doOperation 啟動(dòng)一個(gè) goroutine,模擬一個(gè)需要 2 秒完成的操作。
- main 函數(shù)的 select 等待結(jié)果或超時(shí)。
- 由于 context 設(shè)置為 1 秒后超時(shí),而操作需要 2 秒:
- context 會(huì)在 1 秒后被取消。
- main 函數(shù)中的
<-ctx.Done()
會(huì)先觸發(fā)。 - “Timeout” 會(huì)被打印。
- 同時(shí),在 doOperation 的 goroutine 中,
<-ctx.Done()
也會(huì)觸發(fā)。- “Operation cancelled” 會(huì)被打印。
- goroutine 會(huì)提前退出,不會(huì)發(fā)送結(jié)果。
關(guān)鍵點(diǎn):
- 使用 context 可以在多個(gè) goroutine 之間協(xié)調(diào)取消操作。
- 這種模式允許優(yōu)雅地處理超時(shí),并確保資源不會(huì)被不必要地占用。
- defer cancel() 確保即使發(fā)生 panic,context 也會(huì)被正確取消。
- 這個(gè)例子展示了如何在超時(shí)發(fā)生時(shí)取消正在進(jìn)行的操作,這是簡單的 select + time.After 模式所不能實(shí)現(xiàn)的。
這種模式在處理網(wǎng)絡(luò)請求、數(shù)據(jù)庫查詢或其他可能需要超時(shí)控制的耗時(shí)操作時(shí)特別有用。
11.可能疑惑的問題:
為什么main函數(shù)中的<-ctx.Done()先執(zhí)行,而不是doOperation函數(shù)中的<-ctx.Done()先執(zhí)行 ?在這個(gè)具體的例子中,main 函數(shù)中的 <-ctx.Done()
會(huì)先執(zhí)行。這是因?yàn)椋?/p>
時(shí)間設(shè)置:
- main 函數(shù)中創(chuàng)建的 context 設(shè)置了 1 秒的超時(shí):
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
- doOperation 函數(shù)模擬的操作需要 2 秒才能完成:
case <-time.After(2 * time.Second):
- main 函數(shù)中創(chuàng)建的 context 設(shè)置了 1 秒的超時(shí):
執(zhí)行順序:
- main 函數(shù)調(diào)用 doOperation 并立即進(jìn)入 select 語句等待。
- doOperation 啟動(dòng)一個(gè)新的 goroutine 并立即返回 channel。
- main 函數(shù)和 doOperation 中的 goroutine 并行執(zhí)行。
超時(shí)觸發(fā):
- 1 秒后,main 函數(shù)中的 context 達(dá)到超時(shí)時(shí)間。
- context 被取消,這會(huì)同時(shí)關(guān)閉 main 函數(shù)和 doOperation 中的
ctx.Done()
channel。
結(jié)果:
- main 函數(shù)中的
<-ctx.Done()
分支會(huì)先被觸發(fā),因?yàn)樗诘却?1 秒的超時(shí)。 - 緊接著,doOperation 中的
<-ctx.Done()
也會(huì)被觸發(fā)。
- main 函數(shù)中的
輸出順序:
- 你會(huì)看到 main 函數(shù)先打印 “Timeout”。
- 然后 doOperation 的 goroutine 會(huì)打印 “Operation cancelled”。
這種行為展示了 context 的一個(gè)重要特性:當(dāng)一個(gè) context 被取消時(shí),它會(huì)立即通知所有使用該 context 的 goroutine。這允許程序在不同的部分協(xié)調(diào)取消操作,即使這些部分在不同的 goroutine 中運(yùn)行。
需要注意的是,雖然 main 函數(shù)中的 <-ctx.Done()
先執(zhí)行,但兩者執(zhí)行的時(shí)間差通常非常小,幾乎是同時(shí)的。這個(gè)順序主要是由于 main 函數(shù)直接等待 context 的取消,而 doOperation 中還有一個(gè)額外的 select 語句。
- 注意事項(xiàng):
- select語句不會(huì)按照case的順序進(jìn)行選擇。
- 空的select{}會(huì)永遠(yuǎn)阻塞。
- 在循環(huán)中使用select時(shí),要注意避免CPU占用過高(可以在default中加入短暫的睡眠)。
- 使用select實(shí)現(xiàn)超時(shí)或取消操作時(shí),記得正確關(guān)閉相關(guān)的goroutine和資源。
select語句是Go并發(fā)編程中的一個(gè)強(qiáng)大工具,它允許你同時(shí)處理多個(gè)通道操作,實(shí)現(xiàn)非阻塞I/O、超時(shí)處理、優(yōu)雅退出等復(fù)雜的并發(fā)控制流程。深入理解和靈活運(yùn)用select可以幫助你編寫更加高效和健壯的并發(fā)程序。
到此這篇關(guān)于Go語言協(xié)程通道使用的問題小結(jié)的文章就介紹到這了,更多相關(guān)Go語言協(xié)程通道內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go常用標(biāo)準(zhǔn)庫之fmt的簡介與使用詳解
fmt 是 Go 語言中的一個(gè)常用標(biāo)準(zhǔn)庫,它用于格式化輸入和輸出數(shù)據(jù),這篇文章主要為大家介紹了fmt的基本使用,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-10-10go 判斷兩個(gè) slice/struct/map 是否相等的實(shí)例
這篇文章主要介紹了go 判斷兩個(gè) slice/struct/map 是否相等的實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12GoFrame?gmap遍歷hashmap?listmap?treemap使用技巧
這篇文章主要為大家介紹了GoFrame?gmap遍歷hashmap?listmap?treemap使用技巧的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06Go動(dòng)態(tài)調(diào)用函數(shù)的實(shí)例教程
本文主要介紹了Go動(dòng)態(tài)調(diào)用函數(shù)的實(shí)例教程,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01淺析Go語言如何在終端里實(shí)現(xiàn)倒計(jì)時(shí)
這篇文章主要為大家詳細(xì)介紹了Go語言中是如何在終端里實(shí)現(xiàn)倒計(jì)時(shí)的,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-03-03