文章目录
- 开发篇
- 3.1 包
- 第三方包管理
- 3.2 魔鬼数字
- 3.3 常量 & 枚举
- 3.4 结构体
- 3.5 运算符
- 3.6 函数
- 3.7 参数
- 3.8 返回值
- 3.9 注释
- 通用注释要求
- 包注释要求
- 函数与方法注释要求
- 变量和常量的注释要求
- 编码注释
- Bug的注释
- 带mutex的struct必须是指针receivers
- recieved是值类型还是指针类型
- 其他注释要求
- 3.10 错误
- 3.11 其他
- 参数传递
- 自定义类型的string循环问题
- panic
- 在逻辑处理中禁用panic
- 注意闭包的调用
开发篇
说明:本篇主要是讲解开发中各个环节的开发规范和对一些代码的优化写法。在本文中有一些特别标黄的建议,我真的建议你好好看看那些代码,因为那可能对你提高代码开发会很有帮助。
3.1 包
第三方包管理
【建议3.1.1】项目仓库中包含全量的代码
说明:将依赖源码都放到当前工程的vendor目录下,将全量的代码保存到项目仓库中,这样做有利于避免受第三方变动的影响。
【建议3.1.2】建议采用 Glide 来管理第三方包
第三方包应该尽量获取release版本,而非master分支的版本。master上的版本通常是正在开发的非稳定版本
3.2 魔鬼数字
【规则3.2】代码中禁止使用魔鬼数字。
错误示例:
修改后:
说明:直接使用数字,造成代码难以理解,也难以维护。应采用有意义的静态变量或枚举来代替。
例外情况:有些特殊情况下,如循环或比较时采用数字0,-1,1,这些情况可采用数字。
3.3 常量 & 枚举
【建议3.3.1】 为整数常量添加 String() 方法
如果你利用 iota 来使用自定义的整数枚举类型,务必要为其添加 String() 方法。例如,像这样:
type State int
const
(
Running State = iota
Stopped
Rebooting
Terminated
)
如果你创建了这个类型的一个变量,然后输出,会得到一个整数(http://play.golang.org/p/V5VVFB05HB):
func main() {
state := Running
// print: "state 0"
fmt.Println("state ", state)
}
除非你回顾常量定义,否则这里的0看起来毫无意义。只需要为State类型添加String()方法就可以修复这个问题(http://play.golang.org/p/ewMKl6K302):
func (s State) String() string {
switchs {
caseRunning:
return"Running"
caseStopped:
return"Stopped"
caseRebooting:
return"Rebooting"
caseTerminated:
return"Terminated"
default:
return"Unknown"
}
}
新的输出是:state: Running。显然现在看起来可读性好了很多。在你调试程序的时候,这会带来更多的便利。同时还可以在实现 MarshalJSON()、UnmarshalJSON() 这类方法的时候使用同样的手段。
【建议3.3.2】让 iota 从 a +1 开始增量
在前面的例子中同时也产生了一个我已经遇到过许多次的 bug。假设你有一个新的结构体,有一个State字段:
type T struct{
Name string
Port int
State State
}
现在如果基于 T 创建一个新的变量,然后输出,你会得到奇怪的结果(http://play.golang.org/p/LPG2RF3y39):
func main() {
t := T{Name:"example", Port: 6666}
// prints: "t {Name:example Port:6666 State:Running}"
fmt.Printf("t %+v\n", t)
}
看到 bug 了吗?State
字段没有初始化,Go 默认使用对应类型的零值进行填充。由于State
是一个整数,零值也就是0
,但在我们的例子中它表示Running
。
那么如何知道 State 被初始化了?还是它真得是在Running
模式?没有办法区分它们,那么这就会产生未知的、不可预测的 bug。不过,修复这个很容易,只要让 iota 从 +1 开始(http://play.golang.org/p/VyAq-3OItv):
const
(
Running State = iota + 1
Stopped
Rebooting
Terminated
)
现在t变量将默认输出Unknown,不是吗? ? :
func main() {
t := T{Name:"example", Port: 6666}
// 输出: "t {Name:example Port:6666 State:Unknown}"
fmt.Printf("t %+v\n", t)
}
不过让 iota 从零值开始也是一种解决办法。例如,你可以引入一个新的状态叫做Unknown
,将其修改为:
const
(
Unknown State = iota
Running
Stopped
Rebooting
Terminated
)
3.4 结构体
【规则3.4.1】对于要使用json转换的结构体代码,变量名必须为大写,否则你只会得到一个为空的对象
例如:
BaiduNewsItem struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
PubDate string `xml:"pubDate"`
Author string `xml:"author"`
}
【建议3.4.2】 在初始化结构体时使用带有标签的语法
这是一个无标签语法的例子:
type T struct{
Foo string
Bar int
}
func main() {
t := T{"example", 123}// 无标签语法
fmt.Printf("t %+v\n", t)
}
那么如果你添加一个新的字段到T结构体,代码会编译失败:
type T struct{
Foo string
Bar int
Qux string
}
func main() {
t := T{"example", 123}// 无法编译
fmt.Printf("t %+v\n", t)
}
如果使用了标签语法,Go的兼容性规则(http://golang.org/doc/go1compat)会处理代码。例如在向net包的类型添加叫做Zone的字段,参见:http://golang.org/doc/go1.1#library。回到我们的例子,使用标签语法:
type T struct{
Foo string
Bar int
Qux string
}
func main() {
t := T{Foo:"example", Bar: 123}
fmt.Printf("t %+v\n", t)
}
这个编译起来没问题,而且弹性也好。不论你如何添加其他字段到T结构体。你的代码总是能编译,并且在以后的 Go 的版本也可以保证这一点。只要在代码集中执行go vet,就可以发现所有的无标签的语法。
【建议3.4.3】将结构体的初始化拆分到多行
如果有两个以上的字段,那么就用多行。它会让你的代码更加容易阅读,也就是说不要:
T{Foo: "example", Bar:someLongVariable, Qux:anotherLongVariable, B: forgetToAddThisToo}
而是:
T{
Foo: "example",
Bar: someLongVariable,
Qux: anotherLongVariable,
B: forgetToAddThisToo,
}
这有许多好处,首先它容易阅读,其次它使得允许或屏蔽字段初始化变得容易(只要注释或删除它们),最后添加其他字段也更容易(只要添加一行)。
3.5 运算符
【规则3.5】运算符前后、逗号后面、if后面等需有单空格隔开。
if err != nil {…}
c := a + b
return {}, err
例外情况:
go fmt认为应该删除空格的场景。例如,在传参时,字符串拼接的”+”号。
3.6 函数
【原则3.6.1】保持函数内部实现的组织粒度是相近的。
举例,不应该出现如下函数:
func main() {
initLog()
//这一段代码的组织粒度,明显与其他的不均衡
orm.DefaultTimeLoc = time.UTC
sqlDriver := beego.AppConfig.String("sqldriver")
dataSource := beego.AppConfig.String("datasource")
modelregister.InitDataBase(sqlDriver, dataSource)
Run()
}
应该改为:
func main() {
initLog()
initORM() //修改后,函数的组织粒度保持一致
Run()
}
【建议3.6.2】 返回函数调用
我已经看过很多代码例如(http://play.golang.org/p/8Rz1EJwFTZ):
func bar() (string, error) {
v, err := foo()
iferr != nil {
return"", err
}
returnv, nil
}
然而,你只需要:
func bar() (string, error) {
return foo()
}
更简单也更容易阅读(当然,除非你要对某些内部的值做一些记录)。
【建议3.6.3】 withContext 封装函数
有时对于函数会有一些重复劳动,例如锁/解锁,初始化一个新的局部上下文,准备初始化变量等等……这里有一个例子:
func foo() {
mu.Lock()
defer mu.Unlock()
// foo 相关的工作
}
func bar() {
mu.Lock()
defer mu.Unlock()
// bar 相关的工作
}
func qux() {
mu.Lock()
defer mu.Unlock()
// qux 相关的工作
}
如果你想要修改某个内容,你需要对所有的都进行修改。如果它是一个常见的任务,那么最好创建一个叫做withContext的函数。这个函数的输入参数是另一个函数,并用调用者提供的上下文来调用它:
func withLockContext(fn func()) {
mu.Lock
defer mu.Unlock()
fn()
}
只需要将之前的函数用这个进行封装:
func foo() {
withLockContext(func() {
// foo 相关工作
})
}
func bar() {
withLockContext(func() {
// bar 相关工作
})
}
func qux() {
withLockContext(func() {
// qux 相关工作
})
}
不要光想着加锁的情形。对此来说最好的用例是数据库链接。现在对 withContext 函数作一些小小的改动:
func withDBContext(fn func(db DB)) error {
// 从连接池获取一个数据库连接
dbConn := NewDB()
returnfn(dbConn)
}
如你所见,它获取一个连接,然后传递给提供的参数,并且在调用函数的时候返回错误。你需要做的只是:
func foo() {
withDBContext(func(db *DB) error {
// foo 相关工作
})
}
func bar() {
withDBContext(func(db *DB) error {
// bar 相关工作
})
}
func qux() {
withDBContext(func(db *DB) error {
// qux 相关工作
})
}
你在考虑一个不同的场景,例如作一些预初始化?没问题,只需要将它们加到withDBContext就可以了。这对于测试也同样有效。
这个方法有个缺陷,它增加了缩进并且更难阅读。再次提示,永远寻找最简单的解决方案。
3.7 参数
【建议3.7.1】参数按逻辑紧密程度安排位置, 同种类型的参数放在相邻位置。
举例:
func(m1, m2 *MenuEntry) bool
func (c *Client) Delete(key string, recursive bool, dir bool) (*RawResponse, error)
【建议3.7.2】避免使用标识参数来控制函数的执行逻辑。
举例:
func doAorB(flag int) {
if flag == flagA {
processA1()
return
}
if flag == flagB {
processB1()
return
}
}
特别是标识为布尔值时,通过标识参数控制函数内的逻辑,true执行这部分逻辑,false执行另外一部分逻辑,说明了函数职责不单一。
【建议3.7.3】参数个数不要超过5个
参数过多通常意味着缺少封装,不易维护,容易出错.
3.8 返回值
【规则3.8.1】函数返回值个数不要超过3个。
【建议3.8.2】如果函数的返回值超过3个,建议将其中关系密切的返回值参数封装成一个结构体。
3.9 注释
Go提供了C风格的块注释/* */和C++风格的行注释//。通常为行注释;块注释大多数作为程序包的注释,但也可以用于一个表达式中,或者用来注释掉一大片代码。
godoc用来处理Go源文件,抽取有关程序包内容的文档。在顶层声明之前出现,并且中间没有换行的注释,会随着声明一起被抽取,作为该项的解释性文本。这些注释的本质和风格决定了godoc所产生文档的质量。
Go代码的注释可以被godocs工具转化为文档发布。所以准确的代码注释除了能够帮助阅读代码还有助于代码手册的生成。
Godoc工具说明可参考如下链接:
https://godoc.org/golang.org/x/tools/cmd/godoc
通用注释要求
【原则3.9.1】编写代码首先考虑如何代码自我解释,然后才是添加注释进行补充说明
说明:优秀的代码不写注释也可轻易读懂,注释无法把糟糕的代码变好,需要很多注释来解释的代码往往存在坏味道,需要重构。
示例:注释不能消除代码的坏味道:
// 判断m是否为素数
// 返回值:: 1是素数,0不是素数
func p(m int) int {
var i, k int
k = sqrt(m)
for i = 2; i <= k; i++ {
if m%i == 0 {
break // 发现整除,表示m不为素数,结束遍历
}
}
// 遍历中没有发现整除的情况,返回
if i > k {
return 1
}
// 遍历中没有发现整除的情况,返回
return 0
}
重构代码后,不需要注释:
// IsPrimeNumber return true if num is prime
func IsPrimeNumber(num int) bool {
var i int
sqrtOfNum := sqrt(num)
for i = 2; i <= sqrtOfNum; i++ {
if num%i == 0 {
return false
}
}
return true
}
【原则3.9.2】注释的内容要清楚、明了,含义准确,防止注释二义性。
说明:有歧义的注释反而会导致维护者更难看懂代码,正如带两块表反而不知道准确时间。
示例:注释与代码相矛盾,注释内容也不清楚,前后矛盾。
// 上报网管时要求故障ID与恢复ID相一致
// 因此在此由告警级别获知是不是恢复ID
// 若是恢复ID则设置为ClearId,否则设置为AlarmId
if ClearAlarmLevel != rcData.level {
SetAlarmID(rcData.AlarmId);
} else {
SetAlarmID(rcData.ClearId);
}
正确做法:修改注释描述如下:
// 网管达成协议:上报故障ID与恢复ID由告警级别确定,若是清除级别,ID设置为ClearId,否则设为AlarmId
...
【原则3.9.3】在代码的功能、意图层次上进行注释,即注释用于解释代码难以直接表达的意图,而不是重复描述代码。
说明:注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。
对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释。
注释不是为了名词解释(what),而是说明用途(why)。
示例:如下注释纯属多余。
i++ // increment i
if receiveFlag { // if receiveFlag is TRUE
...
如下这种无价值的注释不应出现(空洞的笑话,无关紧要的注释)。
// 时间有限,现在是:04,根本来不及想为什么,也没人能帮我说清楚
...
而如下的注释则给出了有用的信息:
//由于xx编号网上问题,在xx情况下,芯片可能存在写错误,此芯片进行写操作后,必须进行回读校验,如果回读不正确,需要再重复写-回读操作,最多重复三次,这样可以解决绝大多数网上应用时的写错误问题
time := 0
for (readReg(someAddr) != value) && (time < 3) {
writeReg(someAddr, value)
time++
}
对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释,出彩的或复杂的代码块前要加注释,如:
// Divide result by two, taking into account that x contains the carry from the add.
for i := 0; i < len(result); i++ {
x = (x << 8) + result[i]
result[i] = x >> 1
x &= 1
}
【规则3.9.4】所有导出对象都需要注释说明其用途;非导出对象根据情况进行注释。必须时,应该说明值的取值范围,及默认值。
【规则3.9.5】注释的单行长度不能超过 80 个字符。
【规则3.9.6】注释需要紧贴对应的包声明和函数之前,不能有空行、
【规则3.9.7】非跨度很长的注释,尽量使用 // 方式。
/*
* 1. 确保 template 存在
*/
改成:
// 1. 确保 template 存在
【规则3.9.8】避免多余的空格,两句注释之间保持一个空格。
示例:
// 采用这样的方式
// Sentence one. Sentence two.
// 而不是如下的方式
// Sentence one. Sentence two.
保持和Go的风格一样,参考https://golang.org/cl/20022
【原则3.9.9】注释第一条语句应该为一条概括语句,并且使用被声明的名字作为开头。
例如:
// Compile parses a regular expression and returns, if successful, a Regexp
// object that can be used to match against text.
func Compile(str string) (regexp *Regexp, err error) {
【建议3.9.10】//与注释的文档之间空一格。
示例:
// 采用如下方式
// This is a comment
// for humans.
//而不要采用如下方式:
//This is a comment
//for humans.
对于Go保留的语法,就不需要空一格
//go:generate go run gen.go
详细的语法可以参考:https://golang.org/cmd/compile/#hdr-Compiler_Directives.
包注释要求
【规则3.9.11】每个程序包都应该有一个包注释,一个位于package子句之前的块注释。
对于有多个文件的程序包,包注释只需要出现在一个文件中,任何一个文件都可以。包注释应该用来介绍该程序包,并且提供与整个程序包相关的信息。它将会首先出现在godoc页面上,并会建立后续的详细文档。
1. /*
Package regexp implements a simple library for regular expressions.
The syntax of the regular expressions accepted is:
regexp:
concatenation { '|' concatenation }
concatenation:
{ closure }
closure:
term [ '*' | '+' | '?' ]
term:
'^'
'$'
'.'
character
'[' [ '^' ] character-ranges ']'
'(' regexp ')'
*/
package regexp
如果程序包很简单,则包注释可以非常简短。
// Package path implements utility routines for
// manipulating slash-separated filename paths.
【规则3.9.12】不要依靠用空格进行对齐。
注释不需要额外的格式,例如星号横幅。生成的输出甚至可能会不按照固定宽度的字体进行展现,所以不要依靠用空格进行对齐—godoc,就像gofmt,会处理这些事情。注释是不作解析的普通文本,所以HTML和其它注解,例如_this_,将会逐字的被复制。对于缩进的文本,godoc确实会进行调整,来按照固定宽度的字体进行显示,这适合于程序片段。fmt package的包注释使用了这种方式来获得良好的效果。
根据上下文,godoc甚至可能不会重新格式化注释,所以要确保它们看起来非常直接:使用正确的拼写,标点,以及语句结构,将较长的行进行折叠,等等。
3.3.3 结构、接口及其他类型注释要求
【建议3.27】类型定义一般都以单数信息描述。
示例:
// Request represents a request to run a command.
type Request struct { ...
如果为接口,则一般以以下形式描述。
示例:
// FileInfo is the interface that describes a file and is returned by Stat and Lstat.
type FileInfo interface { ...
函数与方法注释要求
【建议3.9.13】函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、设计约束等
说明:重要的、复杂的函数,提供外部使用的接口函数应编写详细的注释。
【建议3.9.14】如果函数或者方法为判断类型(返回值主要为bool类型),则以 returns true if 开头。
如下例所示:
// HasPrefix returns true if name has any string in given slice as // prefix.
func HasPrefix(name string, prefixes []string) bool { ...
变量和常量的注释要求
Go的声明语法允许对声明进行组合。单个的文档注释可以用来介绍一组相关的常量或者变量。由于展现的是整个声明,这样的注释通常非常简单的。
// Error codes returned by failures to parse an expression.
var (
ErrInternal = errors.New("regexp: internal error")
ErrUnmatchedLpar = errors.New("regexp: unmatched '('")
ErrUnmatchedRpar = errors.New("regexp: unmatched ')'")
...
)
一般建议采用这样的方式:
var (
// BConfig is the default config for Application
BConfig *Config
// AppConfig is the instance of Config, store the config information from file
AppConfig *beegoAppConfig
// AppPath is the absolute path to the app
AppPath string
// GlobalSessions is the instance for the session manager
GlobalSessions *session.Manager
)
编码注释
在编码阶段应该同步写好 变量、函数、包 的注释,最后可以利用 godoc 命令导出文档。注释必须是完整的句子,句子的结尾应该用句号作为结尾(英文句号)。注释推荐用英文,可以在写代码过程中锻炼英文的阅读和书写能力。而且用英文不会出现各种编码的问题。
每个包都应该有一个包注释,一个位于 package 子句之前的块注释或行注释。包如果有多个 go 文件,只需要出现在一个 go 文件中即可。
// ping包实现了常用的ping相关的函数
package ping
Bug的注释
针对代码中出现的bug,可以采用如下教程使用特殊的注释,在godocs中可以做到注释高亮:
// BUG(astaxie):This divides by zero.
var i float = 1/0
http://blog.golang.org/2011/03/godocdocumentinggocode.html
带mutex的struct必须是指针receivers
如果你定义的struct中带有mutex,那么你的receivers必须是指针
recieved是值类型还是指针类型
到底是采用值类型还是指针类型主要参考如下原则:
func(w Win) Tally(playerPlayer)int //w不会有任何改变
func(w *Win) Tally(playerPlayer)int //w会改变数据
更多的请参考:https://code.google.com/p/go-wiki/wiki/CodeReviewComments#Receiver_Type
其他注释要求
当某个部分等待完成时,可用 TODO: 开头的注释来提醒维护人员。
当某个部分存在已知问题进行需要修复或改进时,可用 FIXME: 开头的注释来提醒维护人员。
当需要特别说明某个问题时,可用 NOTE: 开头的注释:
针对代码中出现的bug,可以采用BUG(who):注释,这些注释将被识别为已知的bug,并包含在文档的BUGS区。而其中的who应该是那些可以提供关于这个BUG更多信息的用户名。
比如,下面就是一个bytes包中已知的问题:
1. // BUG(r): The rule Title uses for word boundaries does not handle Unicode punctuation properly.
3.10 错误
【原则3.10.1】错误处理的原则就是不能丢弃任何有返回err的调用,不要采用_丢弃,必须全部处理。接收到错误,要么返回err,要么实在不行就panic,或者使用log记录下来
【规则3.10.2】error的信息不要采用大写字母,尽量保持你的错误简短,但是要足够表达你的错误的意思。
【规则3.10.3】导出的错误变量的命名,以Err开始,如ErrSomething,无需导出的错误变量命名,以Error作为后缀,如specificError
举例:
// 包级别的导出error.
var ErrSomething = errors.New("something went wrong")
func main() {
// 通常情况下我们只需要使用"err"
result, err := doSomething()
// 但是你也可以申明一个新的长名字变量,例如 "somethingError".
// Error作为后缀
var specificError error
result, specificError = doSpecificThing()
// ... 后面就使用specificError.
}
不好的例子:
var ErrorSomething = errors.New("something went wrong")
var SomethingErr = errors.New("something went wrong")
func main() {
var specificErr error
result, specificErr = doSpecificThing()
var errSpecific error
result, errSpecific = doSpecificThing()
var errorSpecific error
result, errorSpecific = doSpecificThing()
}
【规则3.10.4】公共包内禁止使用panic,如果有panic需要内部recover并返回error。
说明:只有当实在不可运行的情况采用panic,例如文件无法打开,数据库无法连接导致程序无法正常运行,但是对于其他的package对外的接口不能有panic。
3.11 其他
【建议3.11.1】在代码中编写字符串形式的json时,使用反单引号,而不是双引号。
例如:
“{“key”:“value”}”
改为格式更清晰的:
`
{
"key":"value"
}
`
【规则3.11.2】相对独立的程序块之间、变量说明之后必须加空行,而逻辑紧密相关的代码则放在一起。
不好的例子:
func formatResponseBody(res *http.Response, httpreq *httplib.BeegoHttpRequest, pretty bool) string {
body, err := httpreq.Bytes()
if err != nil {
log.Fatalln("can't get the url", err)
}
match, err := regexp.MatchString(contentJsonRegex, res.Header.Get("Content-Type"))
if err != nil {
log.Fatalln("failed to compile regex", err)
}
if pretty && match {
var output bytes.Buffer
err := json.Indent(&output, body, "", " ")
if err != nil {
log.Fatal("Response Json Indent: ", err)
}
return output.String()
}
return string(body)
}
应该改为:
func formatResponseBody(res *http.Response, httpreq *httplib.BeegoHttpRequest, pretty bool) string {
body, err := httpreq.Bytes()
if err != nil {
log.Fatalln("can't get the url", err)
}
match, err := regexp.MatchString(contentJsonRegex, res.Header.Get("Content-Type"))
if err != nil {
log.Fatalln("failed to compile regex", err)
}
if pretty && match {
var output bytes.Buffer
err := json.Indent(&output, body, "", " ")
if err != nil {
log.Fatal("Response Json Indent: ", err)
}
return output.String()
}
return string(body)
}
提示:当你需要为接下来的代码增加注释的时候,说明该考虑加一行空行了。
【规则3.11.3】尽早return:一旦有错误发生,马上返回。
举例:不要使用
if err != nil {
// error handling
} else {
// normal code
}
而推荐使用:
if err != nil {
// error handling
return // or continue, etc.
}
// normal code
这样可以减少嵌套深度,代码更加美观。
【建议3.11.4】禁止出现2处及以上的重复代码。
如果出现,必须抽取为独立小函数。不要担心性能问题,编译器会帮你搞定大部分的内联优化。同时认真阅读第四章节的“代码质量保证优先原则”
【建议3.11.5】if条件判断, 同时使用超过3个表达式以上的时候, 使用switch替代。
例如:
if a == 0 || a == 1 || a == 2 || a == 3 {
// ...
}
建议改写为:
switch a {
case 0, 1, 2, 3:
// ....
}
【建议3.11.6】定义bool变量时,要避免判断时出现双重否定,应使用肯定形式的表达式。
举例:
if !notFailed && !isReported { // 晦涩,不容易理解
notifyUser()
} else {
process()
}
应改为:
if isSuccess || isReported {
process()
} else {
notifyUser()
}
【建议3.11.7】for循环初始值从0开始,判断条件使用<无等号的方式。
举例:
for i := 1; i <= 10; i++ {
doSomeThing()
}
应改为:
for i := 0; i < 10; i++ {
doSomeThing()
}
这样子可以迅速准确得出循环次数。
【建议3.11.8】长句子打印或者调用,使用参数进行格式化分行
我们在调用fmt.Sprint
或者log.Sprint
之类的函数时,有时候会遇到很长的句子,我们需要在参数调用处进行多行分割:
下面是错误的方式:
log.Printf(“A long format string: %s %d %d %s”, myStringParameter, len(a),
expected.Size, defrobnicate(“Anotherlongstringparameter”,
expected.Growth.Nanoseconds() /1e6))
应该是如下的方式:
log.Printf(
“A long format string: %s %d %d %s”,
myStringParameter,
len(a),
expected.Size,
defrobnicate(
“Anotherlongstringparameter”,
expected.Growth.Nanoseconds()/1e6,
),
)
【建议3.11.9】 将 for-select 封装到函数中
如果在某个条件下,你需要从 for-select 中退出,就需要使用标签。例如:
func main() {
L:
for{
select {
case<-time.After(time.Second):
fmt.Println("hello")
default:
break L
}
}
fmt.Println("ending")
}
如你所见,需要联合break使用标签。这有其用途,不过我不喜欢。这个例子中的 for 循环看起来很小,但是通常它们会更大,而判断break的条件也更为冗长。
如果需要退出循环,我会将 for-select 封装到函数中:
func main() {
foo()
fmt.Println("ending")
}
func foo() {
for{
select {
case<-time.After(time.Second):
fmt.Println("hello")
default:
return
}
}
}
你还可以返回一个错误(或任何其他值),也是同样漂亮的,只需要:
// 阻塞
if err := foo(); err != nil {
// 处理 err
}
【建议3.11.10】把 slice、map 等定义为自定义类型
将 slice 或 map 定义成自定义类型可以让代码维护起来更加容易。假设有一个Server类型和一个返回服务器列表的函数:
type Server struct{
Name string
}
func ListServers() []Server {
return[]Server{
{Name:"Server1"},
{Name:"Server2"},
{Name:"Foo1"},
{Name:"Foo2"},
}
}
现在假设需要获取某些特定名字的服务器。需要对 ListServers() 做一些改动,增加筛选条件:
// ListServers 返回服务器列表。只会返回包含 name 的服务器。空的 name 将会返回所有服务器。
func ListServers(name string) []Server {
servers := []Server{
{Name:"Server1"},
{Name:"Server2"},
{Name:"Foo1"},
{Name:"Foo2"},
}
// 返回所有服务器
if name == ""{
return servers
}
// 返回过滤后的结果
filtered := make([]Server, 0)
for_, server := range servers {
if strings.Contains(server.Name, name) {
filtered = append(filtered, server)
}
}
return filtered
}
现在可以用这个来筛选有字符串Foo的服务器:
func main() {
servers := ListServers("Foo")
// 输出:“servers [{Name:Foo1} {Name:Foo2}]”
fmt.Printf("servers %+v\n", servers)
}
显然这个函数能够正常工作。不过它的弹性并不好。如果你想对服务器集合引入其他逻辑的话会如何呢?例如检查所有服务器的状态,为每个服务器创建一个数据库记录,用其他字段进行筛选等等……
现在引入一个叫做Servers的新类型,并且修改原始版本的 ListServers() 返回这个新类型:
type Servers []Server
// ListServers 返回服务器列表
func ListServers() Servers {
return[]Server{
{Name:"Server1"},
{Name:"Server2"},
{Name:"Foo1"},
{Name:"Foo2"},
}
}
现在需要做的是只要为Servers类型添加一个新的Filter()方法:
// Filter 返回包含 name 的服务器。空的 name 将会返回所有服务器。
func (s Servers) Filter(name string) Servers {
filtered := make(Servers, 0)
for_, server := range s {
ifstrings.Contains(server.Name, name) {
filtered = append(filtered, server)
}
}
return filtered
}
现在可以针对字符串Foo筛选服务器:
func main() {
servers := ListServers()
servers = servers.Filter("Foo")
fmt.Printf("servers %+v\n", servers)
}
哈!看到你的代码是多么的简单了吗?还想对服务器的状态进行检查?或者为每个服务器添加一条数据库记录?没问题,添加以下新方法即可:
func (s Servers) Check()
func (s Servers) AddRecord()
func (s Servers) Len()
...
【建议3.11.11】 为访问 map 增加 setter,getters
如果你重度使用 map 读写数据,那么就为其添加 getter 和 setter 吧。通过 getter 和 setter 你可以将逻辑封分别装到函数里。这里最常见的错误就是并发访问。如果你在某个 goroutein 里有这样的代码:
m["foo"] = bar
还有这个:
delete(m,"foo")
会发生什么?你们中的大多数应当已经非常熟悉这样的竞态了。简单来说这个竞态是由于 map 默认并非线程安全。不过你可以用互斥量来保护它们:
mu.Lock()
m["foo"] ="bar"
mu.Unlock()
以及:
mu.Lock()
delete(m,"foo")
mu.Unlock()
假设你在其他地方也使用这个 map。你必须把互斥量放得到处都是!然而通过 getter 和 setter 函数就可以很容易的避免这个问题:
func Put(key, value string) {
mu.Lock()
m[key] = value
mu.Unlock()
}
func Delete(key string) {
mu.Lock()
delete(m, key)
mu.Unlock()
}
使用接口可以对这一过程做进一步的改进。你可以将实现完全隐藏起来。只使用一个简单的、设计良好的接口,然后让包的用户使用它们:
type Storage interface {
Delete(key string)
Get(key string) string
Put(key, value string)
}
这只是个例子,不过你应该能体会到。对于底层的实现使用什么都没关系。不光是使用接口本身很简单,而且还解决了暴露内部数据结构带来的大量的问题。
但是得承认,有时只是为了同时对若干个变量加锁就使用接口会有些过分。理解你的程序,并且在你需要的时候使用这些改进。
参数传递
【建议3.11.12】 对于少量数据,不要传递指针
【建议3.11.13】 对于大量数据的 struct 可以考虑使用指针
【建议3.11.14】 传入的参数是 map,slice,chan 不要传递指针,因为 map,slice,chan 是引用类型,不需要传递指针的指针
自定义类型的string循环问题
如果自定义的类型定义了String方法,那么在打印的时候会产生隐藏的一些bug
type MyInt int
func (m MyInt) String() string {
return fmt.Sprint(m) //BUG:死循环
}
func(m MyInt) String() string {
return fmt.Sprint(int(m)) //这是安全的,因为我们内部进行了类型转换
}
panic
尽量不要使用panic,除非你知道你在做什么
在逻辑处理中禁用panic
在main包中只有当实在不可运行的情况采用panic,例如文件无法打开,数据库无法连接导致程序无法
正常运行,但是对于其他的package对外的接口不能有panic,只能在包内采用。
强烈建议在main包中使用log.Fatal来记录错误,这样就可以由log来结束程序。
注意闭包的调用
【原则3.11.15】在循环中调用函数或者goroutine方法,一定要采用显示的变量调用,不要再闭包函数里面调用循环的参数
fori:=0;i<limit;i++{
go func(){ DoSomething(i) }() //错误的做法
go func(i int){ DoSomething(i) }(i)//正确的做法
}