likes
comments
collection
share

go 语言进阶什么是 context context 是协程的上下文,在 go 1.7 版本中引入,用于传递跟踪、取消信

作者站长头像
站长
· 阅读数 26

什么是 context

context 是协程的上下文,在 go 1.7 版本中引入,用于传递跟踪、取消信号和超时信息

context 最佳实践的:

  1. 在函数的签名中传递 context,不推荐在结构体中使用 context
  2. context 作为函数的第一个参数传递
  3. 不要在内层函数创建 context,如果在内层函数中创建 context,会导致 context范围使用不够明确,容易造成 context 误用,造成内存泄露
    • 应该在 fn1 函数中创建 context,然后传递给 fn2 函数,fn2 函数再传递给 fn3 函数
    fn1() -> fn2() -> fn3()
    
  4. 及时取消 context,可以避免资源浪费
    func main() {
      parentCtx := context.Background()
      ctx, cancel := context.WithTimeout(parentCtx, time.Second*3) // 设置 3s 超时
      defer cancel()
      if err := longRunningFunction(ctx, "arg1", 2); err != nil {
        fmt.Println("longRunningFunction error: ", err)   // longRunningFunction error: context deadline exceeded
        cancel()
      }
    }
    func longRunningFunction(ctx context.Context, arg1 string, arg2 int) error {
      select {
      case <-time.After(time.Second * 5): // 模拟耗时操作, 5s
      case <-ctx.Done(): // 超时或者取消
        return ctx.Err()
      }
      return nil
    }
    
  5. 不要在 context 中存储大量数据,只存储一些必要的数据,比如 userIdrequireId 等,不要传递敏感信息
  6. 不要滥用 contextcontext 只在当前请求范围内中使用,不要在全局中使用,如果需要在多个请求共享数据,应该用其它方式

panic 和 recover

panicrecover 使用的细节:

  1. recover 必须在 defer 声明的匿名函数中执行
    • 如果不使用 deferrecover 是在 panic 之前运行的,就无法捕获 panic
  2. recover 与函数调用在同一个协程才能捕获到当前函数的 panic
  3. 当前 goroutine 中的 panic 会被 defer 中的 panic 覆盖
  4. 多个 defer 语句中 panic 的执行顺序
    • 如果没有 recover 语句,会依此输出bapanic: panic main 外加 panic 的堆栈信息
    func fn() {
      defer func() {
        fmt.Println("a")  // 再输出 a
        panic("panic a")
      }()
      defer func() {
        fmt.Println("b")  // 先输出 b
        panic("panic b")
      }()
      panic("panic main") // 最后抛出 panic main,程序结束
    }
    
    • 如果有 recover 语句,会依此输出 barecover: panic a
    func fn() {
      defer func() {
        if err := recover(); err != nil {
          fmt.Println("recover: ", err)  // recover: panic a
        }
      }()
      defer func() {
        fmt.Println("a") // 再输出 a
        panic("panic a")
      }()
      defer func() {
        fmt.Println("b")  // 先输出 b
        panic("panic b")
      }()
      panic("panic main")
    }
    
  5. 多个调用链捕获 panic,会优先被当前协程中的 recover 捕获

channel

channel 的数据结构:

type hchan struct {
  // 当前队列中元素的个数,当我们向 channel 发送数据时,qcount 会加 1,当我们从 channel 接收数据时,qcount 会减 1
  qcount uint
  // 如果我们在创建 channel 时制定了缓冲区的大小,嘛呢 dataqsiz 就等于制定的大小,否则 dataqsiz 为 0,表示该channel 没有缓冲区
  dataqsiz uint
  // buf 字段是一个 unsafe.Pointer 类型的指针,指向缓冲区的起始地址。如果该 channel 没有缓冲区,则 buf 为 nil
  buf unsafe.Pointer
  // 表示缓冲区中每个元素的大小,当我们创建 channel 时,go 会根据元素的类型计算出 elemsize 的值
  elemsize uint16
  // channel 是否已经关闭,当我们通过 close 函数关闭一个 channel 时,go 会将 closed 字段设置为 1
  closed uint32
  // 表示下一次接收元素的位置,当我们从 channel 中接收数据时,go 会从缓冲区中 recvx 索引的位置读取数据,并将 recvx 加 1
  recvx uint
  // 表示下一次发送元素的位置,在 channel 的发送操作中,如果缓冲区未满,则会将数据写入到 sendx 指向的位置,并将 sendx 加 1
  // 如果缓冲区已满,则发送操作会被阻塞,直到有足够的空间可用
  sendx unit
  // 等待接收数据的 goroutine 队列,用于存储等待从 channel 中读取数据的 goroutine
  // 当 channel 中没有数据可读时,接收者 goroutine 会进入 recvq 等待队列中等待数据的到来
  // 当发送者 goroutine 写入数据后,会将 recvq 等待队列中的接收者 goroutine 唤醒,并进行读取操作
  // 在进行读取操作时,会先检查 recvq 等待队列是否为空,如果不为空,则会将队列中的第一个 goroutine 唤醒进行读取操作
  // 同时,由于 recvq 等待队列时一个 FIFO 队列,因此等待时间最场的 goroutine 会排在队列的最前面,最先被唤醒进行读取操作
  recvq waitq
  // 等待发送数据的 goroutine 队列。sendq 字段是一个指向 waitq 结构体的指针,waitq 是一个用于等待队列的结构体
  // wait 中包含了一个指向等待队列中第一个协程的指针和一个指向等待队列中最后一个协程的指针
  // 当一个协程向一个 channel 中发送数据时,如果该 channel 中没有足够的缓冲区来存储数据,那么发送操作将会被阻塞
  // 直到有另一个协程来接收数据或者 channel 中有足够的缓冲区来存储数据,当一个协程被阻塞在发送操作时
  // 它将会被加入到 sendq 队列中,等待另一个协程来接收数据或者 channel 中有足够的缓冲区来存储数据
  sendq waitq
  // channel 的读写锁,确保多个 goroutine 同时访问时的并发安全,保证读写操作的原子性和互斥性
  // 当一个 goroutine 想要对 channel 进行读写操作室,首先需要获取 lock 锁。如果当前 lock 已经被其他 goroutine 占用
  // 则该 goroutine 被阻塞,直到 lock 被释放,一旦该 goroutine 获取到 lock,就可以进行读写操作,并且在操作完成后释放 lock
  // 以便其他 goroutine 可以访问到 channel
  lock mutex
}

channel 需要注意的:

  1. 一个只读的 channel 或一个只写的 channel 都会导致编译不通过
  2. 双向 channel 可以隐式转换成单向的 channel,反之不行
  3. 向一个已经关闭的 channel 发送数据会导致 panic,从一个已经关闭的 channel 读取数据会返回 channel 中剩余的数据,如果 channel 中没有数据,会返回 channel 中元素的零值
    func main() {
      ch := make(chan int, 1)
      ch <- 1
      a, ok := <-ch    // 1, true
      fmt.Println(a, ok)
      close(ch)
      c, ok := <-ch
      fmt.Println(c, ok) // 0, false => 表示 channel 已经被关闭了
    }
    

channel 的使用误区:

  1. 多次关闭 channel 会导致 panic
  2. channel 中传输的数据大小有限制
    func main() {
      arr := [4096]string{}
      ch := make(chan [4096]string)  // channel element type too large (>64kB)
      ch <- arr
      time.Sleep(time.Second * 2)
    }
    
    • 为什么 go 要对 channel 的大小进行限制?
      • channel 在传递数据时,会将数据进行拷贝,从一个 channel 到另一个 channel 至少会被复制一次,传递的值在某个 channel 中停留过,那个这个值会被复制两次,一次发生在发送协程向缓冲队列推入值时,另一次会发生在接收协程从缓冲队列中取出值时,为了避免过大的复制成本,go 限制了 channel 中传输的数据大小

优雅的退出

监听退出信号:

  • os.SIGHUP:值 1,终端控制进程结束(终端连接断开)
  • os.SIGINT:值 2,用户发送 INTR 字符(Ctrl + C) 触发
  • os.SIGTERM:值 15,结束程序(可以被捕获,阻塞或忽略)
  • os.SIGQUIT:值 3,用户发送 QUIT 字符(Ctrl + /)
  • os.SIGKILL:值 9,无条件结束程序(不能被捕获,阻塞或忽略),一般不用监听
func fn(){
  signals := make(chan os.Signal, 1)
  signal.Notify(signals, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
  s := <-signals
  log.Println("receive system signal: ", s)
  ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
  defer cancel()
  // 需要实现一个 shutdown 方法
  err := server.Shutdown(ctx)
  if err != nil {
    log.Println("server shutdown error: ", err)
  }
}

优雅关闭服务流程:

  1. 捕获终止信号
  2. 停止接收新的请求或连接
  3. 等待已有的请求或连接处理完毕
  4. 释放所有占用的资源
  5. 退出程序
转载自:https://juejin.cn/post/7405250711284596774
评论
请登录