go code review 代码调试
作者:洛天枫
Examples
在添加新包时,包括预期使用的示例:
- 一个可运行的示例
- 或一个演示完整调用序列的简单测试
Goroutine Lifetimes
当你使用 goroutines
时,要明确它们何时退出或者是否退出。
goroutine
可以通过阻塞通道发送或接收而泄漏: 即使阻塞的通道不可达,垃圾收集器也不会终止 goroutine
。
即使 goroutine
没有泄漏,当它们不再被需要时,将它们留在空中也会导致其他微妙和难以诊断的问题。发送在已关闭的通道时会引发 panic
。“在结果不需要之后”修改仍在使用的输入仍然会导致数据竞争。并且让 goroutines
在运行中停留任意长的时间会导致不可预测的内存使用。
尽量保持并发代码足够简单,这样 goroutine
的生存期就很明显了。如果这是不可行的,记录下 goroutines
退出的时间和原因。
有些时候我们使用 goroutine 的时候,会忘记捕获 panic,这里提供一个封装好了的 go
方法:
// GoSafe runs the given fn using another goroutine, recovers if fn panics. func GoSafe(log *log.Logger, fn func()) { go RunSafe(log, fn) } // RunSafe runs the given fn, recovers if fn panics. func RunSafe(log *log.Logger, fn func()) { defer Recover(log) fn() } // Recover is used with defer catch panics. func Recover(log *log.Logger) { if p := recover(); p != nil { log.Error("%s\n%s", p, debug.Stack()) } }
Handle Errors
不要使用 _
变量丢弃错误。如果函数返回错误,请检查它以确保函数成功。处理错误,返回错误,或者,在真正异常的情况下,抛出 panic
。
可以参考官方文档中优雅的做法:Effective Go
import
避免重命名导入,除非是为了避免名称冲突
好的包名不需要重命名。如果发生冲突,最好重命名、本地的或特定于项目的导入。
导入按组区分,组与组之间有空白行。标准库包总是在第一组中。
package main import ( "fmt" "hash/adler32" "os" "appengine/foo" "appengine/user" "github.com/foo/bar" "rsc.io/goversion/version" )
使用 goimport 能帮助到您,所以在项目中配置 goimport 是必要的。
Import Blank
仅为其副作用而导入的包(使用import _ "pkg"语法)应该只在程序的主包中导入,或在需要它们的测试中导入。
类似使用其他包的 init
函数
Import Dot
import .
在解决循环依赖时较为好用的方式,但是不能滥用:
package foo_test import ( "bar/testutil" // also imports "foo" . "foo" )
在这种情况下,测试文件不能在包 foo
中,因为它使用 bar/testutil
,它导入 foo
。所以我们使用 'import .
' 形式让文件假装是包 foo
的一部分,即使它不是。除了这种情况,不要使用import .
在你的项目中。它使程序更加难以阅读,因为不清楚像 Quux
这样的名称是当前包还是导入包中的顶级标识符。
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 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)
这可以防止调用者错误地使用返回值:
Parse(Lookup(key)) // compile-time error
并鼓励更健壮和可读的代码:
value, ok := Lookup(key) if !ok { return fmt.Errorf("no value for %q", key) } return Parse(value)
该规则适用于导出函数,但也适用于私有函数。
当它们是函数的有效结果时,像 nil、""、0 和 -1 这样的值是可以的,也就是说,当调用者不需要以不同于其他值的方式处理它们时。
一些标准库函数,比如包 strings
中的函数,返回带内错误值。这极大地简化了字符串操作代码,但代价是需要程序员付出更多努力。一般来说,Go代码应该返回额外的错误值。
Indent Error Flow
尽量将正常的代码路径缩进到最小,并缩进错误处理,首先处理它。这通过允许快速视觉扫描正常路径提高了代码的可读性。例如,不要写:
if err != nil { // error handling } else { // normal code }
相反,写:
if err != nil { // error handling return // or continue, etc. } // normal code
如果语句中有初始化语句,例如:if
if x, err := f(); err != nil { // error handling return } else { // use x }
然后,这可能需要将短变量声明移动到它自己的行:
x, err := f() if err != nil { // error handling return } // use x
Initialisms
名字中属于首字母缩写或首字母缩写的词(例如:"URL"
或"NATO"
)的大小写是一致的。例如,"URL"
应该显示为 "URL"
或 "url"
(如"urlPony"
或"URLPony"
),而不是"Url"
。例如:ServeHTTP
而不是ServeHttp
。对于具有多个初始化“单词”的标识符,可以使用例如"xmlHTTPRequest"
或"XMLHTTPRequest"
。
当"ID"
是"identifier"
的缩写时,这个规则也适用于"ID"
(在大多数情况下,它不是"ego"
、"superego"
中的"ID"
),所以写"appID"
而不是"appId"
。
protobuf
生成的代码不受此规则约束。人类编写的代码要比机器编写的代码具有更高的标准。
Interfaces
Go
接口通常属于使用接口类型值的包,而不是实现这些值的包。实现包应该返回具体的(通常是指针或结构)类型:这样,新的方法就可以添加到实现中,而不需要大量的重构。
不要在API的实现者端定义 "mock"
接口;相反,应该设计API,以便可以使用实际实现的公共API进行测试。
不要在使用之前定义接口:没有一个实际的使用示例,很难判断接口是否必要,更不用说它应该包含哪些方法了。(有点重构的意思,将已有的方法抽象起来)
package consumer // consumer.go type Thinger interface { Thing() bool } func Foo(t Thinger) string { … }
package consumer // consumer_test.go type fakeThinger struct{ … } func (t fakeThinger) Thing() bool { … } … if Foo(fakeThinger{…}) == "x" { … }
// DO NOT DO IT!!! package producer type Thinger interface { Thing() bool } type defaultThinger struct{ … } func (t defaultThinger) Thing() bool { … } func NewThinger() Thinger { return defaultThinger{ … } }
相反,返回一个具体类型,并让使用者模拟生产者实现。
package producer type Thinger struct{ … } func (t Thinger) Thing() bool { … } func NewThinger() Thinger { return Thinger{ … } }
Line Length
在Go代码中没有严格的行长限制,但是要避免令人不舒服的长行。类似地,当行更长的时候,不要添加换行符以保持行短——例如,如果它们是重复的。
大多数情况下,当人们“不自然地”换行时(或多或少地在函数调用或函数声明中换行,尽管存在一些异常),如果它们有合理数量的参数和合理较短的变量名,换行将是不必要的。较长的行似乎和较长的名字联系在一起,去掉长名字会有很大帮助。
换句话说,断行是因为你所写的内容的语义(作为一般规则),而不是因为行长。如果您发现这产生了太长行,那么更改名称或语义,您可能会得到一个好的结果。
实际上,这与函数的长度是完全相同的。没有“函数永远不会超过N行的规则”,但确实存在过长的函数,以及重复过小的函数,解决方法是改变函数的边界,而不是开始计数行数。
Mixed Caps
Named Result Parameters
想想在 godoc
中会是什么样子。命名结果参数如下:
func (n *Node) Parent1() (node *Node) {} func (n *Node) Parent2() (node *Node, err error) {}
将在 godoc
中重复;更好的使用:
func (n *Node) Parent1() *Node {} func (n *Node) Parent2() (*Node, error) {}
另一方面,如果函数返回两个或三个相同类型的参数,或者如果从上下文不清楚结果的含义,添加名称在某些上下文中可能是有用的。不要为了避免在函数内部声明 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)
如果函数只有几行,裸返回是可以的。一旦它是一个中等大小的函数,就要显式地显示返回值。推论:仅仅因为结果参数允许使用裸返回而命名结果参数是不值得的。清晰的文档总是比在函数中节省一两行更重要。
最后,在某些情况下,您需要命名结果参数,以便在延迟闭包中更改它。这总是可以的。
Naked Returns
没有参数的语句返回指定的返回值。这就是所谓的“裸返回”。
func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return }
Package Comments
包注释,像 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" 注释,其他风格的注释在二进制名称之后也可以(如果它在前面,则可以大写),例如,对于您可以编写的目录中的
// Binary seedgen ... package main
// Command seedgen ... package main
// Program seedgen ... package main
// The seedgen command ... package main
// The seedgen program ... package main
// Seedgen .. package main
这些都是例子,合理的变体也是可以接受的。
注意,以小写单词开头的句子不属于包注释的可接受选项,因为它们是公开可见的,应该用正确的英语编写,包括句子的第一个单词大写。当二进制名称是第一个单词时,即使它与命令行调用的拼写不严格匹配,也需要将其大写。
struct
每个结构体必须有自己的构造函数,并且使用 options 模式来构建新的参数。
以上就是go code review 代码调试的详细内容,更多关于go code review的资料请关注脚本之家其它相关文章!