1、尽量使用函数参数的方式传递信息。

协程间少使用共享数据结构(尤其是会变化的全局变量)

2、使用支持并发的go数据结构

比如sync.Map,sync.Once,sync.Map使用要注意几点:
添加不要先查找再添加(查找和添加间数据可能已经并发修改),如下操作是有问题的

val, ok := eMap.Find(key)
   if ok {
       //旧数据
       return
   }

   //处理newNode初始化
   eMap.Store(newNode)
    //........
   //处理新数据

判断是否是新添加数据使用

//处理newNode初始化
   val, loaded :=val, loaded := eMap.LoadOrStore(newNode)
   if loaded {
        //处理旧数据
   } else {
       //新添加数据
   }

另外需要注意,即使按照上面的方法添加,如果新添加数据中涉及必须的初始化,也是有问题的。
因为并发另一个协程认为是旧数据,而添加数据的协程还没初始化完毕。所以初始化必须放在store之前。

删除同理,也不能先Find再决定是不是Delete

3、使用锁

( 无法避免协程间共享数据的情况下)
锁很容易死锁或者拖累效率,使用应遵循以下几个原则:
原则一 锁在数据初始化时赋值,中间不能重新赋值

原则二 锁粒度要小
包括锁的数据粒度小,比如能锁单个node就不锁local区。
粒度小排查方便,性能影响有限,也方便defer解锁

原则三 使用defer解锁
不使用defer,后面增加return流程很容易漏掉解锁步骤。如:

func (node *Node) LinkFindByKey(linkKey LinkKey) *Link {
	node.linkLocker.RLock()
	defer node.linkLocker.RUnlock()
	val, _ := node.LinkTree.Get(linkKey)
	if val == nil {	
		return nil
	}

	return val.(*Link)
}

原则四 把加锁内容单独封装函数,且该函数中不得增加不需要加锁的流程
这是由原则三导致的,不然锁的范围大,很容易死锁,排查也麻烦

原则五 使用for循环访问数据的读锁,尽量保护读取后用局部变量
(以避免锁范围过大,影响效率或难以排查)

for _, v, next := local.GTree.Iterate()(); next != nil; _, v, next = next() {
       //do something
 }

应该保护为

local.GTreeLoker.RLock()
 gTree  :=  local.GTree
 local.GTreeLoker.RUlock()
 for _, v, next := gTree.Iterate()(); next != nil; _, v, next = next() {
       //do something
 }

而不是

local.GTreeLoker.RLock()
  for _, v, next := local.GTree.Iterate()(); next != nil; _, v, next = next() {
       //do something
 }
 local.GTreeLoker.RUlock()

原则五 尽量不要嵌套
一般只要遵循锁最小化,就能做到不嵌套

4、使用channel并发改串行