golang 编码规范
待整理…
1 注释
对象声明的注释应该是完整的句子,即使这看起来有点多余. 当提取到doc文档中时,这种方式可以使它们格式化良好. 注释应以所描述事物的名称开头,并以句点结尾
2 Context
大多数使用Context的函数都应将其作为第一个参数
不要将Context成员添加到结构类型, 应该将ctx参数添加到该类型需要传递的每个方法上
3 Crypto Rand
不要使用软件包 math/rand 生成密钥,即使是一次性密钥也是如此。 在没有种子情况下,生成器是完全可预测的。 将time.Nanoseconds()的作为随机生成器的种子,只有几熵(应该是消耗很少的意思)。 相反,请使用 crypto/rand’s Reader,如果需要文本,请打印为十六进制或base64
import (
"crypto/rand"
// "encoding/base64"
// "encoding/hex"
"fmt"
)
func Key() string {
buf := make([]byte, 16)
_, err := rand.Read(buf)
if err != nil {
panic(err) // out of randomness, should never happen
}
return fmt.Sprintf("%x", buf)
// or hex.EncodeToString(buf)
// or base64.StdEncoding.EncodeToString(buf)
}4 声明空切片
声明空切片时,首选
其次
前者声明nil slice值,而后者声明非nil但长度为零。 它们在功能上等同,他们 len和 cap均为零,而零切片是首选的风格。
5 错误字符串
错误字符串不应大写(除非以专有名词或缩写开头)或标点符号结束,因为它们通常是在其他上下文之后打印的。 也就是说,请使用 fmt.Errorf(“something bad”),不要使用 fmt.Errorf(“Something bad”),这样 log.Printf(“Reading %s: %v”, filename, err)格式就不会出现虚假的大写字母中间消息。 这不适用于日志记录,后者是隐式面向行的,并且未在其他消息中合并。
6 Goroutine生命周期
当您生成goroutine时,请清楚何时或是否退出。 Goroutine可以通过阻塞通道的发送或接收来泄漏:即使goroutine的阻塞通道不可访问,垃圾收集器也不会终止goroutine。 即使goroutine不会泄漏,在不再需要它们时仍在飞行中也会导致其他细微且难以诊断的问题。 在关闭的通道上发送紧急消息。 “在不需要结果之后”修改仍在使用的输入仍然会导致数据争用。 将goroutine进行任意长时间的飞行可能会导致不可预测的内存使用情况。 尝试使并发代码足够简单,以使goroutine生存期显而易见。 如果那不可行,请记录goroutine何时以及为何退出。 ## Imports 除避免名称冲突外,避免重命名导入; 好的软件包名称不需要重命名。 发生名称冲突时,最好重命名本地的或特定于项目的导入。 导入包是按组组织排序的,组之间有空白行。标准库软件包始终在第一组中
go imports 将为您做到这一点
6.1 Import Blank
仅出于辅助作用而导入的软件包(使用语法import _“pkg”)应仅在程序的主软件包或需要它们的测试中导入。 ### Import . 由于循环依赖关系,导入 . 形式不能用于要测试的程序包,因此在测试中很有用
7 In-Band Errors
在C和类似语言中,函数通常返回-1这样的值或null来表示错误或没有结果
// Lookup returns the value for key or "" if there is no mapping for key.
func Lookup(key string) string
// Failing to check a for an in-band error value can lead to bugs:
Parse(Lookup(key)) // returns "parse failure for value" instead of "no value for key"Go对多个返回值的支持提供了更好的解决方案。 函数应该返回一个附加值以指示其其他返回值是否有效,而不是要求客户端检查带内错误值。 该返回值可以是错误,也可以是布尔值(不需要说明时)。 它应该是最终的返回值
// Lookup returns the value for key or ok=false if there is no mapping for key.
func Lookup(key string) (value string, ok bool)鼓励使用更健壮和易读的代码:
8 缩进错误流
尝试使普通代码路径保持最小缩进,并缩进错误处理,并首先对其进行处理。 通过允许在视觉上快速扫描正常路径,可以提高代码的可读性。 例如,不要写
应该写:
如果该 if语句具有初始化语句,例如:
那么这可能需要将short变量声明移至其自己的行: ==这样的目的是让业务代码能够直接清晰的看到==
9 首字母缩写
名称中的缩写词或首字母缩写词(例如“ URL”或“ NATO”)具有一致的大小写。 例如,“ URL”应显示为“ URL”或“ url”(如在“ urlPony”或“ URLPony”中一样),而从不显示为“ Url”。 例如:ServeHTTP而不是ServeHttp。 对于具有多个已初始化“单词”的标识符,请使用“ xmlHTTPRequest”或“ XMLHTTPRequest”。 当“ ID”是“ identifier”的缩写时,该规则也适用于“ ID”(这在几乎所有情况下都不是“ ego”,“ superego”中的“ id”),因此请写上“ appID”而不是“ appId”。 协议缓冲区编译器生成的代码不受此规则约束。 人工编写的代码要比机器编写的代码具有更高的标准。
10 接口(Interfaces)
Go接口通常属于使用接口类型的值的包中,而不是实现这些值的包中。 实现包应返回具体的(通常是指针或结构)类型:这样,可以将新方法添加到实现中,而无需进行大量重构。 不要在“用于模拟”的API的实现方定义接口; 而是设计API,以便可以使用实际实现的公共API对其进行测试。 在使用接口之前,不要先定义它们:如果没有实际的用法示例,很难知道接口是否是必需的,更不用说它应该包含什么方法了。 ### 接口命名 根据命名规则,一种方法的接口,需要在名称后面加上 -er 的后缀,或者通过代理名词的方式来进行修饰:Reader, Writer,Formatter,CloseNotifier 等
这里最麻烦的是,你的一个接口有多个方法的时候,按照这个方式命名,就不总是很清晰明了。那是否要把结构拆分,变成一个接口对应一个方法呢?我觉得这个取决具体的使用场景了。 接口我的理解: 按理解来说应该名字倾向于一种功能的名字, 当然也可以是一种东西的名字. 一种类型一样的感觉
- 命名规则基本和上面的结构体类型
- 单个函数的结构名以 “er” 作为后缀,例如 Reader , Writer
- 两个函数的接口名综合两个函数名
- 三个以上函数的接口名,类似于结构体名
11 bool 类型变量命名
名称应以 Has, Is, Can 或 Allow 开头
12 常量命名
常量均需使用全部大写字母组成,并使用下划线分词
13 行长度
Go代码中没有严格的行长度限制,但要避免行太长。 同样,请勿添加换行符以使行在可读性更强的情况下保持较短(例如,如果它们是重复的)。 在大多数情况下,人们“不自然地”换行(在函数调用或函数声明的中间,或多或少,例如,尽管周围有一些例外情况),如果参数和简短的变量名数量合理,则不需要换行。长行似乎包含长名,而摆脱长名会很有帮助。 换句话说,换行是因为您所写内容的语义(作为一般规则),而不是因为行长。 如果发现行太长,则更改名称或语义,可能会得到不错的结果。 实际上,这和关于功能应该写多长的建议是完全相同的。 没有强制的规则“一个函数的长度不能超过N行”,但是肯定存在这样一个问题,即函数太长或者小函数过于频繁,解决方案是重新划分函数的功能,而不是开始数行数。 ## Named Result Parameters (命名返回值参数)
考虑一下godoc中的表现。命名结果参数如下:
会在godoc中显得繁杂;更好的表现如下:
- 例外 另一方面,在某些情况下,如果函数返回两个或三个相同类型的参数, 或者如果从上下文中不清楚结果的含义,则添加名称可能很有用。不要仅仅为了避免在内部声明var而命名结果参数; 这就以不必要的API冗长为代价,牺牲了实现的简短性。
func (f *Foo) Location() (float64, float64, error)
// 不如用下面的好点
// Location returns f's latitude and longitude.
// Negative values mean south and west, respectively.
func (f *Foo) Location() (lat, long float64, err error)如果函数只有几行,则可以使用裸返回。 一旦成为通用的函数,请明确说明您的返回值。 结论:为了使您可以使用裸返回而命名结果参数是不值得的。文档的清晰性始终比在函数中保存一两行更为重要
14 包注释
像godoc提出的所有注释一样,包注释必须出现在package子句的旁边,且不能有空行
// Package math provides basic constants and mathematical functions.
package math
/*
Package template implements data-driven templates for generating textual
output such as HTML.
....
*/
package template对于“ package main”注释,在二进制名称之后可以使用其他样式的注释(如果使用二进制格式,则可以大写),例如,对于在seedgen包中的package main包,您可以编写
// Binary seedgen ...
package main
// Command seedgen ...
package main
// Program seedgen ...
package main
// The seedgen program ...
package main
// 都是可以的这些示例,以及这些的明智的变体是可以接受的。 请注意,包注释以小写单词开头的句子是不可可接受的,因为它们是公开可见的 应该用适当的英语写成,包括首字母大写的句子。当二进制名称是第一个单词时,将其大写即使它与拼写不完全匹配命令行调用也是必需的 ## 包名 您对包中所有名称的引用都将使用包名完成,因此您可以从标识符中省略该名称。例如,如果您在庞大的包中,您不需要输入ChubbyFile,客户端将其输入为 chubby.ChubbyFile。 而是命名类型 File,客户端将其写为 chubby.File。 避免使用无意义的程序包名称,例如util,common,misc,api,types 和interfaces。 请参阅 http://golang.org/doc/effective_go.html#package-names 和 http://blog.golang.org/package-names 以获得更多信息。
保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,尽量和标准库不要冲突。包名应该为小写单词,不要使用下划线或者混合大小写 ## 文件命名 尽量采取有意义的文件名,简短,有意义,应该为小写单词,使用下划线分隔各个单词 ## 结构体命名
- 采用驼峰命名法,首字母根据访问控制大写或者小写 ## 传递值 不要为了节省一些字节,将指针作为函数参数传递。 如果函数整个过程中都将 x 仅作为 x 使用,则自变量不应是指针。常见的实例包括传递指向字符串( string的指针 )或指向接口值的指针( *io.Reader)。在这两种情况下,值本身都是固定大小,可以直接传递。这个建议不适用于大型结构,甚至不适用于可能增长的小型结构。
15 方法接收者命名
方法的接收者的名称应反映其身份。 通常,其类型的一个或两个字母缩写就足够了(例如,“客户”是“ c”或“ cl”)。 不要使用通用名称,例如“ me”,“ this”或“ self”,这是面向对象语言的典型标识符,这些标识符赋予该方法特殊的含义。 在Go中,方法的接收者只是另一个参数,因此应相应地命名。 该名称不必像方法参数那样具有描述性,因为它的作用是显而易见的,没有任何文档目的。 它可能很短,因为它将出现在该类型的每种方法的几乎每一行上;熟悉承认简洁。也要保持一致:如果在一种方法中将接收器称为“ c”,则在另一种方法中请勿将其称为“ cl”。
- 使用一个字母或2个字母即可
16 方法接收者类型
对于go初学者,接受者的类型如果不清楚,统一采用指针型
选择在方法上使用值接收器还是指针接收器可能很困难,特别是对于新的Go程序员而言。 如果有疑问,请使用指针,但是有时出于效率的原因(例如,小的不变结构或基本类型的值),值接收器才有意义。 一些有用的准则:
- 如果接收方是map,func或channel,请不要使用指向它们的指针。 如果接收方是切片,并且该方法未重新切片或重新分配切片,请不要使用指向该切片的指针。
package main
import (
"fmt"
)
type mp map[string]string
func (m mp) Set(k, v string) {
m[k] = v
}
func main() {
m := make(mp)
m.Set("k", "v")
fmt.Println(m)
}//Channel
package main
import (
"fmt"
)
type ch chan interface{}
func (c ch) Push(i interface{}) {
c <- i
}
func (c ch) Pop() interface{} {
return <-c
}
func main() {
c := make(ch, 1)
c.Push("i")
fmt.Println(c.Pop())
}- 如果需要对slice进行修改,通过返回值的方式重新赋值
//Slice
package main
import (
"fmt"
)
type slice []byte
func main() {
s := make(slice, 0)
s = s.addOne(42)
fmt.Println(s)
}
func (s slice) addOne(b byte) []byte {
return append(s, b)
}- 如果该方法需要更改接收者,则接收者必须是指针。
- 如果接收者是包含sync.Mutex或类似同步字段的结构,则接收者必须是一个指针以避免复制。
- 如果接收者是大型结构或数组,则指针接收者效率更高。多大是大型?假设这等效于将其所有元素作为参数传递给方法。如果感觉太大,则对于接收者来说也太大。
如果接收者是大的结构体或者数组,使用指针传递会更有效率
package main
import (
"fmt"
)
type T struct {
data [1024]byte
}
func (t *T) Get() byte {
return t.data[0]
}
func main() {
t := new(T)
fmt.Println(t.Get())
}- 函数或方法(同时执行或从该方法调用时)是否会使接收者发生变化? 值类型在调用方法时创建接收者的副本,因此外部更新将不会应用于该接收者。 如果更改必须在原始接收者中可见,则接收者必须是指针。
- 如果接收者是结构体,数组或切片,并且其任何元素都是指向可能正在修改的对象的指针,则最好使用指针接收者,因为它会使读者更加清楚意图。
- 如果接收者是一个很小的数组或结构,自然是一个值类型(例如,诸如time.Time类型),没有可变字段且没有指针,或者仅仅是一个简单的基本类型(如int或string),值接收者是合理的。 值接收者可以减少可以生成的垃圾数量; 如果将值传递给值方法,则可以使用堆栈上的副本而不是在堆上分配。 (编译器会尽量避免这种分配,但是它不可能总是成功。)由于这个原因,请勿在没有进行概要分析的情况下选择值类型接收者。
- 最后,如有疑问,请使用指针接收者。 ## 变量名 Go中的变量名应该简短而不是冗长。 对于范围有限的局部变量尤其如此。 比起 lineCount 更倾向于 c。比起 sliceIndex 更倾向于 i。
17 代码分析
- 静态分析我们的源码存在的各种问题,例如多余的代码,提前return的逻辑,struct的tag是否符合标准等。