一、切片append方法不是原子操作

多个协程操作同一个切片,使用append方法添加元素时,存在并发安全问题,需要对append方法加锁

func example() {
	// 添加互斥锁
	var lock sync.Mutex
	eg := errgroup.Group{}
	res := make([]int, 0)
	for i := 0; i < 100; i++ {
		eg.Go(func() error {
			lock.Lock() // 加锁
			res = append(res, i)
			lock.Unlock() // 解锁
			return nil
		})
	}
	// 等待并发请求全部完成
	_ = eg.Wait()
	fmt.Println(res)
}

二、所有for循环里的协程对于for变量是可见的

如下所示例子,obj变量对于所有协程都是可见的,如果不先用临时变量进行赋值(oneObj := obj) 那么res切片中存储的都是同一个元素

type Obj struct {
	Name string
	Age  int
}

func example2() {
	eg := errgroup.Group{}
	in := make([]Obj, 10)
	for i := 0; i < 10; i++ {
		in = append(in, Obj{
			Number: i,
		})
	}
	for _, obj := range in {
		// 需要先进行赋值
		oneObj := obj
		eg.Go(func() error {
			fmt.Println(oneObj)
			return nil
		})
	}
	// 等待并发请求全部完成
	_ = eg.Wait()
}

三、errgroup的使用

推荐使用Kratos 改进的 errgroup,Kratos对errgroup做了一些扩展,加入了 recover和并行数的errgroup,err 中包含详细堆栈信息(coredump 发生时),使得 errgroup 机制更加健壮。

四、内存可见性,happens-before规则

1、import package时

如果package p下引入了package q,那么package q 的 init 函数执行先于 package p 后面执行任何其他代码

2、创建协程时

创建函数本身先于 goroutine 内的第一行代码执行。

3、销毁协程时

goroutine销毁事件并不保证先于所有的程序内的其他事件,销毁事件是不可见的

4、Channel通信

  • channel 的 closing 操作先于 channel receive操作返回0值。因为返回0值是channel关闭导致的
  • 对于初始化有缓存的channel(var c = make(chan int, 10)),send 操作先于 receive 操作
  • 对于初始化无缓存的 channel(var c = make(chan int)), receive 操作先于 send 操作
  • 对于初始化有缓存的channel(var c = make(chan int, C)),如果缓存的长度是C,那么第 K 个元素的 receive 操作先于 第 K+C 个元素 的 send 操作

5、锁

  • 对任意 sync.Mutex 和 sync.RWMutex 的锁变量 L,且n < m,那么第 n 次调用 L.Unlock() 逻辑先于(结果可见于)第 m 次调用 L.Lock() 操作
  • 对 sync.RWMutex 类型的锁变量 L,L.Unlock( ) 可见于 L.Rlock( ) ,第 n 次的 L.Runlock( ) 先于 第 n+1 次的 L.Lock()