defer 与闭包,go 并发常见问题

defer 与闭包

  • 闭包复制的是原对象指针,这就很容易解释闭包的延迟引用现象。
  • defer
    • defer函数的参数,是在defer函数被定义的时候就已经明确了。
    • defer函数的执行顺序是后进先出。
    • defer函数可以操作返回值

根据以上两条规则我们来看如下测试代码,

func main() {
   a := 100000
   println(a)
   //1. 单独defer输出--猜想下它的输出
   defer println("defer :", &a, a)
   //2. defer 配合闭包--猜想下它的输出
   defer func() { println("defer fun :", &a, a) }()
   //3. defer 配合闭包与入参
   defer func(a int) { println("defer fun with param :", &a, a) }(a)
   a++
   println(a)
}
  1. 先看a的指针地址
    12都是a原先地址,但是3使用了入参,go的入参是值传递,所哟3的指针地址跟12不同
  2. 在看a的值
    2闭包复制的是对象指针,取数的时候是最新的数值10001
    1和3取的值是在defer函数被定义的时候就已经明确了。为10000

所以输出如下:

100000
100001
defer fun with param : 0xc0000446e8 100000
defer fun : 0xc000044710 100001
defer : 0xc000044710 100000
进程 已完成,退出代码为 0

Go 并发注意事项

  1. goroutine泄露
  • 泄露的原因:
    如果routine在运行中被阻塞,或者速度很慢,不会正常关闭routine时就会泄漏。阻塞分为两种情况:

    • 写channel时,channel被写满了。
      写一个close的channel会panic;
      defer 与闭包,go 并发常见问题_第1张图片

    • 读channel时,channel被读空了。
      读一个close的channel不会panic,会返回相应的零值。
      defer 与闭包,go 并发常见问题_第2张图片

  • 防止泄露:

    • 尽量选择一个合适的缓冲区大小,可以减少协程阻塞
    • 用select监听多个 case。

举个例子

//选择一个合适的缓冲区大小
myChan = make(chan int, 10) 

// 非阻塞读一次
select {
    case msg := <- myChan:
        fmt.Println(msg)
    default:
        fmt.Println("No Msg")
}

// 阻塞<=1s读
for {
    select {
        case msg := <- myChan:
            fmt.Println(msg)            
            return msg
        case <-time.After(1 * time.Second):
            fmt.Println("You're too slow.")
            return ""
        default:
            fmt.Println("No Msg")        
            time.Sleep(50 * time.Millisecond)
    }
}

// 非阻塞写
select {
    case myChan <- "message":
        fmt.Println("sent the message")
    default:
        fmt.Println("no message sent")
}

遵循一个约定:谁创建,谁停止(谁创建goroutine,谁负责停止goroutine)

  1. 同步
    在生产者消费者模型中,主线程需要等待所有消费者退出后再结束。这里需要一个信号量,在go语言中我们可以使用WaitGroup 进行信息同步,当然也可以使用一个单独的channel 传递结束信息。
  • 使用WaitGroup
func producer(ch chan int) {
   defer close(ch) // defer保证异常退出时自动关闭channel
   for i := 0; i < 6; i++ {
      time.Sleep(time.Second)
      ch <- i
   }
}
func consumer(id int, ch <-chan int, wg *sync.WaitGroup) {
    defer wg.Done() //使用defer确保执行成功
   for v := range ch {
      time.Sleep(time.Millisecond)
      fmt.Printf("ID:%d,task:%d\n", id, v)
   }
   fmt.Printf("ID:%d,task done\n", id)
}
func main() {
   conNum := 2
   var wg sync.WaitGroup
   //定义带缓存的管道
   ch := make(chan int, conNum)

   // 创建生产者
   go producer(ch)
   for i := 0; i < conNum; i++ {
      wg.Add(1) // 避免一次设置所有的数量,
      go consumer(i, ch, &wg)
   }
   wg.Wait()
   fmt.Println(runtime.NumGoroutine())
}
输出:
ID:1,task:0
ID:0,task:1
ID:1,task:2
ID:0,task:3
ID:1,task:4
ID:1,task done
ID:0,task:5
ID:0,task done
1
  • channel
func producer(ch chan int) {
   defer close(ch) // defer保证异常退出时自动关闭channel
   for i := 0; i < 6; i++ {
      time.Sleep(time.Second)
      ch <- i
   }
}
func consumer(id int, ch <-chan int, done chan bool) {
   defer func() { done <- true }() //使用defer确保执行成功
   for v := range ch {
      time.Sleep(time.Millisecond)
      fmt.Printf("ID:%d,task:%d\n", id, v)
   }
   fmt.Printf("ID:%d,task done\n", id)
}
func main() {
   conNum := 2
   var wg sync.WaitGroup
   //定义带缓存的管道
   ch := make(chan int, conNum)
   done := make(chan bool, conNum)

   // 创建生产者
   go producer(ch)
   for i := 0; i < conNum; i++ {
      wg.Add(1) // 避免一次设置所有的数量,
      go consumer(i, ch, done)
   }
   // 使用channel 阻塞主线程
   for i := 0; i < conNum; i++ {
      <-done
   }
   fmt.Println(runtime.NumGoroutine())
}
  1. panic和Recover
    Panic的出现会使服务重启,为了不让服务在遇到panic时频繁重启,我们需要在新启动的goroutine内捕获Panic。但是defer只有在当前协程里定义,才能捕获panic。举个:
good casefunc main() {
   defer func() {
      log.Println("done")  // Println executes normally even if there is a panic
      if x := recover(); x != nil {
         log.Printf("run time panic: %v", x)
      }
   }()
   panic("hello,I'm panic") //panic和defer在同一个协程里面
}

输出:
2022/06/30 14:23:13 done
2022/06/30 14:23:13 run time panic: hello,I'm panic

进程 已完成,退出代码为 0

Bad case:

func main() {
   defer func() {
      log.Println("done")  // Println executes normally even if there is a panic
      if x := recover(); x != nil {
         log.Printf("run time panic: %v", x)
      }
   }()
   go func() {
      panic("hello,I'm Goroutine panic") // Panic在新的go协程
   }()
}
输出:
panic: hello,I'm Goroutine panic

goroutine 6 [running]:
main.main.func2()
        /Users/bytedance/GolandProjects/awesomeProject/main.go:23 +0x27
created by main.main
        /Users/bytedance/GolandProjects/awesomeProject/main.go:22 +0x45

进程 已完成,退出代码为 2

所以在go 的并发中我们启动新的协程时要使用defer捕获新协程的panic,防止拖累主服务。

Go 并发最佳实践

参考网上的实现,提供了一种安全地启动并发的实现,其实就是类似AOP的思想,在所有goroutine启动之前defer+recover确保安全,同时提供了带err处理的方式(可选),省去之前单独传入errChan来收集错误的烦恼

package main

import (
   "context"
   "fmt"
   "sync/atomic"
   "time"
)

type PanicGroup[T int | string] struct {
   ctx    context.Context  //ctx
   param  chan T           //参数 通道
   panics chan interface{} // 协程 panic 通知信道
   dones  chan int         // 协程完成通知信道
   err    chan error       // 错误消息通道
   jobN   int32            // 协程并发数量
}

func (g *PanicGroup[T]) GoWithErrReturn(f func(param chan T) error) *PanicGroup[T] {
   atomic.AddInt32(&g.jobN, 1)
   go func() {
      defer func() {
         if r := recover(); r != nil {
            g.panics <- r
            return
         }
         g.dones <- 1
      }()
      g.err <- f(g.param)
   }()

   return g // 方便链式调用
}

func (g *PanicGroup[T]) Go(f func(param chan T)) *PanicGroup[T] {
   atomic.AddInt32(&g.jobN, 1)
   go func() {
      defer func() {
         if r := recover(); r != nil {
            g.panics <- r
            return
         }
         g.dones <- 1
      }()
      f(g.param)
   }()

   return g // 方便链式调用
}

func (g *PanicGroup[T]) Wait(ctx context.Context) error {
   for {
      select {
      case <-g.dones:
         if atomic.AddInt32(&g.jobN, -1) == 0 {
            return nil
         }
      case p := <-g.panics:
         fmt.Println(p)
         return nil
      case e := <-g.err:
         return e
      case <-ctx.Done():
         return ctx.Err()
      }
   }
}

func NewPanicGroup[T int | string](ctx context.Context, chanCap int) *PanicGroup[T] {
   return &PanicGroup[T]{
      param:  make(chan T, chanCap),
      panics: make(chan interface{}, chanCap),
      dones:  make(chan int, chanCap),
      err:    make(chan error, chanCap),
      ctx:    ctx,
   }
}

func main() {
   ctx := context.Background()
   //定义消费者数量
   comNum := 3
   var g = NewPanicGroup[int](ctx, comNum)

   //创建生产者
   g.Go(producer)
   //创建消费者
   for i := 0; i < comNum; i++ {
      g.Go(consumer)
   }

   // 等待所有 goroutines 结束
   err := g.Wait(ctx)
   if err != nil {
      fmt.Printf("%v", err)
      return
   }
   fmt.Printf("all done")
}

func producer(ch chan int) {
   defer close(ch) // defer保证异常退出时自动关闭channel
   for i := 0; i < 6; i++ {
      time.Sleep(time.Second)
      ch <- i
   }
}
func consumer(ch chan int) {
   for v := range ch {
      if v == 4 {
         panic("o my god!!! panic when task 4")
      }
      time.Sleep(time.Millisecond)
      fmt.Printf("all:6,task:%d\n", v)
   }
   fmt.Printf("task done\n")
}
输出:
all:6,task:0
all:6,task:1
all:6,task:2
all:6,task:3
o my god!!! panic when task 4
all done
进程 已完成,退出代码为 0

附录

注意事项

  • map不能并发写。因为map为引用类型,所以即使函数传值调用,有需要请使用sync.Map包
  • slice并发写要加锁
    参考:
  • Golang并发注意点
  • Go 语言的设计和坑
  • The Go Programming Language Specification - The Go Programming Language
  • Go并发编程规范

你可能感兴趣的:(后端,golang,开发语言,后端)