Skip to main content

style-guide

📅 2026-02-05 ✏️ 2026-03-06 CS GO
No related notes

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#

style

0.1 · 指导篇

不需要指向接口的指针:接口底层数据是个指针; 指针类型实现接口的方法,可以修改类型数据;

适当的,在编译期,验证类型是否实现接口;

// 零值赋值给指针:
// 1.指针类型
var _ YourInterface = (*YourType)(nil)
// 2.非指针类型
var _ YourInterface = YourType{}

避免对接口的修改,影响用户(可能使用了对应类型);

方法的接收者:指针可以调用作为接收者的方法,反之不行; 实现接口:指针作为接收者实现接口,则指针实现接口,而值未实现接口

零值mutex是有效的; 不要内嵌mutex,暴露不需要的方法;

复制slicemap注意:两者都是底层数据含指针,指向真实数据;

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的生命周期进行管理,避免协程泄漏;

  • 可预期的时间内,协程停止运行
  • 有一种方式,使得协程停止运行
  1. 等待goroutine退出:sync.WaitGroupclose(chan)
  2. init()函数内不要产生goroutine

0.2 · 性能篇

基础数据与string之间的转换,strconvfmt包快;

阻止重复的将字符串转换成字节数组的操作,转换就会分配内存;

预先指定容器大小,避免扩容操作;

0.3 · 风格篇

避免代码行过长,建议99个字符;

风格保持一致性:易维护、易理解、低心智成本,易迁移或更新;

相似的声明,成组;

import 分组,排序:标准库,其他

包名:

函数名: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