个人编码规范
枚举值注释必须完善
禁止出现硬编码
有基本的单元测试验证(尽量使用断言而不是手动log人工判定单测是否通过)
接口粒度应该细小、接口依赖应该明确、接口入参出参应该明确,对象化,最好可以做到依赖抽象而不是直接依赖具体的实现
禁止在接口之中有隐藏的依赖条件(入参之中没有的参数比如环境变量,入参应该是决定输出的唯一条件)
接口颗粒度要小一些(入参、出参尽可能简化易读),如果逻辑过于复杂应该对象化拆分,确保大接口拆出的小接口都走单元测试
禁止IDE右侧出现notice黄色告警(单词拼写错误、命名不规范、公有方法没注释等)
所有的指针访问都需要判定是否为nil
所有的数组访问都需要判定length防止数组越界
所有的除数都要判定被除数不是0
关于同步转异步,取决于业务场景。如果是接口响应不强依赖执行结果的,比如用户关注频道后发送欢迎语,关注成功后异步发送欢迎语即可,不能阻塞主要流程,因为用户端不强依赖欢迎语,但是需要快速进入主页面。
包命名规范
禁止滥用init,个人建议整个项目不应该有init,main函数也是唯一的执行入口
禁止滥用全局变量,应该保证依赖的关系足够清晰明了
模块依赖管理,包与包之间的依赖关系清晰(按功能分块按业务水平分层,按数据流垂直分层,每一层之间尽可能解耦),比如三层架构、领域驱动模型等都是追求分层,数据对象模型每一层之间都是解耦的,比如vo(view object视图)和持久化层对象po(数据库映射对象)肯定是转换过的,而不是直接将po往http对外接口抛。
模块依赖管理,包的依赖应当尽可能放在私有属性(并且以选项模式注入依赖)。优点在于依赖会更加清晰容易管理,以及对于单元测试会友好很多。比如:
1.不友好的写法
package tool
var db *DB
type DB struct {
}
func (db *DB) Get() {
}
type A struct {
}
func (a *A) Get() {
if db == nil {
panic("nil")
}
db.Get()
}
// goland生成的B.Get单元测试
package tool
import "testing"
func TestA_Get(t *testing.T) {
tests := []struct {
name string
}{
{
name: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 由于db未初始化是nil所以肯定会有异常
// 这种A的函数Get的依赖对于外部调用者而言是隐式的
// 应该将A的依赖变成显式,对单元测试和维护以及调用都会友好很多
a := &A{}
a.Get()
})
}
}
2.更改后的写法
package tool
var db *DB
type DB struct {
}
func (db *DB) Get() {
}
type A struct {
db *DB
}
func (a *A) Get() {
if a.db == nil {
panic("nil")
}
a.db.Get()
}
// goland生成的单元测试
package tool
import "testing"
func TestA_Get(t *testing.T) {
type fields struct {
db *DB
}
tests := []struct {
name string
fields fields
}{
{
name: "",
fields: fields{
// 此时依赖已经显示出来
// 对于单元测试更加友好
// 并且管理A的依赖也会更加直观
db: new(DB),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &A{
db: tt.fields.db,
}
a.Get()
})
}
}
- 易读,函数禁止超过50行,如果过长应该考虑适当拆分,看过一个函数300行并且参数还是interface,这种逻辑线上不出异常是非常困难的。
// 对于下面的函数而言,QueryC是QueryA和QueryB的合体
// 如果我要保证QueryA的正常那么输入只有2种情况,单元测试,很方便覆盖
// 但是如果保证QueryC的正常我需要验证4中输入情况,正常来说,就会懒,覆盖不充分
// 随着函数变大,输入的可能性变多,自然就会无法覆盖所有的情况
package tool
func QueryA(a bool) int {
if a {
return 1
}
return 2
}
func QueryB(a bool) int {
if a {
return 3
}
return 4
}
func QueryC(a bool, b bool) int {
var one int
var two int
if a {
one = 1
} else {
one = 2
}
if b {
two = 3
} else {
two = 4
}
return one + two
}
扩展性,任何有可能或者极大可能有扩展需求的接口应该考虑扩展性(设计模式的应用),常见的就是三层
if else
应当状态模式替换,日志logger的输出使用责任链模式,非法参数过滤器模式,带上设计模式的关键字可以提升可读性.边界条件,接口编写应当考虑边界条件,比如query数据库起码要考虑加一下limit限制兜底的数据量。
函数的命名除了见名之意,加上一些设计模式的关键字也可以提高可读性,比如将A对象适配为B对象adapter后缀
# 比如过滤器filter做后缀
func EmojiFilter()
# 比如拦截器
func TraceInterceptor()
# 比如装饰器
func (*water) sugarDecorator()
- 如何给变量取一个好名字
风格规范比如小驼峰
避开关键字
避免单字母
名词或者形容词见名知意
复数s
is作为bool类型前缀
避免有歧义的缩写(avg这样的没有 tmp这样的有)
// 一些公认的简写
identification id
average avg
maximum max
minimum min
buffer buf
error err
message msg
image img
length len
library lib
password pwd
position pos
data transfer object dto
view object vo
- 方法命名
个人建议在见名知意的基础上,除了翻译这样的操作,可以加入一些设计模式关键字比如adapter\chain\invoke之类的
架构设计角度
- 微服务之间解耦(mongodb/MySQL数据库层隔离、redis隔离(key业务划分/微服务之间隔离))
- 禁止随意将其他微服务依赖的数据库写入到正在开发的微服务配置,业务数据访问走GRPC
- 错误日志的错误信息严格按照标准(logger)