本章目录:

0x00 前言简述

0x01 Go开发规范

// 注释文本/* 注释文本 */

0x02 指导原则

init()Exittime.Timetime.Duration


0x03 规范辅助工具

  • Linting 相关工具


0x00 前言简述

通过前面的Go语言基础学习告一段落,本章主要对 Go 语言开发规范进行记录与实践, 便于养成良好的开发习惯也可叫做规则(不至于进入一些大厂而因为开发习惯没养成而痛苦),规则的存在是为了使代码库易于管理,同时仍然允许工程师更有效地使用 Go 语言功能.

golintgo vet

Go 编程语言规范 (https://golang.org/ref/spec) 版本 Jul 26, 2021

Go 的通用准则可查看官方提供的参考指南:

  1. Effective Go

  2. Go Common Mistakes

  3. Go Code Review Comments

第三方公司Go开发规范参考: https://github.com/uber-go/

0x01 Go开发规范


命名规范
被包外部引用被包内部调用


目录&package 包命名

尽量保持package的名字和目录一致,采取有意义的包(简短而简洁)名,包名使用小写,不要使用下划线和大写字母,不用复数,例如


.go 文件命名
_test.go



constant-常亮命名

全部大写,并以_分割



variable-变量命名

一般为驼峰命名,遵循以下规则

  • 变量为私有,首字母小写

  • 变量为公有。首字母大写

  • 单词为特有名次,而且是首个单词,则特有名词小写

若变量为布尔类型,则名称一般以"Has"、"Is"、"Can"、"Allow"开头



function-函数命名

采用驼峰命名法,注意特殊的匿名函数,以及单元测试函数。



struct-结构体命名

采用驼峰命名法,struct 声明和初始化用多行,特别注意匿名结构体。


interface-接口命名

命名规范基本和结构体一致, 但是单个函数习惯以"er"为后缀。



注释规范

Go语言中注释符号如下:

单行: 多行: 

Tips: 多行注释中可以嵌套单行注释。

//// 注释文本3
Packagedoc.go文件名的字母数序
Package


注释使用的范围:

  • 包注释

  • 接口注释

  • 方法注释

  • 代码逻辑注释

注释示例:


Tips :注释中的URL将会变成HTML链接。


样式规范


缩进与括号
;Tab



代码一致性

一致性的代码更容易维护、是更合理的、需要更少的学习成本、并且随着新的约定出现或者出现错误后更容易迁移、更新、修复 bug

相反,在一个代码库中包含多个完全不同或冲突的代码风格会导致维护成本开销、不确定性和认知偏差。所有这些都会直接导致速度降低、代码审查痛苦、而且增加 bug 数量。

将这些标准应用于代码库时,建议在 package(或更大)级别进行更改,子包级别的应用程序通过将多个样式引入到同一代码中,违反了上述关注点。



Import (包导入)

引入多个包时,按照三中类型区分,标准包,程序内部包,第三方包,建议写的时候有顺序的导入你的包。

默认情况下,这是 goimports 应用的分组


导入别名

如果程序包名称与导入路径的最后一个元素不匹配,则必须使用导入别名。


(1) 函数分组与顺序

在进行Go语言时函数应按粗略的调用顺序排序,同一文件中的函数应按接收者分组。

struct, const, var
newXYZ()/NewXYZ()

由于函数是按接收者分组的,因此普通工具函数应在文件末尾出现。


(2) 减少不必要的嵌套以及else

处理错误情况/特殊情况

如果在 if 的两个分支中都设置了变量,则可以将其替换为单个 if。

WeiyiGeek.减少不必要的嵌套以及else



0x02 指导原则


Variable (变量)


顶层变量声明
var


类型不完全匹配,请指定类型



对于未导出的顶层常量和变量,使用_作为前缀
varsconsts
err

基本依据:顶级变量和常量具有包范围作用域,使用通用名称可能很容易在其他文件中意外使用错误的值。


本地变量声明
:=s := "foo"
var



缩小变量作用域

描述: 如果有可能,尽量缩小变量作用范围,除非它与 减少嵌套 的规则冲突。

如果需要在 if 之外使用函数调用的结果,则不应尝试缩小范围。



避免可变全局变量

描述: 使用选择依赖注入方式避免改变全局变量,既适用于函数指针又适用于其他值类型。


避免使用内置名称

在Go语言规范概述了几个内置的 ,不应在Go项目中使用的名称标识(Go 编程语言规范 - go.dev)

例如:


根据上下文的不同,将这些标识符作为名称重复使用,将在当前作用域(或任何嵌套作用域)中隐藏原始标识符,或者混淆代码。

在最好的情况下,编译器会报错;在最坏的情况下,这样的代码可能会引入潜在的、难以恢复的错误。


go gofmt 与go vet



使用原始字符串字面值,避免转义

描述: Go 支持使用 原始字符串字面值,也就是 " ` " 来表示原生字符串,在需要转义的场景下,我们应该尽量使用这种方案来替换。

例如,可以跨越多行并包含引号。使用这些字符串可以避免更难阅读的手工转义的字符串。



Struct (结构体)使用字段名初始化结构体
go vet

例外:如果有 3 个或更少的字段,则可以在测试表中省略字段名称。


省略结构中的零值字段

描述: 初始化具有字段名的结构时,除非提供有意义的上下文,否则忽略值为零的字段。
也就是,让我们自动将这些设置为零值,这有助于通过省略该上下文中的默认值来减少阅读的障碍,只指定有意义的值。


在字段名提供有意义上下文的地方包含零值。例如,表驱动测试 中的测试用例可以受益于字段的名称,即使它们是零值的。


var
varvar user User

这将零值结构与那些具有类似于为[初始化 Maps]创建的,区别于非零值字段的结构区分开来,并与我们更喜欢的 declare empty slices 方式相匹配。


初始化 Struct 引用

&T{}new(T)



结构体中的嵌入
例如 mutex

内嵌应该提供切实的好处,比如以语义上合适的方式添加或增强功能,它应该在对用户不利影响的情况下完成这项工作。

结构体中的嵌入不应该是以下几个方面:

  • 纯粹是为了美观或方便。

  • 使外部类型更难构造或使用。

  • 影响外部类型的零值。如果外部类型有一个有用的零值,则在嵌入内部类型之后应该仍然有一个有用的零值。

  • 作为嵌入内部类型的副作用,从外部类型公开不相关的函数或字段。

  • 公开未导出的类型。

  • 影响外部类型的复制形式。

  • 更改外部类型的API或类型语义。

  • 嵌入内部类型的非规范形式。

  • 公开外部类型的实现详细信息。

  • 允许用户观察或控制类型内部。

  • 通过包装的方式改变内部函数的一般行为,这种包装方式会给用户带来一些意料之外情况。


someno
WeiyiGeek.结构体嵌入


避免在公共结构中嵌入类型

描述: 嵌入的类型泄漏实现细节、禁止类型演化和模糊的文档。

AbstractListAbstractList

相反,只需手动将方法写入具体的列表,该列表将委托给抽象列表。


推荐操作:


Go 允许 类型嵌入 作为继承和组合之间的折衷,外部类型获取嵌入类型的方法的隐式副本。

默认情况下,这些方法委托给嵌入实例的同一方法,结构还获得与类型同名的字段,所以,如果嵌入的类型是 public,那么字段是 public。

为了保持向后兼容性,外部类型的每个未来版本都必须保留嵌入类型, 很少需要嵌入类型,这是一种方便,可以帮助您避免编写冗长的委托方法。

即使嵌入兼容的抽象列表 interface,而不是结构体,这将为开发人员提供更大的灵活性来改变未来,但仍然泄露了具体列表使用抽象实现的细节。


无论是使用嵌入式结构还是使用嵌入式接口,嵌入式类型都会限制类型的演化.

  • 向嵌入式接口添加方法是一个破坏性的改变。

  • 删除嵌入类型是一个破坏性的改变。

  • 即使使用满足相同接口的替代方法替换嵌入类型,也是一个破坏性的改变。

尽管编写这些委托方法是乏味的,但是额外的工作隐藏了实现细节,留下了更多的更改机会,还消除了在文档中发现完整列表接口的间接性操作。



功能选项

功能选项是一种模式,您可以在其中声明一个不透明 Option 类型,该类型在某些内部结构中记录信息。您接受这些选项的可变编号,并根据内部结构上的选项记录的全部信息采取行动。

将此模式用于您需要扩展的构造函数和其他公共 API 中的可选参数,尤其是在这些功能上已经具有三个或更多参数的情况下。


Optionoptions


fmt.Stringer



Function (函数)避免使用 

在Go语言开发应该避免使用init(),当必须要使用其时,代码应先尝试:

  1. 无论程序环境或调用如何,都要完全确定。

  2. 避免依赖于其他init()函数的顺序或副作用。虽然init()顺序是明确的,但代码可以更改,因此init()函数之间的关系可能会使代码变得脆弱和容易出错。

  3. 避免访问或操作全局或环境状态,如机器信息、环境变量、工作目录、程序参数/输入等。

  4. 避免I/O,包括文件系统、网络和系统调用。

main()main()

特别是,打算由其他程序使用的库应该特别注意完全确定性,而不是执行“init magic”

WeiyiGeek.避免使用init
init()
database/sql



避免参数语义不明确(Avoid Naked Parameters)
意义不明确的参数/* ... */
booltrue/false



优雅退出方式 
os.Exitlog.Fatal*panic
main()os.Exitlog.Fatal*


原则上:退出的具有多种功能的程序存在一些问题:

go testdefer
main()os.Exitlog.Fatal
main()


Interface (接口)1.指向 interface 的指针

您几乎不需要指向接口类型的指针,您应该将接口作为值进行传递,在这样的传递过程中,实质上传递的底层数据仍然可以是指针。

接口实质上在底层用两个字段表示:

  • 一个指向某些特定类型信息的指针,您可以将其视为"type"。

  • 数据指针。如果存储的数据是指针,则直接存储。如果存储的数据是一个值,则存储指向该值的指针。

如果希望接口方法修改基础数据,则必须使用指针传递(将对象指针赋值给接口变量)。



2.Interface 合理性验证

在编译时验证接口的符合性。这包括:

  • 将实现特定接口的导出类型作为接口API 的一部分进行检查

  • 实现同一接口的(导出和非导出)类型属于实现类型的集合

  • 任何违反接口合理性检查的场景,都会终止编译,并通知给用户

补充: 上面3条是编译器对接口的检查机制,大体意思是错误使用接口会在编译期报错.所以可以利用这个机制让部分问题在编译期暴露。

WeiyiGeek.Bad&Good
*Handlerhttp.Handlervar _ http.Handler = (*Handler)(nil)
*Handlernil



3.接口与接收器 (receiver)

使用值接收器的方法既可以通过值调用,也可以通过指针调用。

带指针接收器的方法只能通过指针或 addressable values调用.

例如:


类似的,即使方法有了值接收器,也同样可以用指针接收器来满足接口.


Effective Go 中有一段关于 pointers vs. values 的精彩讲解.

值接收器方法集

接口的匹配(或者叫实现), 类型实现了接口的所有方法叫匹配;具体的讲,要么是类型的值方法集匹配接口,要么是指针方法集匹配接口

具体的匹配分两种:

  • 值方法集和接口匹配: 给接口变量赋值的不管是值还是指针对象,都ok,因为都包含值方法集.

  • 指针方法集和接口匹配: 只能将指针对象赋值给接口变量,因为只有指针方法集和接口匹配.

如果将值对象赋值给接口变量,会在编译期报错(会触发接口合理性检查机制)
为啥 i = s2Val 会报错,因为值方法集和接口不匹配,必须要指针方法集才匹配.



Slices 或 Maps (切片和字典)


初始化 Maps
make(..)


Tips: 所以,在尽可能的情况下,请在初始化时提供 map 容量大小,详细请看 指定Map容量提示。

map literals(map 初始化列表)


make


在边界处拷贝 Slices 和 Maps

描述: slices 和 maps 包含了指向底层数据的指针,因此在需要复制它们时要特别注意。

接收 Slices 和 Maps

当 map 或 slice 作为函数参数传入时,如果您存储了对它们的引用,则用户可以对其进行修改。


map 或 slice 的修改

同样请注意用户对暴露内部状态的 map 或 slice 的修改。



追加时优先指定切片容量
make()增加执行效率



nil 是一个有效的 slice
nilnil
len(s) == 0nil
varmake()


一个为nil,另一个不是



Defer (资源释放)

描述: 在Go语言中,常常使用 defer 释放资源,诸如文件和锁。

Defer 的开销非常小,只有在您可以证明函数执行时间处于纳秒级的程度时,才应避免这样做,使用 defer 提升可读性是值得的,因为使用它们的成本微不足道。

defer
Sync (同步包)
sync.Mutexsync.RWMutexmutex


结构体指针
私有结构体类型



ErrorHandling (错误处理)

Go 中有多种声明错误(Error) 的选项:

errors.Newfmt.ErrorfError()"pkg/errors".Wrap

返回错误时,请考虑以下因素以确定最佳选择:

这是一个不需要额外信息的简单错误吗?

errors.New

客户需要检测并处理此错误吗?

Error()

您是否正在传播下游函数返回的错误?

如果是这样,请查看本文后面有关错误包装 section on error wrapping 部分的内容。

fmt.Errorf

Tips: 错误处理原则是不能丢弃有返回err的调用,不能用_丢弃,必须全部处理尽早 return,采用独立的错误流处理。


1.错误声明 (Error Declare)
errors.New


Error()


最好公开匹配器功能以检查错误


2.错误包装 (Error Wrapping)

一个(函数/方法)调用失败时,有三种主要的错误传播方式:

"pkg/errors".Wrap"pkg/errors".Causefmt.Errorf

建议在可能的地方添加上下文,以使您获得诸如“调用服务 foo:连接被拒绝”之类的更有用的错误,而不是诸如“连接被拒绝”之类的模糊错误。

避免使用“failed to”之类的短语


err

另请参见 Don't just check errors, handle them gracefully. 不要只是检查错误,要优雅地处理错误



3.处理类型断言失败
“comma ok”



4.避免使用 panic

描述: 在生产环境中运行的代码必须避免出现 panic。

panic 是 cascading failures 级联失败的主要根源 ,如果发生错误,该函数必须返回错误,并允许调用方决定如何处理它。


panic/recover

程序初始化是一个例外:程序启动时应使程序中止的不良情况可能会引起 panic。


t.Fatalt.FailNow



5.go.uber.org/atomic
int32int64
atomic.Bool



Channel (通道)

描述: channel 通常 size 应为 1 或是无缓冲的。

默认情况下,channel 是无缓冲的,其 size 为零,任何其他尺寸都必须经过严格的审查。我们需要考虑如何确定大小,考虑是什么阻止了 channel 在高负载下和阻塞写时的写入,以及当这种情况发生时系统逻辑有哪些变化。

(翻译解释:按照原文意思是需要界定通道边界,竞态条件,以及逻辑上下文梳理)

iota (枚举)

描述: 在 Go 中引入枚举的标准方法是声明一个自定义类型和一个使用了 iota 的 const 组。由于变量的默认值为 0,因此通常应以非零值开头枚举。


在某些情况下,使用零值是有意义的(枚举从零开始),例如,当零值是理想的默认行为时。



Unit (单元测试)
_test.go以Test开头后接测试函数名(注意首字母大写)以Benchmark开头后接函数名以Example开头后接函数名称


组测试

当测试逻辑是重复的时候,通过 subtests 使用 table 驱动的方式编写 case 代码看上去会更简洁。


很明显,使用 test table 的方式在代码逻辑扩展的时候,比如新增 test case,都会显得更加的清晰。

teststt
givewant



Performance (性能提升)

描述: 性能方面的特定准则只适用于高频场景。


字符串类型转换

优先使用 strconv 而不是 fmt,将原语转换为字符串或从字符串转换时,strconv速度比fmt快。



避免字符串到字节的转换

描述: 不要反复从固定字符串创建字节 slice, 相反请执行一次转换并捕获结果。



指定容器容量

描述: 尽可能指定容器容量,以便为容器预先分配内存,这将在添加元素时最小化后续分配(通过复制和调整容器大小)。

指定Map容量:

make(map[T1]T2, hint)make()

注意,与slices不同。map capacity提示并不保证完全的抢占式分配,而是用于估计所需的hashmap bucket的数量。
因此,在将元素添加到map时,甚至在指定map容量时,仍可能发生分配。

例如:


指定切片容量:

make()make([]T, length, capacity)
slice capacitymake()append()



Time (时间处理)
time
一天有 24 小时、一小时有 60 分钟、一周有七天、一年 365 天

例如,1 表示在一个时间点上加上 24 小时并不总是产生一个新的日历日。

Tips : Go 语言的格式化字符串为"2006-01-02 15:04:06"


使用  表达瞬时时间
time.Timetime.Time



使用  表达时间段

先看示例:


回到第一个例子,在一个时间瞬间加上 24 小时,我们用于添加时间的方法取决于意图。

Time.AddDateTime.Add


time.Durationtime.Time
flagtime.ParseDurationtime.Durationencoding/jsonUnmarshalJSONtime.Timedatabase/sqlDATETIMETIMESTAMPtime.Timegopkg.in/yaml.v2time.Timetime.ParseDurationtime.Duration
time.Durationintfloat64
encoding/jsontime.Duration


time.Timestring
Time.UnmarshalTexttime.RFC3339Time.Formattime.Parse
"time"

如果您比较两个时间瞬间,则差异将不包括这两个瞬间之间可能发生的闰秒。



String (字符串处理)


字符串 string format 变量
Printfconstgo vet


命名 Printf 样式的函数

Printfgo vet
WrapfWrapgo vetf


0x03 规范辅助工具


Linting 相关工具

比任何 "blessed" linter 集更重要的是,lint在一个代码库中始终保持一致。

我们建议至少使用以下linters,因为我认为它们有助于发现最常见的问题,并在不需要规定的情况下为代码质量建立一个高标准:

  • errcheck 以确保错误得到处理

  • goimports 格式化代码和管理 imports

  • golint 指出常见的文体错误

  • govet 分析代码中的常见错误

  • staticcheck 各种静态分析检查


Lint Runners

描述: 我们推荐 golangci-lint 作为go-to lint的运行程序,这主要是因为它在较大的代码库中的性能以及能够同时配置和使用许多规范。这个repo有一个示例配置文件.golangci.yml和推荐的linter设置。

golangci-lint 有various-linters可供使用。建议将上述linters作为基本set,我们鼓励团队添加对他们的项目有意义的任何附加linters。


欢迎各位志同道合的朋友一起学习交流,如文章有误请在下方留下您宝贵的经验知识,个人邮箱地址【master#weiyigeek.top】 


专栏书写不易,如果你觉得这个专栏还不错的,请给这篇专栏点个赞、投个币、收个藏、关个注,转个发,这将对我的肯定,谢谢!。

fmt.Printf("不要白嫖哟,亲!")