一文教你Golang如何正確關閉通道
序言
Go 在通道這一塊,沒有內(nèi)置函數(shù)判斷通道是否已經(jīng)關閉,也沒有可以直接獲取當前通道數(shù)量的方法。所以對于通道,Go 顯示的不是那么優(yōu)雅。另外,如果對通道進行了錯誤的使用,將會直接引發(fā)系統(tǒng) panic,這是一件很危險的事情。
如何判斷通道是否關閉
雖然沒有判斷通道是否關閉的內(nèi)置函數(shù),但是官方為我們提供了一種語法來判斷通道是否關閉:
v, ok := <-ch // 如果ok為true則代表通道已經(jīng)關閉
利用這個語法,我們可以編寫這樣的代碼判斷通道是否關閉:
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("通道關閉") return } } }() time.Sleep(1 * time.Second) }
也可以用 for range
簡化語法,通道關閉后會主動退出 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("通道關閉") return }() time.Sleep(1 * time.Second) }
什么樣的情況會 panic
有三種情況會引發(fā) panic:
// 會引發(fā)channel panic的情況一:發(fā)送數(shù)據(jù)到已經(jīng)關閉的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ù)時關閉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的情況二:重復關閉channel // panic: close of closed channel func TestChannelPanic2(t *testing.T) { var ch = make(chan int) close(ch) close(ch) } // 會引發(fā)channel panic的情況三:未初始化關閉 // panic: close of nil channel func TestChannelPanic3(t *testing.T) { var ch chan int close(ch) }
我們在實際的業(yè)務中應該避免這三種不同的 panic,未初始化就關閉的情況較為少見,也不容易犯錯誤,重要的是要防止關閉后發(fā)送數(shù)據(jù)和重復關閉通道。
如何避免 panic
在 go 中有一條原則:Channel Closing Principle,它是指不要從接收端關閉 channel,也不要關閉有多個并發(fā)發(fā)送者的 channel。只要我們嚴格遵守這個原則,就可以有效的避免panic。其實這個原則就是讓我們規(guī)避關閉后發(fā)送
和重復關閉
這兩種情況。
為了應對關閉后發(fā)送數(shù)據(jù)這種情況,我們很容易想到Channel Closing Principle的第一句:不要從接收端關閉 channel。所以我們應該從發(fā)送端關閉 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() }
這種方式可以應對單發(fā)送者的情況,如果我們的程序有多個發(fā)送者,那么就要考慮Channel Closing Principle的第二句話:不要關閉有多個并發(fā)發(fā)送者的 channel。那么這種情況下,我們應該如何正確的回收通道呢?這個時候我們可以考慮引入一個額外的通道,當接收端不想再接收數(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
這個通道是沒有關閉語句的,那么這段代碼會引發(fā)內(nèi)存泄漏嗎?答案是不會,因為我們正確退出了發(fā)送端和接收端的所有協(xié)程,等到這個通道沒有任何代碼使用后,Go 的垃圾回收會回收此通道。
那如果此時我們的程序變得更為復雜:有多個接收者和多個發(fā)送者,這個時候怎么辦呢?我們可以引入另外一個中間者,當任意協(xié)程想關閉的時候,都通知這個中間者,所有協(xié)程也同時監(jiān)聽這個中間者,收到中間者的退出信號時,退出當前協(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 } // 提前關閉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 { // 提前關閉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) }
避免重復關閉通道
可以使用 sync.once 語法來避免重復關閉通道:
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 語法避免重復關閉通道:
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é)
如何正確關閉 gotoutine 和 channel 防止內(nèi)存泄漏是一個重要的課題,如果在編碼過程中,遇到了需要打破Channel Closing Principle原則的情況,一定要思考自己的代碼設計是否合理。
到此這篇關于一文教你Golang如何正確關閉通道 的文章就介紹到這了,更多相關go關閉通道 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Golang 字符串轉(zhuǎn)time類型實現(xiàn)
本文主要介紹了Golang 字符串轉(zhuǎn)time類型實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-03-03利用Go語言快速實現(xiàn)一個極簡任務調(diào)度系統(tǒng)
任務調(diào)度(Task Scheduling)是很多軟件系統(tǒng)中的重要組成部分,字面上的意思是按照一定要求分配運行一些通常時間較長的腳本或程序。本文將利用Go語言快速實現(xiàn)一個極簡任務調(diào)度系統(tǒng),感興趣的可以了解一下2022-10-10在Golang中使用http.FileServer返回靜態(tài)文件的操作
這篇文章主要介紹了在Golang中使用http.FileServer返回靜態(tài)文件的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12