一文教你Golang如何正確關(guān)閉通道
序言
Go 在通道這一塊,沒有內(nèi)置函數(shù)判斷通道是否已經(jīng)關(guān)閉,也沒有可以直接獲取當(dāng)前通道數(shù)量的方法。所以對于通道,Go 顯示的不是那么優(yōu)雅。另外,如果對通道進行了錯誤的使用,將會直接引發(fā)系統(tǒng) panic,這是一件很危險的事情。
如何判斷通道是否關(guān)閉
雖然沒有判斷通道是否關(guān)閉的內(nèi)置函數(shù),但是官方為我們提供了一種語法來判斷通道是否關(guān)閉:
v, ok := <-ch // 如果ok為true則代表通道已經(jīng)關(guān)閉
利用這個語法,我們可以編寫這樣的代碼判斷通道是否關(guān)閉:
func TestChanClosed(t *testing.T) {
var ch = make(chan int)
// send
go func() {
for {
ch <- 1
}
}()
// receive
go func() {
for {
if v, ok := <-ch; ok {
t.Log(v)
} else {
t.Log("通道關(guān)閉")
return
}
}
}()
time.Sleep(1 * time.Second)
}也可以用 for range 簡化語法,通道關(guān)閉后會主動退出 for 循環(huán):
func TestChanClosed(t *testing.T) {
var ch = make(chan int)
// send
go func() {
for {
ch <- 1
}
}()
// receive
go func() {
for v := range ch {
t.Log(v)
}
t.Log("通道關(guān)閉")
return
}()
time.Sleep(1 * time.Second)
}什么樣的情況會 panic
有三種情況會引發(fā) panic:
// 會引發(fā)channel panic的情況一:發(fā)送數(shù)據(jù)到已經(jīng)關(guān)閉的channel
// panic: send on closed channel
func TestChannelPanic1(t *testing.T) {
var ch = make(chan int)
close(ch)
time.Sleep(10 * time.Millisecond)
go func() {
ch <- 1
}()
t.Log(<-ch)
}
// 會引發(fā)channel panic的情況一的另外一種:發(fā)送數(shù)據(jù)時關(guān)閉channel
// panic: send on closed channel
func TestChannelPanic11(t *testing.T) {
var ch = make(chan int)
go func() {
go func() {
// 沒有接收數(shù)據(jù)的地方,此處會一直阻塞
ch <- 1
}()
}()
time.Sleep(20 * time.Millisecond)
close(ch)
}
// 會引發(fā)channel panic的情況二:重復(fù)關(guān)閉channel
// panic: close of closed channel
func TestChannelPanic2(t *testing.T) {
var ch = make(chan int)
close(ch)
close(ch)
}
// 會引發(fā)channel panic的情況三:未初始化關(guān)閉
// panic: close of nil channel
func TestChannelPanic3(t *testing.T) {
var ch chan int
close(ch)
}我們在實際的業(yè)務(wù)中應(yīng)該避免這三種不同的 panic,未初始化就關(guān)閉的情況較為少見,也不容易犯錯誤,重要的是要防止關(guān)閉后發(fā)送數(shù)據(jù)和重復(fù)關(guān)閉通道。
如何避免 panic
在 go 中有一條原則:Channel Closing Principle,它是指不要從接收端關(guān)閉 channel,也不要關(guān)閉有多個并發(fā)發(fā)送者的 channel。只要我們嚴(yán)格遵守這個原則,就可以有效的避免panic。其實這個原則就是讓我們規(guī)避關(guān)閉后發(fā)送和重復(fù)關(guān)閉這兩種情況。
為了應(yīng)對關(guān)閉后發(fā)送數(shù)據(jù)這種情況,我們很容易想到Channel Closing Principle的第一句:不要從接收端關(guān)閉 channel。所以我們應(yīng)該從發(fā)送端關(guān)閉 channel:
func TestSendClose(t *testing.T) {
var (
ch = make(chan int)
wg = sync.WaitGroup{}
// 10毫秒后通知發(fā)送端停止發(fā)送數(shù)據(jù)
after = time.After(10 * time.Millisecond)
)
wg.Add(2)
// send
go func() {
for {
select {
case <-after:
close(ch)
wg.Done()
return
default:
ch <- 1
}
}
}()
// receive
go func() {
defer wg.Done()
for v := range ch {
t.Log(v)
}
return
}()
wg.Wait()
}這種方式可以應(yīng)對單發(fā)送者的情況,如果我們的程序有多個發(fā)送者,那么就要考慮Channel Closing Principle的第二句話:不要關(guān)閉有多個并發(fā)發(fā)送者的 channel。那么這種情況下,我們應(yīng)該如何正確的回收通道呢?這個時候我們可以考慮引入一個額外的通道,當(dāng)接收端不想再接收數(shù)據(jù)時,就發(fā)送數(shù)據(jù)到這個額外的通道中,來通知所有的發(fā)送端退出:
func TestManySendAndOneReceive(t *testing.T) {
var (
sender = 3
wg = sync.WaitGroup{}
numCh = make(chan int)
stopCh = make(chan struct{})
// 10毫秒后通知發(fā)送端停止發(fā)送數(shù)據(jù)
after = time.After(10 * time.Millisecond)
)
wg.Add(1)
// send
for i := 0; i < sender; i++ {
go func() {
for {
select {
case <-stopCh:
fmt.Println("收到退出信號")
return
case numCh <- 1:
//fmt.Println("發(fā)送成功", value)
}
}
}()
}
// receive
go func() {
for {
select {
case v := <-numCh:
fmt.Println("接收到數(shù)據(jù)", v)
case <-after:
close(stopCh)
wg.Done()
return
}
}
}()
wg.Wait()
}看完這段代碼,我們發(fā)現(xiàn) numCh 這個通道是沒有關(guān)閉語句的,那么這段代碼會引發(fā)內(nèi)存泄漏嗎?答案是不會,因為我們正確退出了發(fā)送端和接收端的所有協(xié)程,等到這個通道沒有任何代碼使用后,Go 的垃圾回收會回收此通道。
那如果此時我們的程序變得更為復(fù)雜:有多個接收者和多個發(fā)送者,這個時候怎么辦呢?我們可以引入另外一個中間者,當(dāng)任意協(xié)程想關(guān)閉的時候,都通知這個中間者,所有協(xié)程也同時監(jiān)聽這個中間者,收到中間者的退出信號時,退出當(dāng)前協(xié)程:
func TestManySendAndManyReceive(t *testing.T) {
var (
maxRandomNumber = 5000
receiver = 10
sender = 10
wg = sync.WaitGroup{}
numCh = make(chan int)
stopCh = make(chan struct{})
toStop = make(chan string, 1)
stoppedBy string
)
wg.Add(receiver)
// moderator
go func() {
stoppedBy = <-toStop
close(stopCh)
}()
// senders
for i := 0; i < sender; i++ {
go func(id string) {
for {
value := rand.Intn(maxRandomNumber)
if value == 0 {
select {
case toStop <- "sender#" + id:
default:
}
return
}
// 提前關(guān)閉goroutine
select {
case <-stopCh:
return
default:
}
select {
case <-stopCh:
return
case numCh <- value:
}
}
}(strconv.Itoa(i))
}
// receivers
for i := 0; i < receiver; i++ {
go func(id string) {
defer wg.Done()
for {
// 提前關(guān)閉goroutine
select {
case <-stopCh:
return
default:
}
select {
case <-stopCh:
return
case value := <-numCh:
if value == maxRandomNumber-1 {
select {
case toStop <- "receiver#" + id:
default:
}
return
}
t.Log(value)
}
}
}(strconv.Itoa(i))
}
wg.Wait()
t.Log("stopped by", stoppedBy)
}避免重復(fù)關(guān)閉通道
可以使用 sync.once 語法來避免重復(fù)關(guān)閉通道:
type MyChannel struct {
C chan interface{}
once sync.Once
}
func NewMyChannel() *MyChannel {
return &MyChannel{C: make(chan interface{})}
}
func (mc *MyChannel) SafeClose() {
mc.once.Do(func(){
close(mc.C)
})
}也可以使用 sync.Mutex 語法避免重復(fù)關(guān)閉通道:
type MyChannel struct {
C chan interface{}
closed bool
mutex sync.Mutex
}
func NewMyChannel() *MyChannel {
return &MyChannel{C: make(chan interface{})}
}
func (mc *MyChannel) SafeClose() {
mc.mutex.Lock()
if !mc.closed {
close(mc.C)
mc.closed = true
}
mc.mutex.Unlock()
}
func (mc *MyChannel) IsClosed() bool {
mc.mutex.Lock()
defer mc.mutex.Unlock()
return mc.closed
}總結(jié)
如何正確關(guān)閉 gotoutine 和 channel 防止內(nèi)存泄漏是一個重要的課題,如果在編碼過程中,遇到了需要打破Channel Closing Principle原則的情況,一定要思考自己的代碼設(shè)計是否合理。
到此這篇關(guān)于一文教你Golang如何正確關(guān)閉通道 的文章就介紹到這了,更多相關(guān)go關(guān)閉通道 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang websocket 服務(wù)端的實現(xiàn)
這篇文章主要介紹了golang websocket 服務(wù)端的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
Golang 字符串轉(zhuǎn)time類型實現(xiàn)
本文主要介紹了Golang 字符串轉(zhuǎn)time類型實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03
利用Go語言快速實現(xiàn)一個極簡任務(wù)調(diào)度系統(tǒng)
任務(wù)調(diào)度(Task Scheduling)是很多軟件系統(tǒng)中的重要組成部分,字面上的意思是按照一定要求分配運行一些通常時間較長的腳本或程序。本文將利用Go語言快速實現(xiàn)一個極簡任務(wù)調(diào)度系統(tǒng),感興趣的可以了解一下2022-10-10
在Golang中使用http.FileServer返回靜態(tài)文件的操作
這篇文章主要介紹了在Golang中使用http.FileServer返回靜態(tài)文件的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12

