style-guide
0.0.1 · golang#
https://github.com/golang/go/wiki/CodeReviewComments#go-code-review-comments https://github.com/golang/go/wiki/CommonMistakes https://go.dev/doc/effective_go#errors
0.0.2 · benbjohnson#
https://medium.com/@benbjohnson/structuring-applications-in-go-3b04be4ff091
0.0.3 · uber#
https://github.com/uber-go/guide/blob/master/style.md
0.0.3.1 · 指南
指向接口的指针:一般直接传值;实现接口方法时,使用指针可以修改底层数据
验证接口的合理性:接口体指针实现了接口方法 — var _ 接口 = (*接口体)nil 或者 结构体内嵌接口 — var _ 接口 = 接口体{}
接收者与接口:指针接收者与值接收者
mutex的0值是有效的:可以直接 var mu sync.Mutex ; mu.Lock() ,可以结构体内嵌mutex直接使用(不可导出的结构体),或者申明为结构体的一个字段(可导出结构体,将mutex申明为私有)
在边界处复制slice和map:slice和map有指向底层数据的指针,复制、返回时,需要新建一个slice或者map,重新复制其中的数据。
defer和清理:使用defer清理资源,例如锁、文件等
channel的大小设置为1(buffered)或者不设置(unbuffered):
枚举从1开始: iota+1 ,当0被视作默认行为的时候,可以从0开始
使用time包处理时间:time.Time表示时间实例,time.Duration表示一个时间段;time包没有考虑到闰秒(leap second)
错误类型:申明错误的不同选择1. errors.New 简单错误,无额外信息 2. fmt.Errorf 3. 自定义类型实行error接口的Error()方法 客户端发现且处理错误 4. “pkg/errors”.Wrap 传播由下游函数返回的错误 ; 注意直接暴露自定义的错误类型结构体,应该使用一系列方法,去设置错误、判断错误
包装错误:如果调用函数失败,主要有三种传播错误的选择:1. 在不需要添加特定上下文信息且保证原错误的情况下,返回原错误 2. 使用”pkg/errors”.Wrap 包装上下文,因此可以提供更多信息,且使用 “pkg/errors”.Cause可以拿到原错误 3. 如果调用者不需要发现与处理错误,使用fmt.Errorf ;推荐尽可能的添加上下文而不是使用不详细的错误,在添加上下文的时候,保证言简意赅
处理类型断言失败:使用common-ok方式接收断言的返回值;而不是使用返回值,因为在这种情况下,不准确的类型会导致panic
不要panic:运行在生产环境下的代码应该避免panic,panic是级联故障的一个主要原因;如果一个错误发生了,应该返回这个错误给调用者,让其决定如何处理这个错误;panic/recover不是一个处理错误的策略:一个程序只有在发生不可恢复的错误下才必须panic,比如一个对nil解引用;程序初始化是一个意外,在程序启动的时候,不好的东西可能会打断程序从而可能造成panic;甚至在test中,使用t.Fatal 或者t.FailNow 替代panic确保test被标记为failed;
使用 go.uber.org/atomic:sync/stomic包使用原生类型进行操作,这很容易忘记使用atomic操作读取或者修改一个变量,uber的atomic包隐藏了底层类型,封装了一些安全的类型;
避免可变的全局变量:避免可变的全局变量,而是选择依赖注入
避免在可导出的类型中内嵌类型:内嵌的类型缺失了实现的细节,阻碍类型的演变,并且模糊了文档;go允许内嵌作为组合和继承的折中方法,外层类型隐式复制了内嵌类型的方法,这些复制的方法,默认委托给了内嵌类型的实例的方法;外层的这个结构体同时获得和内嵌类型名称相同的字段,如果内嵌类型是可导出的,那么这个字段也是可导出的;为了保持向后兼容性,外层类型的未来版本必须保持这个内嵌的类型;内嵌类型很少是必须的;很方便帮助我们避免啰嗦的委托方法;内嵌一个可兼容的接口提供给开发者更多的灵活性,但是依旧缺失了 具体类型使用抽象实现的细节;内嵌结构体或者接口都限制了外层类型的演变:*添加方法到内嵌接口是一个重大改变 *从一个内嵌结构体移除方法是一个重大改变 *移除内嵌类型是一个重大改变 *替换内嵌的类型,即使使用满足接口的可替代类型,是一个重大改变;
尽管写这些委托的方法是很啰嗦的,这个额外努力隐藏了实现的细节,也提供了可以改变的机会,并且消除了为了发现完全列表的间接性;
避免使用build-in 的名字:
避免init():可能的话,避免init();当不可避免或者必需的时候,代码应该是:*完全确定的,与程序的环境与调用无关 *避免依赖有序或者其他init()函数的副作用 *避免操作全局的 或者环境变量 *避免I/O,包括文件系统、网络、系统调用 ; init()更好或者必须的场景:*复制的表达式,不能使用单赋值语句表示 *可拔插的hooks *确定的提前计算
0.0.3.2 · 性能
使用strcov 而不是fmt: 避免在循环内,对一个固定的字符串进行转字节数组操作: 更倾向于指定容器容量:
0.0.3.3 · 规范
一致性:
相似申明分组:
import分组顺序:
包命名:
函数命名:
import别名:
函数分组与排序:
减少嵌套:优先处理特殊情况和处理错误,尽早的返回或跳出循环
不必要的else:
顶部变量申明:顶部变量使用var ,不指定类型,除非和右边表达式返回的类型不一致;
将不可导出的全局变量加上前缀_ :不可导出的,顶部的var 或者const 添加下划线_前缀;特例,不可导出的错误值,使用err作前缀;原理,因为顶部的全局变量有包的范围,使用普通的名字可能造成在其他文件有意外的错误使用;
内嵌在结构体中:内嵌的类型应该放在类型内部前面部分的字段中,而且与其他的普通字段有一个分割行;内嵌不应该: *纯粹为了好看或方便 *使得外层类型构造或使用起来更难 *影响外层类型的零值 *使得外层类型暴露出不相干的函数或字段 *暴露出不可导出的类型 *影响外层类型的复制语义 *改变外层类型的的API或者类型语义 *嵌入内部类型的非规范形式 *暴露出外层类型的实现细节 *允许用户观察和控制类型内部 *通过某种包装来改变内部功能的一般行为,使用户感到惊讶
使用字段名称初始化结构体:
局部变量申明: 使用短变量申明:= ,某些特殊情况下,默认值更清晰时,可以使用 var,比如声明空切片
nil是有效的切片:长度为0的切片,可以返回nil ,判断切片的长度是否为0,使用len(),而不用nil,切片的零值(使用var申明)可以不在make()后直接使用 注意:nil切片与长度为0的切片是不等价的,某些情况被不同对待的(比如序列化)
缩小变量的范围: 优先级在减少嵌套的后面
避免裸露的参数:多个参数的意思不明确的时候,在函数调用参数后面使用/**/注释;但是,为了可读性与类型安全,可以使用自定义的类型,可以使用int的别名,定义几个枚举
使用原生string字面量,从而避免转移: hello"nihao"
初始化结构体引用: 使用&{T}而不是new(T)
初始化map: 倾向于使用make()初始化空map,然后程序化的填充map;如果map知道了固定数量的元素,那么使用字面量初始化
格式化字符串:format使用的字符串,如果在函数外面,应该使用const固定,便于go vet静态分析到;
打印格式函数的命名:便于go vet发现并检测格式化所用的字符串,默认使用Printf-格式,或者以f结尾命名,但是需要在govet的时候指定函数名称
0.0.3.4 · 模式
表格驱动测试: Functional Options:
0.0.3.5 · Linting# style of go#
0.1 · 指导篇
不需要指向接口的指针:接口底层数据是个指针; 指针类型实现接口的方法,可以修改类型数据;
适当的,在编译期,验证类型是否实现接口;
// 零值赋值给指针:
// 1.指针类型
var _ YourInterface = (*YourType)(nil)
// 2.非指针类型
var _ YourInterface = YourType{}
避免对接口的修改,影响用户(可能使用了对应类型);
方法的接收者:指针可以调用值作为接收者的方法,反之不行;
实现接口:指针、值作为接收者实现接口,则指针实现接口,而值未实现接口
零值mutex是有效的;
不要内嵌mutex,暴露不需要的方法;
复制slice和map注意:两者都是底层数据含指针,指向真实数据;
copy(dstSlice, srcSlice)
newMap := make(map[oldMapKeyType]oldMapValueType, len(oldMap))
for k, v := range oldMap {
newMap[k] = v
}
使用defer进行清理工作;
channel的大小应该是0或1:认真考虑决定channel大小的原因
枚举值应该从1开始,避免0值问题;
使用time包处理时间:
time.Time表示时间点;
time.Duration表示时间段;
当使用数字表示时间时,字段名需要加上单位;
错误变量、错误类型:
errors.Is()
errors.As()
errors.New("err str")
fmt.Errorf("%w",err)
包装错误:
// 添加错误上下文
fmt.Errorf("%w",err) // 错误被包装了,可以使用 errors.Is() 和 errors.As()
fmt.Errorf("%v",err)
命名错误:变量以Err开头,类型以Error结尾; 处理错误(仅处理一次):
- 特定错误,进行匹配,分别处理
- 可恢复错误,打印并优雅降级处理
- 直接、或包装后返回
处理断言失败的情况;
不要panic;
仅在不可恢复的错误时,使用panic;
使用atomic包;
使用依赖注入,而不是全局变量;
阻止在pub结构体中内嵌类型,会暴露实现细节,阻碍结构体调整;私有,并使用代理模式; 内嵌的不可导出的类型中,含有可导出字段,也会暴露出来; 内嵌结构也比内嵌结构体好,但依旧暴露实现细节; 内嵌的问题:对内嵌类型的修改,都是breaking change;
阻止使用go内建的名称:error,string之类的;
尽可能不使用init()函数,如果要使用,应该:
- 不能依赖其他
init()函数 - 不操作全局变量或环境变量?
- 不进行IO操作 如下情况也许适合:
- 复杂的计算
- 可拔插的钩子
在main函数退出(而不是其他函数):
使用os.Exit()或log.Fatal*立刻退出;
从其他函数退出程序,有如下缺点:控制流程不清晰;难以测试;跳过了清理工作;
尽可能的只退出一次;
在序列化或反序列化结构体时,使用字段tag;避免修改字段名,导致break;
对goroutine的生命周期进行管理,避免协程泄漏;
- 可预期的时间内,协程停止运行
- 有一种方式,使得协程停止运行
- 等待goroutine退出:
sync.WaitGroup或close(chan) init()函数内不要产生goroutine
0.2 · 性能篇
基础数据与string之间的转换,strconv比fmt包快;
阻止重复的将字符串转换成字节数组的操作,转换就会分配内存;
预先指定容器大小,避免扩容操作;
0.3 · 风格篇
避免代码行过长,建议99个字符;
风格保持一致性:易维护、易理解、低心智成本,易迁移或更新;
相似的声明,成组;
import 分组,排序:标准库,其他
包名:
- 全部小写,无大写字母和下划线
- 尽量不重命名包名
- 简短、简明
- 不要复数
- 不要信息量很小的包名:common, util, lib 等 https://go.dev/blog/package-names https://rakyll.org/style-packages
函数名:MixedCaps
导入包别名:仅在导入路径最后一个元素不符合包名的情况下使用; 其他情况,除非冲突,否则不使用别名;
函数分组及排序:
- 粗略的以调用顺序排序
- 以接收器分组 函数在结构体、常量、变量后;工厂模式函数紧跟着类型,但在其他方法之前; 无接收器函数在最后;
尽可能减少嵌套:先处理错误、特殊情况,尽早返回或continue循环;
减少不必要的else;
顶层变量声名:不需要类型,仅在非想要的类型时使用;
不可导出的全局变量/常量,以下划线开头:避免在不同文件之间,意外使用错误;
结构体内嵌: 被内嵌的结构体,应该放在最上面,且使用空行隔开; 不应该:
- 不应以便利为导向
- 不能使得外部类型更难构造或使用
- 不影响外部类型的零值:如果外部类型零值有用,那么内嵌后应该保持
- 不能给外部类型暴露了无联系的函数或字段
- 不能改变外部类型的类型语意
- 不能暴露外部类型的实现细节
- 不允许用户操作内部类型
- 不能因改变内部类型功能而导致外部类型的使用发生改变
局部变量声名:使用:=短变量声名;
零值时,使用var声明;
nil是一个有效的切片;以长度判断空切片,而不是nil; 但nil不等于空切片,在某些场景表现不一致,比如序列化;
尽可能缩小变量的范围(在不违背减少嵌套的情况下);
避免裸参数,给参数加上注释? 最好基于需要,基于基本类型,使用新类型
使用原生字符串,避免转义;
初始化结构体:
- 使用字段名进行初始化
- 零值的字段,可以省略
- 使用
var进行零值初始化结构体 - 使用
&T{}进行结构体指针初始化
使用make初始化map;
map有固定元素数量时,使用字面量初始化;
格式化字符串:将格式字符串用const申明,便于go vet静态分析?
命名打印风格的函数:函数名以f结尾; https://kuzminva.wordpress.com/2017/11/07/go-vet-printf-family-check
0.4 · 模式篇
表测试驱动 https://go.dev/blog/subtests
Functional Options
0.5 · linting#
golangci-lint