用 recover 处理 goroutine 中 panic

异步开启 goroutine 的地方,需要在最顶层增加recover(),捕捉panic,避免个别 goroutine 出错导致整体退出:

package main

import (
    "fmt"
    "time"
)

func Do1() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()

    time.Sleep(2 * time.Second)
    panic("Do1 panic")
}

func Do2() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()

    time.Sleep(3 * time.Second)
    panic("Do2 panic")
}

func main() {
    go Do1()
    go Do2()

    time.Sleep(5 * time.Second)
    fmt.Println("main done")
}

控制 goroutine 数量

业务处理代码中不能开goroutine,此举会导致goroutine数量不可控,容易引起系统雪崩,如果需要启用goroutine做异步处理,请在初始化时启用固定数量goroutine,通过channel和业务处理代码交互,初始化goroutine的函数,原则上应该从main函数入口处明确的调用

多线程 map 需要用锁

当有并发读写map的操作,必须加上读写锁RWMutex,否则go runtime会因为并发读写报panic,或者使用sync.Map替代;

单例中判断 nil 语句也要在锁中

package main

import (
    "fmt"
    "sync"
    "time"
)

//使用的包

var person *Person
var lock sync.Mutex

type Person struct {
    Name string
    Age  int
}

func getPerson() *Person {
    lock.Lock()
    defer lock.Unlock()
    if person == nil {
        person = &Person{"Lee", 20}
    }
    return person
}

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            p := getPerson()
            fmt.Println(*p)
        }()
    }

    time.Sleep(2 * time.Second)
}

使用 sync.once 创建单例

package main

import (
    "fmt"
    "sync"
    "time"
)

//使用的包

var person *Person
var once sync.Once

type Person struct {
    Name string
    Age  int
}

func getPerson() *Person {
    if person == nil {
        person = &Person{"Lee", 20}
    }
    return person
}

func main() {

    for i := 0; i < 10; i++ {
        once.Do(
            func() {
                p := getPerson()
                fmt.Println(*p)
            })
    }

    time.Sleep(2 * time.Second)
}

Do 中的代码只被执行一次。

对外使用的包返回结果要有 err

对于提供给外部使用的package,返回函数里必须带上err返回,并且保证在err == nil情况下,返回结果不为nil,比如:

resp, err := package1.GetUserInfo(xxxxx)
// 在err == nil 情况下,resp不能为nil或者空值

对每个层级做空指针或者空数据判别

当操作有多个层级的结构体时,基于防御性编程的原则,需要对每个层级做空指针或者空数据判别,特别是在处理复杂的页面结构时,如:

type Section struct {
     Item   *SectionItem
     Height int64
     Width  int64
 }
 type SectionItem struct {
     Tag    string
     Icon   string
     ImageURL string
     ImageList []string
     Action *SectionAction
 }
 type SectionAction struct {
     Type  string
     Path  string
     Extra string
 }

func getSectionActionPath(section *Section) (path string, img string, err error) {
   if section.Item == nil || section.Item.Action == nil { // 要做好足够防御,避免因为空指针导致的panic
      err = fmt.Errorf("section item is invalid")
      return
   }

   path = section.Item.Action.Path

   img = section.Item.ImageURL
   // 对取数组的内容,也一定加上防御性判断
   if len(section.Item.ImageList) > 0 { 
      img = section.Item.ImageList[0]
   }
   return
}

尽量使用栈空间加快程序运行速度

对于生命期在函数内的对象,定义在函数内,将使用栈空间,减少gc压力:

func MakeProject() (project *Project){

   project := &Project{} // 使用堆空间
   var tempProject Project  // 使用栈空间

   return
}

使用 defer 回收复杂的资源对象

func MakeProject() {
   conn := pool.Get()
   defer pool.Put(conn)

   // 业务逻辑
   ...
   return
}

不要在循环里使用 defer

不能在循环里加defer,特别是defer执行回收资源操作时。因为defer是函数结束时才能执行(在 for 中用函数将逻辑包裹,即可在for 中的函数执行完毕后立即释放资源),并非循环结束时执行,某些情况下会导致资源(如连接资源)被大量占用而程序异常:

// 反例:
for {
   row, err := db.Query("SELECT ...")
   if err != nil {
      ...
   }
   defer row.Close() // 这个操作会导致循环里积攒许多临时资源无法释放
   ...
}

// 正确的处理,可以在循环结束时直接close资源,如果处理逻辑较复杂,可以打包成函数:
for {
   func () {
      row, err := db.Query("SELECT ...")
      if err != nil {
         ...
      }
      defer row.Close()
      ...
   }()
}

创建 slice 时候指定 cap 大小

对于可预见容量的slice或者map,在make初始化时,指定cap大小,可以大大降低内存损耗

字符串拼接使用 bytes.Buffer 替代

逻辑操作中涉及到频繁拼接字符串的代码,请使用bytes.Buffer替代。使用string进行拼接会导致每次拼接都新增string对象,增加GC负担:

// 正例:
var buf bytes.Buffer
for _, name := range userList {
   buf.WriteString(name)
   buf.WriteString(",")
}
return buf.String()

// 反例:
var result string
for _, name := range userList {
   result += name + ","
}
return result

预编译固定的正则表达式

对于固定的正则表达式,可以在全局变量初始化时完成预编译,可以有效加快匹配速度,不需要在每次函数请求中预编译:

var wordReg = regexp.MustCompile("[\\w]+")
func matchWord(word string) bool {
   return wordReg.MatchString(word)
}

使用 json.RawMessage 解析不确定结构的 JSON

JSON 解析时,遇到不确定是什么结构的字段,建议使用json.RawMessage而不要用interface,这样可以根据业务场景,做二次unmarshal而且性能比interface快很多;

package main

import (
    "encoding/json"
    "fmt"
)

type TestStruct struct {
    Type int
    Body json.RawMessage
}

type Person struct {
    Name string
    Age  int
}

type Worker struct {
    Name string
    Job  string
}

func main() {
    input := `
       {
        "Type": 1,
        "Body":{ 
            "Name":"ff",
            "Age" : 19
         }
    }`

    ts := TestStruct{}

    if err := json.Unmarshal([]byte(input), &ts); err != nil {
        panic(err)
    }

    switch ts.Type {
    case 1:
        var p Person
        if err := json.Unmarshal(ts.Body, &p); err != nil {
            panic(err)
        }
        fmt.Println(p)
    case 2:
        var w Worker
        if err := json.Unmarshal(ts.Body, &w); err != nil {
            panic(err)
        }
        fmt.Println(w)
    }

}

只读变量不加锁

锁使用的粒度需要根据实际情况进行把控,如果变量只读,则无需加锁;读写,则使用读写锁sync.RWMutex;

使用 rand.seed 做随机初始化

使用随机数时(math/rand),必须要做随机初始化(rand.Seed),否则产生出的随机数是可预期的,在某些场合下会带来安全问题。一般情况下,使用math/rand可以满足业务需求,如果开发的是安全模块,建议使用crypto/rand,安全性更好;