• Go channel的用法
  • 发布于 2个月前
  • 455 热度
    0 评论

前言:

Go channel是Go语言中用于协程间通信的一种特殊类型。通道(channel)是Go语言并发编程的两大基石之一,用于goroutine之间的同步和通信。 通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。


通道有以下几个特点:
1.通道是引用类型,必须使用make函数创建,其容量一旦确定,不会动态增加。
2.当写满时,不可以写;取空时,不可以取。
3.发送操作将持续阻塞直到数据被接收,而接收操作也将持续阻塞直到有数据可接收。
4.通道一次只能接收一个数据元素。
5.通道提供了一种无需共享内存即可进行通信的方式,通过通道进行通信可以避免并发安全问题。


今天的文章我们就channel相关问题展开。

close channel 设计理念

func closechan(c *hchan) {
   // 堆代码 duidaima.com
   // 关闭一个 nil channel, 抛出 panic
   if c == nil {
     panic(plainError("close of nil channel"))
   }
}
为什么关闭一个已经关闭的 channel 会 panic ?官方这样设计的初衷,应该是希望开发者不要依赖于 close 函数,而是要求开发者通过合理设计 goroutine + channel 工作流来提高程序的健壮性。

如何检测 channel 关闭
package main

import (
   "fmt"
   "time"
)

func main() {
   ch := make(chan int)

   go func() {
      if val, ok := <-ch; !ok {
         // channel 已关闭
         fmt.Println("channel closed")
      } else {
         fmt.Printf("val = %d", val)
      }
   }()

   ch <- 1024
   close(ch)

   time.Sleep(time.Second)
}
如何实现健壮的 channel close 方法
关闭一个已经关闭的 channel 会 panic, 实现一个方法,可以让调用方无需考虑边界情况,直接调用即可。下面的代码只是作为技术解决方案探究,没有任何实际意义 (不要应用在你的任何业务代码中)。

1. recover
通过 recover 函数捕获 panic, 可以保证关闭一个已经关闭的 channel 报错不会导致程序终止。
package main
// 堆代码 duidaima.com
import (
 "fmt"
)

func main() {
 defer func() {
  if err := recover(); err != nil {
   fmt.Printf("recover err: %v\n", err)
  }
 }()

 ch := make(chan int)

 close(ch)
 // 关闭一个已经关闭的 channel
 close(ch)
}
2. sync.Once
通过 sync.Once 方法保证 close(channel) 只会被调用一次。
package main

import (
 "sync"
)

type myChan struct {
 ch   chan int
 once sync.Once
}

func (c *myChan) close() {
 c.once.Do(func() {
  close(c.ch)
 })
}

func main() {
 ch := &myChan{
  ch: make(chan int),
 }

 ch.close()
 // 关闭一个已经关闭的 channel
 ch.close()
}
3. atomic.CAS
通过 atomic.CAS 方法保证 close(channel) 只会被调用一次。
package main

import "sync/atomic"

type myChan struct {
 ch     chan int
 closed int32
}

func (c *myChan) close() {
 if atomic.CompareAndSwapInt32(&c.closed, 0, 1) {
  close(c.ch)
 }
}

func main() {
 ch := &myChan{
  ch: make(chan int),
 }
 ch.close()
 // 关闭一个已经关闭的 channel
 ch.close()
}
4. context.Context
通过 context.Context 保证 close(channel) 的操作顺序同步。
package main

import (
 "context"
)

type myChan struct {
 ch     chan int
 ctx    context.Context
 cancel context.CancelFunc
}

func (c *myChan) close() {
 select {
 case <-c.ctx.Done():
  return
 default:
  close(c.ch)
  // 事件同步
  c.cancel()
 }
}

func main() {
 ctx, cancel := context.WithCancel(context.Background())

 ch := &myChan{
  ch:     make(chan int),
  ctx:    ctx,
  cancel: cancel,
 }

 ch.close()
 // 关闭一个已经关闭的 channel
 ch.close()
}

channel 最佳实践
1.channel 类型作为参数时,指定操作类型 (读/写)
2.使用 select + default 处理多个 channel 轮询场景
3.永远不要在读取方向关闭 channel, 只在写入端关闭 channel
4.不要依赖任何应用层实现的 channel 关闭检测方法函数,应该将 channel 的读写操作进行分离 (通过不同的 goroutine),并实现只在一个写入端关闭 channel
5.使用 context.Context 控制 channel 的生命周期
6.充分考虑缓冲和非缓冲 channel 的使用场景
7.无缓冲 channel 提供了阻塞机制,虽然避免了数据竞态,但是当数据较多时降低了性能,而且可能引发死锁

8.缓冲 channel 虽然避免了阻塞,但是有潜在的数据竞态,而且需要考虑缓冲区大小,设计不合理容易浪费资源


channel 和锁如何选择?
当你发现使用锁使程序变得复杂时,可以试试使用 channel 会不会使程序变得简单。

锁的使用场景
1.访问共享数据结构中的缓存信息
2.保存应用程序上下文和状态信息
3.保护某个结构内部状态和完整性
4.高性能要求的临界区代码
channel 的使用场景
1.线程 (goroutine) 通信
2.并发通信
3.异步操作
4.任务分发
5.传递数据所有权
数据逻辑组合 (如 Pipeline, FanIn FanOut 等并发模式)
官方给出的建议是除了特殊的、底层的应用程序外,其他情况最好使用 channel 或其他同步原语来完成 (但是从大多数开源组件实现代码来看,并没有遵守官方的建议)。
用户评论