channel的基本操作和注意事项


channel存在3种状态

  • nil,未初始化的状态,只进行了声明,或者手动赋值为nil
  • active,正常的channel,可读或者可写
  • closed,已关闭,千万不要误认为关闭channel后,channel的值是nil


    channel可进行3种操作

  • 关闭

把这3种操作和3种channel状态可以组合出9种情况:

操作 nil的channel 正常channel 已关闭channel
<- ch 阻塞 成功或阻塞 读到零值
ch <- 阻塞 成功或阻塞 panic
close(ch) panic 成功 panic

对于nil通道的情况,也并非完全遵循上表,有1个特殊场景:当nil的通道在select的某个case中时,这个case会阻塞,但不会造成死锁。

参考代码请看:https://dave.cheney.net/2014/03/19/channel-axioms

使用channel的10种常用操作

1. 使用for range读channel

  • 场景:当需要不断从channel读取数据时
  • 原理:使用 for-range 读取channel,既安全又便利,当channel关闭时,for循环会自动退出,无需主动监测channel是否关闭,可以防止读取已经关闭的channel,造成读到数据为通道所存储的数据类型的零值。
  • 用法:

    1. for x := range ch{
    2. fmt.Println(x)
    3. }

    2. 使用_,ok判断channel是否关闭

  • 场景:读channel,但不确定channel是否关闭时

  • 原理:读已关闭的channel会得到零值,如果不确定channel,需要使用ok进行检测。ok的结果和含义:
    • true:读到数据,并且通道没有关闭。
    • false:通道关闭,无数据读到。
  • 用法:

    1. if v, ok := <- ch; ok {
    2. fmt.Println(v)
    3. }

    3. 使用select处理多个channel

  • 场景:需要对多个通道进行同时处理,但只处理最先发生的channel时

  • 原理:select可以同时监控多个通道的情况,只处理未阻塞的case。当通道为nil时,对应的case永远为阻塞,无论读写。特殊关注:普通情况下,对nil的通道写操作是要panic的。
  • 用法:

    1. // 分配job时,如果收到关闭的通知则退出,不分配job
    2. func (h *Handler) handle(job *Job) {
    3. select {
    4. case h.jobCh<-job:
    5. return
    6. case <-h.stopCh:
    7. return
    8. }
    9. }

    4. 使用channel的声明控制读写权限

  • 场景:协程对某个通道只读或只写时

  • 目的:A. 使代码更易读、更易维护,B. 防止只读协程对通道进行写数据,但通道已关闭,造成panic。
  • 用法:
    • 如果协程对某个channel只有写操作,则这个channel声明为只写。
    • 如果协程对某个channel只有读操作,则这个channe声明为只读。 ```go // 只有generator进行对outCh进行写操作,返回声明 // <-chan int,可以防止其他协程乱用此通道,造成隐藏bug func generator(int n) <-chan int { outCh := make(chan int) go func(){ for i:=0;i<n;i++{
      1. outCh<-i
      } }() return outCh }

// consumer只读inCh的数据,声明为<-chan int // 可以防止它向inCh写数据 func consumer(inCh <-chan int) { for x := range inCh { fmt.Println(x) } }

  1. <a name="zrw7s"></a>
  2. ## 5. 使用缓冲channel增强并发
  3. - 场景:并发
  4. - 原理:有缓冲通道可供多个协程同时处理,在一定程度可提高并发性。
  5. - 用法:
  6. ```go
  7. // 无缓冲
  8. ch1 := make(chan int)
  9. ch2 := make(chan int, 0)
  10. // 有缓冲
  11. ch3 := make(chan int, 1)
  1. func test() {
  2. inCh := generator(100)
  3. outCh := make(chan int, 10)
  4. // 使用5个`do`协程同时处理输入数据
  5. var wg sync.WaitGroup
  6. wg.Add(5)
  7. for i := 0; i < 5; i++ {
  8. go do(inCh, outCh, &wg)
  9. }
  10. go func() {
  11. wg.Wait()
  12. close(outCh)
  13. }()
  14. for r := range outCh {
  15. fmt.Println(r)
  16. }
  17. }
  18. func generator(n int) <-chan int {
  19. outCh := make(chan int)
  20. go func() {
  21. for i := 0; i < n; i++ {
  22. outCh <- i
  23. }
  24. close(outCh)
  25. }()
  26. return outCh
  27. }
  28. func do(inCh <-chan int, outCh chan<- int, wg *sync.WaitGroup) {
  29. for v := range inCh {
  30. outCh <- v * v
  31. }
  32. wg.Done()
  33. }

6. 为操作加上超时

  • 场景:需要超时控制的操作
  • 原理:使用select和time.After,看操作和定时器哪个先返回,处理先完成的,就达到了超时控制的效果
  • 用法: ```go func doWithTimeOut(timeout time.Duration) (int, error) { select { case ret := <-do():
    1. return ret, nil
    case <-time.After(timeout):
    1. return 0, errors.New("timeout")
    } }

func do() <-chan int { outCh := make(chan int) go func() { // do work }() return outCh }

  1. <a name="Zw182"></a>
  2. ## 7. 使用time实现channel无阻塞读写
  3. - 场景:并不希望在channel的读写上浪费时间
  4. - 原理:是为操作加上超时的扩展,这里的操作是channel的读或写
  5. - 用法:
  6. ```go
  7. func unBlockRead(ch chan int) (x int, err error) {
  8. select {
  9. case x = <-ch:
  10. return x, nil
  11. case <-time.After(time.Microsecond):
  12. return 0, errors.New("read time out")
  13. }
  14. }
  15. func unBlockWrite(ch chan int, x int) (err error) {
  16. select {
  17. case ch <- x:
  18. return nil
  19. case <-time.After(time.Microsecond):
  20. return errors.New("read time out")
  21. }
  22. }

注:time.After等待可以替换为default,则是channel阻塞时,立即返回的效果

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

  • 场景:退出时,显示通知所有协程退出
  • 原理:所有读ch的协程都会收到close(ch)的信号
  • 用法: ```go func (h *Handler) Stop() { close(h.stopCh)

    // 可以使用WaitGroup等待所有协程退出 }

// 收到停止后,不再处理请求 func (h *Handler) loop() error { for { select { case req := <-h.reqCh: go handle(req) case <-h.stopCh: return } } }

  1. <a name="exDXD"></a>
  2. ## 9. 使用chan struct{}作为信号channel
  3. - 场景:使用channel传递信号,而不是传递数据时
  4. - 原理:没数据需要传递时,传递空struct
  5. - 用法:
  6. ```go
  7. // 上例中的Handler.stopCh就是一个例子,stopCh并不需要传递任何数据
  8. // 只是要给所有协程发送退出的信号
  9. type Handler struct {
  10. stopCh chan struct{}
  11. reqCh chan *Request
  12. }

10. 使用channel传递结构体的指针而非结构体

  • 场景:使用channel传递结构体数据时
  • 原理:channel本质上传递的是数据的拷贝,拷贝的数据越小传输效率越高,传递结构体指针,比传递结构体更高效
  • 用法: ```go reqCh chan *Request

// 好过 reqCh chan Request

  1. <a name="V4vb3"></a>
  2. ## 11. 使用channel传递channel
  3. - 场景:使用场景有点多,通常是用来获取结果。
  4. - 原理:channel可以用来传递变量,channel自身也是变量,可以传递自己。
  5. - 用法:下面示例展示了有序展示请求的结果,另一个示例可以见[另外文章的版本3](https://link.segmentfault.com/?url=http%3A%2F%2Flessisbetter.site%2F2019%2F06%2F09%2Fgolang-first-class-function%2F%23%25E7%2589%2588%25E6%259C%25AC3)。
  6. ```go
  7. package main
  8. import (
  9. "fmt"
  10. "math/rand"
  11. "sync"
  12. "time"
  13. )
  14. func main() {
  15. reqs := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
  16. // 存放结果的channel的channel
  17. outs := make(chan chan int, len(reqs))
  18. var wg sync.WaitGroup
  19. wg.Add(len(reqs))
  20. for _, x := range reqs {
  21. o := handle(&wg, x)
  22. outs <- o
  23. }
  24. go func() {
  25. wg.Wait()
  26. close(outs)
  27. }()
  28. // 读取结果,结果有序
  29. for o := range outs {
  30. fmt.Println(<-o)
  31. }
  32. }
  33. // handle 处理请求,耗时随机模拟
  34. func handle(wg *sync.WaitGroup, a int) chan int {
  35. out := make(chan int)
  36. go func() {
  37. time.Sleep(time.Duration(rand.Intn(3)) * time.Second)
  38. out <- a
  39. wg.Done()
  40. }()
  41. return out
  42. }

原文链接:http://lessisbetter.site/2019/01/20/golang-channel-all-usage/