网站建设分析,泽库县wap网站建设公司,wordpress商品按钮代码,临沂网站建设优化目录 Golang 并发 Channel的用法1. channel 的创建2. nil channel读写阻塞示例close示例 3. channel 的读写4. channel 只读只写5. 关闭channelchannel关闭后#xff0c;剩余的数据能否取到读取关闭的channel#xff0c;将获取零值使用ok判断#xff0c;是否关闭使用for-ran… 目录 Golang 并发 Channel的用法1. channel 的创建2. nil channel读写阻塞示例close示例 3. channel 的读写4. channel 只读只写5. 关闭channelchannel关闭后剩余的数据能否取到读取关闭的channel将获取零值使用ok判断是否关闭使用for-range退出使用close(ch)关闭所有下游协程 6. 当函数传递channel时是传递的引用还是值参考 Golang 并发 Channel的用法
1. channel 的创建
ch : make(chan int)上面是创建了无缓冲的 channel一旦有 goroutine 往 channel 发送数据那么当前的 goroutine 会被阻塞住直到有其他的 goroutine 消费了 channel 里的数据才能继续运行。
ch : make(chan int, 2)上面示例中的第二个参数表示 channel 可缓冲数据的容量。只要当前 channel 里的元素总数不大于这个可缓冲容量则当前的 goroutine 就不会被阻塞住。
2. 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 : 0t : time.NewTimer(time.Second * 5)for {select {case input : -c:sum sum inputfmt.Println(addIntegers , input : strconv.Itoa(input) , sum : strconv.Itoa(sum))case -t.C:c nilfmt.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 : -cfmt.Println(main goroutine , read num : strconv.Itoa(num))c nilfmt.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
3. channel 的读写
写操作
ch : make(chan int)
ch - 1读操作
data - ch当我们不再使用 channel 的时候可以对其进行关闭 close(ch)如果 channel 关闭继续读取关闭后的 channel不会产生 pannic还是可以读到数据将得到零值即对应类型的默认值。
为了能知道当前 channel 是否被关闭可以使用下面的写法来判断。
当channel关闭时ok false当channel未关闭时ok true if v, ok : -ch; !ok {fmt.Println(channel 已关闭读取不到数据)}另一种写法可以使用 for-range 使用下面的写法不断的获取 channel 里的数据直到channel关闭跳出循环执行后面的代码。 for data : range ch {// get data dosomething}4. 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 : -chif !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 0for _ range exitChan {totalif 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)5. 关闭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 - 1ch - 2ch - 3close(ch)go func() {for {value : -ch//此处为假设判断value永远不会等于10if 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关闭时okfalse当channel未关闭时oktrue
通过判断channel是否关闭当channel关闭时程序可以正常退出代码示例如下
func TestCloseDemo02(t *testing.T) {done : make(chan struct{})ch : make(chan int, 3)ch - 1ch - 2ch - 3close(ch)go func() {for {value, ok : -chif !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 - 1ch - 2ch - 3close(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(workerstrconv.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 exitfor {select {case -stopCh:fmt.Println(workerName, goroutine , Recv stop signal , return)returndefault: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)
PASS6. 当函数传递channel时是传递的引用还是值
golang 传递给函数chan类型时是值传递和引用传递
golang默认都是采用值传递即拷贝传递有些值天生就是指针slice、map、channel
可以看出来map和slice都是指针传递即函数内部是可以改变参数的值的。而array是数组传递不管函数内部如何改变参数都是改变的拷贝值并未对原值进行处理。
在 Go 语言中所有的函数参数传递都是值传递pass by value当将参数传递给函数时实际上是将参数的副本传递给函数。然而这并不意味着在函数内部对参数的修改都不会影响原始数据。因为在 Go 中有些数据类型本身就是引用类型比如切片slice、映射map、通道channel、接口interface和指针pointer。当这些类型作为参数传递给函数时虽然传递的是值但值本身就是一个引用。
小结 Go 语言中的参数传递总是值传递意味着传递的总是变量的副本无论是基本数据类型还是复合数据类型。由于复合数据类型如切片、映射、通道、接口和指针内部包含的是对数据的引用所以在函数内部对这些参数的修改可能会影响到原始数据。理解这一点对于编写正确和高效的Go代码至关重要。
另外即使是引用类型比如切片当长度或容量比如使用 append 函数发生变化了可能会导致分配新的底层数组。这种情况下原始切片不会指向新的数组但是函数内部的切片会。因此如果想在函数内部修改切片的长度或容量并反映到外部应该传递一个指向切片的指针。
参考
https://www.cnblogs.com/-wenli/p/12350181.htmlhttps://segmentfault.com/a/1190000017958702https://zhuanlan.zhihu.com/p/395278270https://zhuanlan.zhihu.com/p/613771870Go里面如何实现广播 https://juejin.cn/post/6844903857395335182