一文教你Golang如何正确关闭通道

 更新时间:2023年10月31日 10:43:16   作者:灯火消逝的码头  
Go在通道这一块,没有内置函数判断通道是否已经关闭,也没有可以直接获取当前通道数量的方法,因此如果对通道进行了错误的使用,将会直接引发系统 panic,这是一件很危险的事情,下面我们就来学习一下如何正确关闭通道吧

序言

Go 在通道这一块,没有内置函数判断通道是否已经关闭,也没有可以直接获取当前通道数量的方法。所以对于通道,Go 显示的不是那么优雅。另外,如果对通道进行了错误的使用,将会直接引发系统 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 循环:

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

有三种情况会引发 panic:

// 会引发channel panic的情况一:发送数据到已经关闭的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)
}

// 会引发channel panic的情况一的另外一种:发送数据时关闭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)
}

// 会引发channel panic的情况二:重复关闭channel
// panic: close of closed channel
func TestChannelPanic2(t *testing.T) {
	var ch = make(chan int)
	close(ch)
	close(ch)
}

// 会引发channel panic的情况三:未初始化关闭
// panic: close of nil channel
func TestChannelPanic3(t *testing.T) {
	var ch chan int
	close(ch)
}

我们在实际的业务中应该避免这三种不同的 panic,未初始化就关闭的情况较为少见,也不容易犯错误,重要的是要防止关闭后发送数据和重复关闭通道。

如何避免 panic

在 go 中有一条原则:Channel Closing Principle,它是指不要从接收端关闭 channel,也不要关闭有多个并发发送者的 channel。只要我们严格遵守这个原则,就可以有效的避免panic。其实这个原则就是让我们规避关闭后发送重复关闭这两种情况。

为了应对关闭后发送数据这种情况,我们很容易想到Channel Closing Principle的第一句:不要从接收端关闭 channel。所以我们应该从发送端关闭 channel:

func TestSendClose(t *testing.T) {
	var (
		ch = make(chan int)
		wg = sync.WaitGroup{}
		// 10毫秒后通知发送端停止发送数据
		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()
}

这种方式可以应对单发送者的情况,如果我们的程序有多个发送者,那么就要考虑Channel Closing Principle的第二句话:不要关闭有多个并发发送者的 channel。那么这种情况下,我们应该如何正确的回收通道呢?这个时候我们可以考虑引入一个额外的通道,当接收端不想再接收数据时,就发送数据到这个额外的通道中,来通知所有的发送端退出:

func TestManySendAndOneReceive(t *testing.T) {
	var (
		sender = 3
		wg     = sync.WaitGroup{}
		numCh  = make(chan int)
		stopCh = make(chan struct{})
		// 10毫秒后通知发送端停止发送数据
		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("发送成功", value)
				}
			}
		}()
	}

	// receive
	go func() {
		for {
			select {
			case v := <-numCh:
				fmt.Println("接收到数据", v)
			case <-after:
				close(stopCh)
				wg.Done()
				return
			}
		}
	}()

	wg.Wait()
}

看完这段代码,我们发现 numCh 这个通道是没有关闭语句的,那么这段代码会引发内存泄漏吗?答案是不会,因为我们正确退出了发送端和接收端的所有协程,等到这个通道没有任何代码使用后,Go 的垃圾回收会回收此通道。

那如果此时我们的程序变得更为复杂:有多个接收者和多个发送者,这个时候怎么办呢?我们可以引入另外一个中间者,当任意协程想关闭的时候,都通知这个中间者,所有协程也同时监听这个中间者,收到中间者的退出信号时,退出当前协程:

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关闭通道 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • jenkins配置golang 代码工程自动发布的实现方法

    jenkins配置golang 代码工程自动发布的实现方法

    这篇文章主要介绍了jenkins配置golang 代码工程自动发布,jks是个很好的工具,使用方法也很多,我只用了它简单的功能,对jenkins配置golang相关知识感兴趣的朋友一起看看吧
    2022-07-07
  • Golang 并发以及通道的使用方式

    Golang 并发以及通道的使用方式

    这篇文章主要介绍了Golang 并发以及通道的使用方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-03-03
  • 基于golang channel实现的轻量级异步任务分发器示例代码

    基于golang channel实现的轻量级异步任务分发器示例代码

    这篇文章主要给大家介绍了关于基于golang channel实现的轻量级异步任务分发器的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-07-07
  • golang jwt+token验证的实现

    golang jwt+token验证的实现

    这篇文章主要介绍了golang jwt+token验证的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-10-10
  • goland 设置project gopath的操作

    goland 设置project gopath的操作

    这篇文章主要介绍了goland 设置project gopath的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-05-05
  • go语言实现同步操作项目示例

    go语言实现同步操作项目示例

    本文主要介绍了go语言实现同步操作项目示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • golang利用不到20行代码实现路由调度详解

    golang利用不到20行代码实现路由调度详解

    这篇文章主要给大家介绍了关于golang利用不到20行代码实现路由调度的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-08-08
  • Go利用反射reflect实现获取接口变量信息

    Go利用反射reflect实现获取接口变量信息

    反射是通过实体对象获取反射对象(Value、Type),然后可以操作相应的方法。本文将利用Go语言中的反射reflect实现获取接口变量信息,需要的可以参考一下
    2022-05-05
  • Go语言时间相关操作合集(超详细)

    Go语言时间相关操作合集(超详细)

    在开发应用程序的过程中,经常需要记录某些操作的时间或者格式化时间戳,因此大部分编程语言都会有操作时间的库,Go语言当然也不例外,本文我们就一起来学习一下time包的使用
    2023-08-08
  • Go语言LeetCode题解961在长度2N的数组中找出重复N次元素

    Go语言LeetCode题解961在长度2N的数组中找出重复N次元素

    这篇文章主要为大家介绍了Go语言LeetCode题解961在长度2N的数组中找出重复N次元素示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12

最新评论