Golang 并发 Channel的用法

这篇具有很好参考价值的文章主要介绍了Golang 并发 Channel的用法。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Golang 并发 Channel的用法

channel 的创建

ch := make(chan int)

上面是创建了无缓冲的 channel,一旦有 goroutine 往 channel 发送数据,那么当前的 goroutine 会被阻塞住,直到有其他的 goroutine 消费了 channel 里的数据,才能继续运行。

ch := make(chan int, 2)

上面示例中的第二个参数表示 channel 可缓冲数据的容量。只要当前 channel 里的元素总数不大于这个可缓冲容量,则当前的 goroutine 就不会被阻塞住。

nil channel

nil是pointers, interfaces, maps, slices, channels 和 function 类型的零值,表示未初始化值。nil不是未定义状态,它本身就是值。error是接口类型,因此error变量可以为nil,但string不能为nil。

下面我们看下nil 通道有什么特点,空通道对操作的反应如下:

  • 从空通道读、写会永远阻塞
  • 关闭通道会终止程序(panic)

空通道是一种特殊通道,总是阻塞。对比非空已关闭的通道仍然可以进行读取,并能够读取对应类型的零值,但对于已关闭的通道发送信息会终止程序。

一般 nil channel 用在 select 上,让 select 不再从这个 channel 里读取数据

读写阻塞示例

示例如下:

func TestNil(t *testing.T) {
	c := make(chan int)

	go sendIntegers(c)
	addIntegers(c)
}

func addIntegers(c chan int) {
	sum := 0
	t := time.NewTimer(time.Second * 5)
	for {
		select {
		case input := <-c:
			sum = sum + input
			fmt.Println("addIntegers , input : " + strconv.Itoa(input) + " , sum : " + strconv.Itoa(sum))
		case <-t.C:
			c = nil
			fmt.Println("addIntegers , nil channel , sum : " + strconv.Itoa(sum))
		}
	}
}

func sendIntegers(c chan int) {
	for {
		time.Sleep(time.Second * 1)
		c <- rand.Intn(100)
	}
}

输出如下

=== RUN   TestNil
addIntegers , input : 81 , sum : 81
addIntegers , input : 87 , sum : 168
addIntegers , input : 47 , sum : 215
addIntegers , input : 59 , sum : 274
addIntegers , nil channel , sum : 274
panic: test timed out after 30s

此示例会一直阻塞下去,addIntegers是程序的主协程会一直阻塞下去,sendIntegers是子协程同样会一直阻塞下去。

其中:输出中的panic是单元测试的Test引发的异常,不需要考虑在内。

close示例

func TestCloseNil(t *testing.T) {
	c := make(chan int)
	go writeChannel(c)
	num := <-c
	fmt.Println("main goroutine , read num : " + strconv.Itoa(num))
	c = nil
	fmt.Println("main goroutine , to close channel .")
	close(c)
	time.Sleep(time.Second * 10)

}

func writeChannel(c chan int) {
	fmt.Println("writeChannel goroutine ,  running ...")
	c <- 1
}

输出如下

=== RUN   TestCloseNil
writeChannel goroutine ,  running ...
main goroutine , read num : 1
main goroutine , to close channel .
--- FAIL: TestCloseNil (0.00s)
panic: close of nil channel [recovered]
	panic: close of nil channel

关闭nil通道会引起程序panic

channel 的读写

写操作

ch := make(chan int)
ch <- 1

读操作

data <- ch

当我们不再使用 channel 的时候,可以对其进行关闭:

 close(ch)

不过读取关闭后的 channel,不会产生 pannic,还是可以读到数据。

如果关闭后的 channel 没有数据可读取时,将得到零值,即对应类型的默认值。

为了能知道当前 channel 是否被关闭,可以使用下面的写法来判断。

 if v, ok := <-ch; !ok {
  fmt.Println("channel 已关闭,读取不到数据")
 }

还可以使用下面的写法不断的获取 channel 里的数据:

 for data := range ch {
  // get data dosomething
 }

这种用法会在读取完 channel 里的数据后就结束 for 循环,执行后面的代码。

channel 只读只写

在默认情况下,管道是双向的,可读可写,在使用 channel 时我们还可以控制 channel 只读只写操作:

声明为只写,如下:

var chan2 chan<- int
chan2 = make(chan int, 3)
chan2 <- 20

如果试着读此chan,则编译报错,编译错误如下:

invalid operation: cannot receive from send-only channel chan2 (variable of type chan<- int) compiler (InvalidReceive)

声明为只读,不可写,否则编译报错,如下:

var chan3 <-chan int
nm2 := <-chan3

函数可以声明chan只读只写,代码示例:

// 只写操作
func send(ch chan<- int, exitChan chan struct{}) {
	for i := 0; i < 5; i++ {
		time.Sleep(time.Second * 1)
		ch <- i
	}
	close(ch)
	var a struct{}
	exitChan <- a
}

// 只读操作
func recv(ch <-chan int, exitChan chan struct{}) {
	for {
		v, ok := <-ch
		if !ok {
			break
		}
		fmt.Println("recv goroutine , value : " + strconv.Itoa(v))
	}
	var a struct{}
	exitChan <- a
}
func TestOnlyReadWrite(t *testing.T) {
	ch := make(chan int, 10)
	exitChan := make(chan struct{}, 2)
	go send(ch, exitChan)
	go recv(ch, exitChan)
	var total = 0
	for _ = range exitChan {
		total++
		if total == 2 {
			break
		}
	}
	fmt.Println("main goroutine , 结束")
}

输出如下:

=== RUN   TestOnlyReadWrite
recv goroutine , value : 0
recv goroutine , value : 1
recv goroutine , value : 2
recv goroutine , value : 3
recv goroutine , value : 4
main goroutine , 结束
--- PASS: TestOnlyReadWrite (5.03s)

关闭channel

channel关闭后,剩余的数据能否取到

golang channel关闭后,其中剩余的数据,是可以继续读取的,channel关闭之后,仍然可以从channel中读取剩余的数据,直到数据全部读取完成。

对于关闭的channel的读写需要注意两点:

  • 如果继续向channel发送数据,会引起panic,
  • 如果继续读数据,得到的是零值(对于int,就是0)。

读取关闭的channel,将获取零值

当读取已关闭的channel时,如果继续读取channel,获取到的是零值,不会堵塞,

另外即使是无缓冲的channel,也将能一直获取到零值。

代码示例如下

func TestCloseDemo01(t *testing.T) {

	done := make(chan struct{})
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	ch <- 3
	close(ch)

	go func() {
		for {
			value := <-ch
			//此处为假设判断,value永远不会等于10
			if value == 10 {
				break
			}
			fmt.Println("read channel , value : ", value)
			time.Sleep(time.Second * 1)
		}
		done <- struct{}{}
	}()

	select {
	case <-done:
		fmt.Println("读取channel,正常结束")
	case <-time.After(time.Second * 5):
		fmt.Println("超时退出")
	}
}

输出如下:

=== RUN   TestCloseDemo01
read channel , value :  1
read channel , value :  2
read channel , value :  3
read channel , value :  0
read channel , value :  0
超时退出
--- PASS: TestCloseDemo01 (5.00s)

使用ok判断,是否关闭

读取channel,判断是否关闭:

value, ok := <-ch
  • 当channel关闭时,ok=false
  • 当channel未关闭时,ok=true

通过判断channel是否关闭,当channel关闭时,程序可以正常退出,代码示例如下:

func TestCloseDemo02(t *testing.T) {

	done := make(chan struct{})
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	ch <- 3
	close(ch)

	go func() {
		for {
			value, ok := <-ch
			if !ok {
				break
			}
			fmt.Println("read channel , value : ", value)
			time.Sleep(time.Second * 1)
		}
		done <- struct{}{}
	}()

	select {
	case <-done:
		fmt.Println("读取channel,正常结束")
	case <-time.After(time.Second * 5):
		fmt.Println("超时退出")
	}
}

输出如下:

=== RUN   TestCloseDemo02
read channel , value :  1
read channel , value :  2
read channel , value :  3
读取channel,正常结束
--- PASS: TestCloseDemo02 (3.03s)
PASS

使用for-range退出

for-range是使用频率很高的结构,常用它来遍历数据,range能够感知channel的关闭,当channel被发送数据的协程关闭时,range就会结束,接着退出for循环。

它在并发中的使用场景是:当协程只从1个channel读取数据,然后进行处理,处理后协程退出。

下面这个示例程序,当通道被关闭时,协程可自动退出。

func TestCloseDemo02(t *testing.T) {
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	ch <- 3
	close(ch)
	for v := range ch {
		fmt.Println("value", v)
	}
	time.Sleep(time.Second * 10)
}

使用close(ch)关闭所有下游协程

  • 关闭通道,可以主动通知所有协程退出的场景

当启动100个worker时,只要main()执行关闭stopCh,每一个worker都会都到信号,进而关闭。如果main()向stopCh发送100个数据,这种就低效了。

//close关闭所有子协程
func TestCloseDemo04(t *testing.T) {

	ch := make(chan int, 3)
	stopCh := make(chan struct{})

	for i := 1; i < 6; i++ {
		worker("worker"+strconv.Itoa(i), stopCh, ch)
	}

	time.Sleep(time.Second * 5)
	close(stopCh)

	time.Sleep(time.Second * 5)
}


func worker(workerName string, stopCh <-chan struct{}, ch <-chan int) {
	go func() {
		defer fmt.Println(workerName, "goroutine , worker exit")
		// Using stop channel explicit exit
		for {
			select {
			case <-stopCh:
				fmt.Println(workerName, "goroutine , Recv stop signal , return")
				return
			default:
				fmt.Println(workerName, "goroutine , worker default ...")
			}
			time.Sleep(time.Second * 3)
		}
	}()
}

输出如下

=== RUN   TestCloseDemo04
worker5 goroutine , worker default ...
worker3 goroutine , worker default ...
worker4 goroutine , worker default ...
worker1 goroutine , worker default ...
worker2 goroutine , worker default ...
worker3 goroutine , worker default ...
worker2 goroutine , worker default ...
worker5 goroutine , worker default ...
worker4 goroutine , worker default ...
worker1 goroutine , worker default ...
worker4 goroutine , Recv stop signal , return
worker4 goroutine , worker exit
worker2 goroutine , Recv stop signal , return
worker2 goroutine , worker exit
worker5 goroutine , Recv stop signal , return
worker5 goroutine , worker exit
worker1 goroutine , Recv stop signal , return
worker1 goroutine , worker exit
worker3 goroutine , Recv stop signal , return
worker3 goroutine , worker exit
--- PASS: TestCloseDemo04 (10.01s)
PASS

函数传递引用or值

golang 传递给函数chan类型时,是值传递和引用传递?

  • golang默认都是采用值传递,即拷贝传递
  • 有些值天生就是指针(slice、map、channel)

可以看出来map和slice都是指针传递,即函数内部是可以改变参数的值的。而array是数组传递,不管函数内部如何改变参数,都是改变的拷贝值,并未对原值进行处理。

在 Go 语言中,所有的函数参数传递都是值传递(pass by value),当将参数传递给函数时,实际上是将参数的副本传递给函数。然而,这并不意味着在函数内部对参数的修改都不会影响原始数据。因为在 Go 中,有些数据类型本身就是引用类型,比如切片(slice)、映射(map)、通道(channel)、接口(interface)和指针(pointer)。当这些类型作为参数传递给函数时,虽然传递的是值,但值本身就是一个引用。

小结
Go 语言中的参数传递总是值传递,意味着传递的总是变量的副本,无论是基本数据类型还是复合数据类型。由于复合数据类型(如切片、映射、通道、接口和指针)内部包含的是对数据的引用,所以在函数内部对这些参数的修改可能会影响到原始数据。理解这一点对于编写正确和高效的Go代码至关重要。

另外即使是引用类型,比如切片,当长度或容量(比如使用 append 函数)发生变化了,可能会导致分配新的底层数组。这种情况下,原始切片不会指向新的数组,但是函数内部的切片会。因此,如果想在函数内部修改切片的长度或容量并反映到外部,应该传递一个指向切片的指针。文章来源地址https://www.toymoban.com/news/detail-832661.html

参考

  • https://www.cnblogs.com/-wenli/p/12350181.html
  • https://segmentfault.com/a/1190000017958702
  • https://zhuanlan.zhihu.com/p/395278270
  • https://zhuanlan.zhihu.com/p/613771870
  • Go里面如何实现广播 https://juejin.cn/post/6844903857395335182

到了这里,关于Golang 并发 Channel的用法的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • golang发送get请求的各种操作:自定义header请求头、带cookie请求、channel并发请求

    请求参数放到url.Values{} 接收文件–ioutil.WriteFile 接收文件–io.Copy

    2024年02月06日
    浏览(44)
  • golang实现webgis后端开发

    目录 前言 二、实现步骤 1.postgis数据库和model的绑定 2.将pg库中的要素转换为geojson (1)几何定义 (2)将wkb解析为几何类型 (3)定义geojson类型 (4)数据转换 (5)数据返回  2.前端传入的geojson储存到数据库 3、其他功能实现 总结         停更了接近一个月都在研究一门新语言gola

    2024年02月08日
    浏览(49)
  • golang学习-channel管道

    1、定义 管道是golang语言提供的goroutine间的通讯方式,channel可以让一个goroutine发送特定的值给另一个goroutine的通讯机制。 管道是引用类型。 golang语言中channel是一种特殊的类型。像一个队列一样,先进先出。 var 变量 chan 元素类型 var ch1 chan int //声明一个传递整型的管道 var

    2024年01月19日
    浏览(48)
  • golang channel

    channel是不同协程之间异步通信的数据结构。 1 构造 2 读操作 3 写 4 关闭 5 多路复用 实现对多个channel同时监听 三个核心: 1 并发读写安全,需要锁 2 环形缓冲区(数组+头尾指针),好处是复用数组空间,同时保证内存地址连续 3 承载阻塞goroutine的队列 hchan:channel数据结构

    2024年02月11日
    浏览(39)
  • Golang之Channel详细介绍

    一、概述 通道(Channel)是 Golang 在语言级别上提供的 goroutine 间的通讯方式,可以使用channel在多个 goroutine 之间传递消息。如果说 goroutine 是 Go 程序并发的执行体,channel 就是它们之间的连接。channel 是可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。 Golang 的并发

    2024年02月12日
    浏览(35)
  • golang channel执行原理与代码分析

    使用的go版本为 go1.21.2 首先我们写一个简单的chan调度代码 因为ch的数据获取方式有两种,所以这个示例代码写了两次的ch读与写 老样子通过go build -gcflags -S main.go获取到对应的汇编代码 调度make最终被转换为CALL runtime.makechan 调度ch - struct{}{}最终被转换为CALL runtime.chansend1 由于我

    2024年02月05日
    浏览(34)
  • 001 Golang-channel-practice

    最近在练习并发编程。加上最近也在用Golang写代码,所以记录一下练习的题目。   第一道题目是用10个协程打印100条信息,创建10个协程。每个协程都会有自己的编号。每个协程都会被打印10次。   本题的关键是: 创建goroutines :通过 for i := 0; i 10; i++ 循环,创建了10个gorout

    2024年02月02日
    浏览(44)
  • 详解如何在Golang中监听多个channel

    这篇文章主要为大家详细介绍了如何在Golang中实现监听多个channel,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下    我们可以使用select来同时监听多个goroutine。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package main import (   

    2024年02月13日
    浏览(85)
  • 004 Golang-channel-practice 左右括号匹配

    第四题 左右括号打印 一个协程负责打印“(”,一个协程负责打印“)”,左右括号的数量要匹配。在这道题目里,我在main函数里进行了一个死循环。会产生一个随机数,随机数就是接下来要打印的左括号的数量。 例如:((((()))))、(())、()。这样是正确的。一个左括号要匹

    2024年02月02日
    浏览(41)
  • 007 Golang-channel-practice 打印水分子

    对应leetcode 1117 https://leetcode.cn/problems/building-h2o/description/ 题目大意:在三个为一组的字符串中,打印两个H,一个O,顺序不限。 这在go里面很容易实现。只需要在每个函数前加上一个go,就可以轻松实现并发了。直接看代码! 打印效果:  

    2024年01月20日
    浏览(38)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包