本文转载:https://www.debuginn.cn/6832.html
Go 语言是一种强类型、编译型的语言,在开发过程中,代码规范是尤为重要的,一个小小的失误可能会带来严重的事故,拥有一个良好的 Go 语言开发习惯是尤为重要的,遵守开发规范便于维护、便于阅读理解和增加系统的健壮性。
以下是我们项目组开发规范加上自己开发遇到的问题及补充,希望对你有所帮助: 注:我们将以下约束分为三个等级,分别是:【强制】 、【推荐】 、【参考】 。
Go 编码相关
【强制】 代码风格规范遵循 go
官方标准:CodeReviewComments
,请使用官方golint lint
进行风格静态分析;
【强制】 代码格式规范依照gofmt
,请安装相关 IDE
插件,在保存代码或者编译时,自动将源码通过gofmt
做格式化处理,保证团队代码格式一致(比如空格,递进等)
【强制】 业务处理代码中不能开goroutine
,此举会导致goroutine
数量不可控,容易引起系统雪崩,如果需要启用goroutine
做异步处理,请在初始化时启用固定数量goroutine
,通过channel
和业务处理代码交互,初始化goroutine
的函数,原则上应该从main
函数入口处明确的调用:
|
|
【强制】 异步开启goroutine
的地方(如各种cronder
),需要在最顶层增加recover()
,捕捉panic
,避免个别cronder
出错导致整体退出:
|
|
【强制】 当有并发读写map
的操作,必须加上读写锁RWMutex
,否则go runtime
会因为并发读写报panic
,或者使用sync.Map
替代;
【强制】 对于提供给外部使用的package
,返回函数里必须带上err
返回,并且保证在err == nil
情况下,返回结果不为nil
,比如:
|
|
【强制】 当操作有多个层级的结构体时,基于防御性编程的原则,需要对每个层级做空指针或者空数据判别,特别是在处理复杂的页面结构时,如:
|
|
【推荐】 生命期在函数内的资源对象,如果函数逻辑较为复杂,建议使用defer进行回收:
|
|
对于生命期在函数内的对象,定义在函数内,将使用栈空间,减少gc
压力:
|
|
【强制】 不能在循环里加defer
,特别是defer
执行回收资源操作时。因为defer
是函数结束时才能执行,并非循环结束时执行,某些情况下会导致资源(如连接资源)被大量占用而程序异常:
|
|
【推荐】 对于可预见容量的slice
或者map
,在make
初始化时,指定cap
大小,可以大大降低内存损耗,如:
|
|
【推荐】 逻辑操作中涉及到频繁拼接字符串的代码,请使用bytes.Buffer
替代。使用string
进行拼接会导致每次拼接都新增string
对象,增加GC
负担:
|
|
【强制】 对于固定的正则表达式,可以在全局变量初始化时完成预编译,可以有效加快匹配速度,不需要在每次函数请求中预编译:
|
|
【推荐】 JSON
解析时,遇到不确定是什么结构的字段,建议使用json.RawMessage
而不要用interface
,这样可以根据业务场景,做二次unmarshal
而且性能比interface
快很多;
【强制】 锁使用的粒度需要根据实际情况进行把控,如果变量只读,则无需加锁;读写,则使用读写锁sync.RWMutex
;
【强制】 使用随机数时(math/rand
),必须要做随机初始化(rand.Seed
),否则产生出的随机数是可预期的,在某些场合下会带来安全问题。一般情况下,使用math/rand
可以满足业务需求,如果开发的是安全模块,建议使用crypto/rand
,安全性更好;
【推荐】 对性能要求很高的服务,或者对程序响应时间要求高的服务,应该避免开启大量gouroutine
;
说明:官方虽然号称goroutine
是廉价的,可以大量开启goroutine
,但是由于goroutine
的调度并没有实现优先级控制,使得一些关键性的goroutine
(如网络/磁盘IO,控制全局资源的goroutine
)没有及时得到调度而拖慢了整体服务的响应时间,因而在系统设计时,如果对性能要求很高,应避免开启大量goroutine
。
打点规范
【强制】 打点使用.来做分隔符,打点名称需要包含业务名,模块,函数,函数处理分支等,参考如下:
|
|
【强制】 打点使用场景是监控系统的实时状态,不适合存储任何业务数据;
【强制】 在打点个数太多时,展示时速度会变慢。建议单个服务打点的key
不超过10000个,key
中单个维度不同值不超过 1000
个(千万不要用 user_id 来打点);
【推荐】 如果展示的时候需要拿成百上千个key的数据通过 Graphite 的聚合函数做聚合,最后得到一个或几个 key。这种情况下可以在打点的时候就把这个要聚合的点聚合好,这样展示的时候只要拿这几个 key,对展示速度是巨大的提升。
日志相关
【强制】 日志信息需带上下文,其中logid
必须带上,同一个请求打的日志都需带上logid
,这样可以根据logid
查找该次请求相关的信息;
【强制】 对debug/notice/info
级别的日志输出,必须使用条件输出或者使用占位符方式,避免使用字符拼接方式:
|
|
【强制】 如果是解析json
出错的日志,需要将报错err
及原内容一并输出,以方便核查原因;
【推荐】 对debug/notice/info
级别的日志,在打印日志时,默认不显示调用位置(如/path/to/code.go:335
)
说明:go
获取调用栈信息是比较耗时的操作(runtime.Caller
),对于性能要求很高的服务,特别是大量调用的地方,应尽量避免开发人员在使用该功能时,需知悉这个调用带来的代价。
Redis 相关
【推荐】 统一使用:作为前缀后缀分隔符,这里可以根据 Redis
中间件 key proxy
怎么解析分析Key
进行自定义,便于基础服务的数据可视化及问题排查;
【强制】 避免使用 HMGET/HGETALL/HVALS/HKEYS/SMEMBERS
阻塞命令这类命令在value
较大时,对 Redis
的 CPU/带宽消耗较高,容易导致响应过慢引发系统雪崩;
【强制】 不可把 Redis
当成存储,如有统计相关的需求,可以考虑异步同步到数据库进行统计,Redis
应该回归缓存的本质;
【推荐】 避免使用大 key
,按经验超过 10k 的 value
,可以压缩(gzip/snappy
等算法)后存入内存,可以减少内存使用,其次降低网络消耗,提高响应速度:
|
|
【推荐】 Redis
的分布式锁,可以使用:
|
|
【推荐】 尽量避免在逻辑循环代码中调用 Redis
,会产生流量放大效应,请求量较大时需采用其他方法优化(比如静态配置文件);
【推荐】 key
尽量离散读写,通过uid/imei/xid
等跟用户/请求相关的后缀分摊到不同分片,避免分片负载不均衡;
【参考】 当缓存量大,请求量较高,可能超出 Redis
承受范围时,可充分利用本地缓存(localcache)+redis
缓存的组合方案来缓解压力,削减峰值:
使用这个方法需要具备这几个条件:
cache
内容与用户无关,key 状态不多,属于公共信息;- 该
cache
内容时效性较高,但是访问量较大,有峰值流量。
|
|
【参考】 对于请求量高,实时性也高的内容,如果纯粹使用缓存,当缓存失效瞬间,会导致大量请求穿透到后端服务,导致后端服务有雪崩危险:
如何兼顾扛峰值,保护后端系统,同时也能保持实时性呢?在这种场景下,可以采用随机更新法更新数据,方法如下:
正常请求从缓存中读取,缓存失效则从后端服务获取; 在请求中根据随机概率1%(或者根据实际业务场景设置比率)会跳过读取缓存操作,直接从后端服务获取数据,并更新缓存。 这种做法能保证最低时效性,并且当访问量越大,更新概率越高,使得内容实时性也越高。
如果结合上一条 localcache+rediscache
做一二级缓存,则可以达到扛峰值同时保持实时性。
数据库相关
【强制】 操作数据库 sql
必须使用 stmt
格式,使用占位符替代参数,禁止拼接 sql
;
【强制】 SQL语句查询时,不得使用 SELECT * (即形如 SELECT * FROM tbl WHERE)
,必须明确的给出要查询的列名,避免表新增字段后报错;
【强制】 对于线上业务 SQL
,需保证命中索引,索引设计基于业务需求及字段区分度,一般可区分状态不高的字段(如 status
等只有几个状态),不建议加到索引中;
【强制】 在成熟的语言中,有实体类,数据访问层(repository / dao
)和业务逻辑层(service
);在我们的规范中存储实体struct
放置于entities
包下;
【强制】 对于联合索引,需将区分度较大的字段放前面,区分度小放后面,查找时可以减少被检索数据量;
|
|
【强制】 所有数据库表必须有主键 id
;
【强制】 主键索引名为 pk字段名; 唯一索引名为 uk字段名; 普通索引名则为 idx_字段名;
【强制】 防止因字段类型不同造成的隐式转换,导致索引失效,造成全表扫描问题;
【强制】 业务上有唯一特性的字段,即使是多字段的组合,也必须建成唯一索引;
【强制】 一般事务标准操作流程:
|
|
【强制】 执行事务操作时,请确保SELECT ... FOR UPDATE
条件命中索引,使用行锁,避免一个事务锁全表的情况;
【强制】 禁止超过三个表的join
,需要join
的字段,数据类型必须一致,多表关联查询时,保证被关联的字段有索引;
【强制】 数据库max_open
连接数不可设置过高,会导致代理连接数打满导致不可用状况;