用 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,安全性更好;