【golang】使用通道时需要注意的一些问题

这篇具有很好参考价值的文章主要介绍了【golang】使用通道时需要注意的一些问题。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

环境

Go 1.20
Windows 11

常识

1.定义通道变量:

ch := make(chan int) // 可存放int类型数据,缓冲为0
ch := make(chan any) // 可存放任意类型数据,缓冲为0
ch := make(chan int, 5) // 存放int类型数据,缓冲为5

// 默认的通道是既可以写入又可以读取的,但我们也可以限制通道的方向
ch := make(<-chan int) // 只能从此通道读取数据,且不能关闭此通道
ch := make(chan<- int) // 只能写入数据到此通道

length := len(ch) // 通道里有多少个数据
capacity := cap(ch) // 通道的缓冲区大小

2.通道遵循FIFO先入先出规则,可以保证元素的顺序

3.通道是并发安全的,不会因多个协程的同时写入而发生数据错乱

注意点

下面的代码例子会经常出现调用display函数,这是我自己定义的一个函数,主要用于打印信息,代码如下:

func display(msg ...any) {
	fmt.Print(time.Now().Format(time.DateTime), " ")
	fmt.Println(msg...)
}

为了减少代码冗余,下面的代码例子就不再贴出此函数的代码了。

1、对一个没有关闭的通道进行读写时,如果遇上了阻塞,并且此时已经没有其它活跃(非阻塞)的协程在运行了,会报deadlock错误!

怎么理解这句话呢,首先要了解读写通道时什么情况下会阻塞:

  1. 往缓冲已满的通道写入数据时会阻塞
  2. 读取空的通道会阻塞
  3. 通道未初始化,例如var ch chan int就是未初始化的

针对第1点,假设通道缓冲是N,那么在第 N + 1 次写入时会阻塞(定义通道变量时如果不指定N的大小,则N默认等于0)

针对第2点,如果这个空的通道是已关闭的,则不会阻塞,读取到的是这个通道数据类型的零值

例子1:

func main() {
	ch := make(chan int)
	// 协程1
	go func() {
		for i := 0; i < 3; i++ {
			display("准备发送:", i)
			ch <- i
			display("已发送完毕:", i)
		}
	}()

	for data := range ch {
		display("获得数据:", data)
	}
}

上面代码运行后会报错:fatal error: all goroutines are asleep - deadlock!

原因是,当【协程1】往通道写入3个数据后,【协程1】就结束运行了,这时【main协程】(是的,main函数也是运行在协程里的)读取出这3个数据后,并没有退出for-range循环,而是继续读取已空的ch通道,发生了阻塞,但这时只有【main协程】在运行了,只剩下一个协程,所以报错。

例子1修改一下:

func main() {
	ch := make(chan int)
	// 协程1
	go func() {
		for i := 0; i < 3; i++ {
			display("准备发送:", i)
			ch <- i
			display("已发送完毕:", i)
		}
	}()
    // 协程2
	go func() {
		for data := range ch {
			display("获得数据:", data)
		}
	}()

    // 死循环
	for {
	}
}

经修改后代码不会再报错了,原因是,【协程1】退出后,虽然【协程2】还在阻塞式地读取空通道,但这时除了【协程2】以外,还有一个活跃的【main协程】在运行,所以不会报错。

例子1再修改下:

func main() {
	ch := make(chan int)
	// 协程1
	go func() {
		for i := 0; i < 3; i++ {
			display("准备发送:", i)
			ch <- i
			display("已发送完毕:", i)
		}
		close(ch) // 新添加代码
	}()

	for data := range ch {
		display("获得数据:", data)
	}
}

协程1在写入完所有数据后,使用close(ch)关闭了通道,这时也不会再报错了。原因是,对于已关闭的通道,for-range循环读取完通道的数据后,会自动结束循环,不会阻塞在读取通道处,所以不会报错。

2、给一个已关闭的通道发送数据,或者再次关闭一个已关闭的通道,会导致panic

这句话告诉我们,当发送方不再需要发送数据时,可以关闭通道,但不能让接收方去关闭。
因为接收方并不知道发送方是否还需要发送数据,如果胡乱关闭了通道,会导致发送方触发panic

3、已关闭的通道是可以继续读取里面的数据的
func main() {
	ch := make(chan int, 2)
	ch <- 123
	ch <- 456
	close(ch)

	// 使用for-range读取已关闭通道,通道空了之后会自动跳出循环
	for data := range ch {
		display(data)
	}

	// 方式2:使用ok变量判断通道是否已空
	/*for {
		data, ok := <-ch
		if !ok {
			break
		}
		display(data)
	}*/

    // 方式3:通过通道长度来判断通道是否已空
	/*num := len(ch)
	for i := 0; i < num; i++ {
		data := <-ch
		display(data)
	}*/
}
4、双向通道可以传递给参数为单向通道的函数
// 函数参数是单向通道
func sendMessage(in chan<- int) {
	for i := 0; i < 3; i++ {
		in <- i
	}
	close(in)
}

func main() {
	ch := make(chan int) // 双向通道
	go sendMessage(ch)

	for data := range ch {
		display(data)
	}
}
5、当读取通道与select搭配使用,并且设置了超时时间时,通道一定要设置缓冲

先看例子:

func sendMessage(in chan<- int, sleep time.Duration) {
	time.Sleep(sleep)
	in <- 1
}

func main() {
	display("开始")
	display("协程数量:", runtime.NumGoroutine())

	ch1 := make(chan int) // 错误
	// 正确:ch1 := make(chan int, 1)

    // 协程1
	go sendMessage(ch1, 5 * time.Second)

	select {
	case v := <-ch1:
		display("从通道1获取到了数据:", v)
	case <-time.After(1 * time.Second):
		display("超时了,退出select")
	}

	for {
		display("协程数量:", runtime.NumGoroutine())
		time.Sleep(1 * time.Second)
	}
}

如上面代码所示,一开始我们创建了一个无缓冲的通道ch1,然后开启【协程1】,【协程1】在 5 秒后会往通道写入一个数据,但select的超时时间只设置了 1 秒。也就是说,在【协程1】往通道写入数据前,select语句就已经因为超时而结束了,此时的ch1通道已经没有接收方,只剩下发送方了。往一个无缓冲的通道写入数据会导致【协程1】阻塞,而且没有了接收方,【协程1】就会永远阻塞下去,无法结束退出,从而导致协程泄露

观察超时后打印出来的协程数量,一直都是2,不会降低为1,也证实了上面的说法。所以在定义通道变量时,一定要设置缓冲区。

其实调高 select的超时时间,也能解决这个问题。但有时候我们可能无法得知协程具体的执行耗时,从而预估出一个合理的超时时间,所以稳妥起见,还是定义一个带缓冲的通道比较好。文章来源地址https://www.toymoban.com/news/detail-518038.html

到了这里,关于【golang】使用通道时需要注意的一些问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 使用vscode写golang的一些大坑(单元测试、goimports、接口实现)

    之前使用的是goland,定位代码、代码补全、代码测试、git版本管理一应俱全,使用方便,但是奈何内存占用太大,平时使用的的项目又比较多,所以决定转战vscode。 在使用vscode开发的过程,目前碰到了三个问题: 查看源码时,无法根据接口定义查找到所有的实现。 goland的

    2024年02月06日
    浏览(28)
  • Golang协程,通道详解

    进程 (Process)就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位,进程是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间。 一个进程至少有 5 种基本状态,它们是:初始态,执行态,等待状

    2024年02月12日
    浏览(20)
  • Go新项目-Golang使用RemoteAddr()获取远程主机地址的注意事项(9)

    http.Request 下的方法RemoteAddr() 可以获取客户端的地址和端口号,最近的一个项目中用到了这个方法。 使用过程中一直都没有什么问题,但是当项目上线之后就发现不管怎么获取ip,客户端地址都是127.0.0.1 。 对于这个问题一直百思不得其解,最后搞了半天才发现是nginx 的原因。

    2024年01月20日
    浏览(43)
  • golang IDE 使用 go-1.7 无法识别 goroot问题

    当前使用了 golang IDE 要设定 go-1.17 版本作为默认 GOROOT 系统环境变量已经定义好 打开了 ide 会出现下面问题,选择 1.17 后会出现下面报错 The selected directory is not a valid horne for GO SDK 修改 $GOROOT 下文件增加一个变量 再次在 IDC 选择 GOROOT 就可以找到 go 1.17.2 版本 选择后,需要关闭

    2024年02月16日
    浏览(39)
  • SQL中使用DATE_FORMATE格式转换需要注意的问题

    【参考格式】 %Y:4位的年份 %y:2位的年份 %m:2位的月份 %d:2位的日期 %H:24小时制的小时 %h:12小时制的小时 %i:2位的分钟 %s:2位的秒钟 %W:星期的全名 %w:星期的缩写 %M:月份的全名 %b:月份的缩写 参考链接

    2024年02月07日
    浏览(30)
  • 【Golang】三分钟让你快速了解Go语言&为什么我们需要Go语言?

    博主简介: 努力学习的大一在校计算机专业学生,热爱学习和创作。目前在学习和分享:数据结构、Go,Java等相关知识。 博主主页: @是瑶瑶子啦 所属专栏: Go语言核心编程 近期目标: 写好专栏的每一篇文章 Go 语言从 2009 年 9 月 21 日开始作为谷歌公司 20% 兼职项目,即相关

    2023年04月21日
    浏览(46)
  • golang文件相对路径问题

    目录结构 2.具体代码:

    2024年01月17日
    浏览(37)
  • golang文件内容覆盖问题

    通过golang读取数据库站点映射配置,生成nginx conf文件,并检查和重启nginx服务,已达到站点自动化部署目的。 当目标文件中内容很长,而写入的内容很短时,目标文件内容无法完全覆盖。 此分析未必正确,日后查到确切原因,再来补充。 os.OpenFile(fileName, os.O_CREATE|os.O_RDWR,

    2024年02月01日
    浏览(28)
  • golang编译问题

    编译时提示工作目录文件系统只读 go: creating work dir: mkdir /tmp/go-build2069696743: read-only file system 方法一: 尝试了很多办法,重新挂载可读可写,还是不行,只能重启 方法二: 重新设置GOTMPDIR 编译时拉不下来指定依赖包 设置国内源 Go 1.13 及以上(推荐) 如果是低版本golang参考

    2024年02月06日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包