一文教你Golang如何正確關閉通道
序言
Go 在通道這一塊,沒有內置函數判斷通道是否已經關閉,也沒有可以直接獲取當前通道數量的方法。所以對于通道,Go 顯示的不是那么優(yōu)雅。另外,如果對通道進行了錯誤的使用,將會直接引發(fā)系統(tǒng) panic,這是一件很危險的事情。
如何判斷通道是否關閉
雖然沒有判斷通道是否關閉的內置函數,但是官方為我們提供了一種語法來判斷通道是否關閉:
v, ok := <-ch // 如果ok為true則代表通道已經關閉
利用這個語法,我們可以編寫這樣的代碼判斷通道是否關閉:
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ā)送數據到已經關閉的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ā)送數據時關閉channel
// panic: send on closed channel
func TestChannelPanic11(t *testing.T) {
var ch = make(chan int)
go func() {
go func() {
// 沒有接收數據的地方,此處會一直阻塞
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ā)送數據和重復關閉通道。
如何避免 panic
在 go 中有一條原則:Channel Closing Principle,它是指不要從接收端關閉 channel,也不要關閉有多個并發(fā)發(fā)送者的 channel。只要我們嚴格遵守這個原則,就可以有效的避免panic。其實這個原則就是讓我們規(guī)避關閉后發(fā)送和重復關閉這兩種情況。
為了應對關閉后發(fā)送數據這種情況,我們很容易想到Channel Closing Principle的第一句:不要從接收端關閉 channel。所以我們應該從發(fā)送端關閉 channel:
func TestSendClose(t *testing.T) {
var (
ch = make(chan int)
wg = sync.WaitGroup{}
// 10毫秒后通知發(fā)送端停止發(fā)送數據
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。那么這種情況下,我們應該如何正確的回收通道呢?這個時候我們可以考慮引入一個額外的通道,當接收端不想再接收數據時,就發(fā)送數據到這個額外的通道中,來通知所有的發(fā)送端退出:
func TestManySendAndOneReceive(t *testing.T) {
var (
sender = 3
wg = sync.WaitGroup{}
numCh = make(chan int)
stopCh = make(chan struct{})
// 10毫秒后通知發(fā)送端停止發(fā)送數據
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("接收到數據", v)
case <-after:
close(stopCh)
wg.Done()
return
}
}
}()
wg.Wait()
}看完這段代碼,我們發(fā)現 numCh 這個通道是沒有關閉語句的,那么這段代碼會引發(fā)內存泄漏嗎?答案是不會,因為我們正確退出了發(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
}總結
如何正確關閉 gotoutine 和 channel 防止內存泄漏是一個重要的課題,如果在編碼過程中,遇到了需要打破Channel Closing Principle原則的情況,一定要思考自己的代碼設計是否合理。
到此這篇關于一文教你Golang如何正確關閉通道 的文章就介紹到這了,更多相關go關閉通道 內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
在Golang中使用http.FileServer返回靜態(tài)文件的操作
這篇文章主要介紹了在Golang中使用http.FileServer返回靜態(tài)文件的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12

