Go 语言开发设计指北
Go 语言是一种强类型、编译型的语言,在开发过程中,代码规范是尤为重要的,一个小小的失误可能会带来严重的事故,拥有一个良好的 Go 语言开发习惯是尤为重要的,遵守开发规范便于维护、便于阅读理解和增加系统的健壮性。 以下是我们项目组开发规范加上自己开发遇到的问题及补充,希望对你有所帮助: 注:我们将以下约束分为三个等级,分别是:【强制】、【推荐】、【参考】。 Go 编码相关 【强制】代码风格规范遵循 go 官方标准:CodeReviewComments,请使用官方 golint lint 进行风格静态分析; 【强制】代码格式规范依照 gofmt,请安装相关 IDE 插件,在保存代码或者编译时,自动将源码通过 gofmt 做格式化处理,保证团队代码格式一致(比如空格,递进等) 【强制】业务处理代码中不能开 goroutine,此举会导致 goroutine 数量不可控,容易引起系统雪崩,如果需要启用 goroutine 做异步处理,请在初始化时启用固定数量 goroutine,通过 channel 和业务处理代码交互,初始化 goroutine 的函数,原则上应该从 main 函数入口处明确的调用: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 func crond() { defer func() { if err := recover(); err != nil { // dump stack & log } }() // do something } func main() { // init system go crond() go crond2() // handlers } 【强制】异步开启 goroutine 的地方(如各种 cronder ),需要在最顶层增加 recover(),捕捉 panic,避免个别 cronder 出错导致整体退出: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func globalCrond() { for _ := ticker.C { projectCrond() itemCrond() userCrond() } } func projectCrond() { defer func() { if err := recover(); err != nil { // 打日志,并预警 } } // do } 【强制】当有并发读写 map 的操作,必须加上读写锁 RWMutex,否则 go runtime 会因为并发读写报 panic,或者使用 sync.Map 替代; 【强制】对于提供给外部使用的 package,返回函数里必须带上 err 返回,并且保证在 err == nil 情况下,返回结果不为 nil,比如: 1 2 resp, err := package1.GetUserInfo(xxxxx) // 在err == nil 情况下,resp不能为nil或者空值 【强制】当操作有多个层级的结构体时,基于防御性编程的原则,需要对每个层级做空指针或者空数据判别,特别是在处理复杂的页面结构时,如: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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 } 【推荐】生命期在函数内的资源对象,如果函数逻辑较为复杂,建议使用 defer 进行回收: 1 2 3 4 5 6 7 8 func MakeProject() { conn := pool.Get() defer pool.Put(conn) // 业务逻辑 ... return } 对于生命期在函数内的对象,定义在函数内,将使用栈空间,减少 gc 压力: 1 2 3 4 5 6 7 func MakeProject() (project *Project){ project := &Project{} // 使用堆空间 var tempProject Project // 使用栈空间 return } 【强制】不能在循环里加 defer,特别是 defer 执行回收资源操作时。因为 defer 是函数结束时才能执行,并非循环结束时执行,某些情况下会导致资源(如连接资源)被大量占用而程序异常: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // 反例: 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 或者 map,在 make 初始化时,指定cap大小,可以大大降低内存损耗,如: 1 2 3 4 5 6 7 8 9 10 11 headList := make([]home.Sections, 0, len(srcHomeSection)/2) tailList := make([]home.Sections, 0, len(srcHomeSection)/2) dstHomeSection = make([]*home.Sections, 0, len(srcHomeSection)) …. if appendToHead { headList = append(headList, info) } else { tailList = append(tailList, info) } …. dstHomeSection = append(dstHomeSection, headList…) dstHomeSection = append(dstHomeSection, tailList…) 【推荐】逻辑操作中涉及到频繁拼接字符串的代码,请使用 bytes.Buffer 替代。使用 string 进行拼接会导致每次拼接都新增 string 对象,增加 GC 负担: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 正例: 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 【强制】对于固定的正则表达式,可以在全局变量初始化时完成预编译,可以有效加快匹配速度,不需要在每次函数请求中预编译: 1 2 3 4 var wordReg = regexp.MustCompile("[\\w]+") func matchWord(word string) bool { return wordReg.MatchString(word) } 【推荐】JSON 解析时,遇到不确定是什么结构的字段,建议使用 json.RawMessage 而不要用 interface,这样可以根据业务场景,做二次 unmarshal 而且性能比 interface 快很多; 【强制】锁使用的粒度需要根据实际情况进行把控,如果变量只读,则无需加锁;读写,则使用读写锁 sync.RWMutex; 【强制】使用随机数时(math/rand),必须要做随机初始化(rand.Seed),否则产生出的随机数是可预期的,在某些场合下会带来安全问题。一般情况下,使用math/rand可以满足业务需求,如果开发的是安全模块,建议使用crypto/rand,安全性更好; 【推荐】对性能要求很高的服务,或者对程序响应时间要求高的服务,应该避免开启大量 gouroutine; 说明:官方虽然号称 goroutine 是廉价的,可以大量开启 goroutine,但是由于 goroutine 的调度并没有实现优先级控制,使得一些关键性的 goroutine(如网络/磁盘IO,控制全局资源的goroutine)没有及时得到调度而拖慢了整体服务的响应时间,因而在系统设计时,如果对性能要求很高,应避免开启大量goroutine。 打点规范 【强制】打点使用.来做分隔符,打点名称需要包含业务名,模块,函数,函数处理分支等,参考如下: 1 2 // 业务名.服务名.模块.功能.方法 service.gateway.module.action.func 【强制】打点使用场景是监控系统的实时状态,不适合存储任何业务数据; 【强制】在打点个数太多时,展示时速度会变慢。建议单个服务打点的key不超过10000个,key中单个维度不同值不超过 1000个(千万不要用 user_id 来打点); 【推荐】如果展示的时候需要拿成百上千个key的数据通过 Graphite 的聚合函数做聚合,最后得到一个或几个 key。这种情况下可以在打点的时候就把这个要聚合的点聚合好,这样展示的时候只要拿这几个 key,对展示速度是巨大的提升。 日志相关 【强制】日志信息需带上下文,其中 logid 必须带上,同一个请求打的日志都需带上 logid,这样可以根据 logid 查找该次请求相关的信息; 【强制】对debug/notice/info 级别的日志输出,必须使用条件输出或者使用占位符方式,避免使用字符拼接方式: 1 log.Debug("get home page failed %s, id %d", err, id) 【强制】如果是解析 json 出错的日志,需要将报错 err 及原内容一并输出,以方便核查原因; 【推荐】对debug/notice/info级别的日志,在打印日志时,默认不显示调用位置(如/path/to/code.go:335) 说明:go 获取调用栈信息是比较耗时的操作(runtime.Caller),对于性能要求很高的服务,特别是大量调用的地方,应尽量避免
以上内容来自博客订阅源,版权归原作者。关注作者后可以在 关注列表 第一时间查看作者文章。
登录后,加入这场讨论。
登录参与