本篇笔记总结提炼了如何进行高质量编码的注意事项,从概述、组成分类、到各个部分如何参与实践都给出了明确的总结,并依次列举实践中的正反例来说明。
0.高质量编码概述
(1)高质量编码目标:正确可靠、简洁清晰
- 正确可靠:边界条件考虑完备,异常情况处理,稳定性保证
- 简洁清晰:易读易维护
(2)组成部分:注释、代码规范、控制流程、错误和异常处理
1.注释
注释的范围:公共符号(包中的变量、常量、函数、结构;库中的所有东西)必须注释,实现接口的方法不需要注释。
注释的功能分类:
(1)解释代码作用:适合注释公共符号
- 正例:提供额外有效信息
// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode 0_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
- 反例:把名字可以看出来的东西重复一遍,信息熵为0
// Returns true if the table cannet hold any more entries
func IsTableFull() bool
(2)解释代码如何做的:适合注释函数内实现过程
- 正例:解释相对复杂的名字、index、切片等在做什么,判断会产生哪些结果
// Add the Referer header from the most recent
/ request URL to the new one, if it's not https->http:
if ref := refererForURL(reqs[len(reqs)-1].URL, req.URL); ref !="" {
req.Header.Set( "Referer", ref)
}
- 反例:描述一遍 “这是for循环” ,而不说for循环什么要做什么,信息熵为0
// Process every element in the list
for e : range elements {
process(e)
}
(3)解释代码实现的原因:适合解释代码外部因素,提供额外上下文
- 正例:一些很短但是上下文信息遥远不好理解的地方,详细说明写这里用到的信息
switch resp.StatusCode {
//...
case 307, 308:
redirectMethod = reqMethod
shouldRedirect = true
includeBody = true
if ireq.GetBody == nil && ireq.outgoingLength() !=0{
// We had a request body, and 307/308 require
// re-sending it, but GetBody is not defined. So just
// return this response to the user instead of an
// eror, like we did in Go 1.7 and earlier.
shouldRedirect = false
}
}
- 反例:含义未知的判断与赋值,长时间过后很难理解
redisclient := l.svcCtx.RedisClient
if incFlag == true {
info, e := redisclient.GetCtx(l.ctx, redisKey)
if e != nil && e != redis.Nil {
return e
}
if len(info) ==0 {
redisclient.SetCtx(l.ctx, redisKey, "1")
}else {
redisclient.IncrCtx(l.ctx, redisKey)
}
}
(4)解释代码什么情况会出错:适合解释代码的限制条件
- 正例:给使用者说明在什么时候会怎么样的注意点,并给出例子
// parseTimeZone parses a time zone string and returns its length. Time zones
// are human-generated and unpredictable. We can't do prectse error checking.
// On the other hand, for a correct parse there must be a time zone at the
// beginning of the string, so it's almost always true that there's one
// there. We look at the beginning of the string for a run of upper-case letters. If there are more than 5, it's an error.
// If there are 4 or 5 and the last isa t, it's a time zone.
// If there are 3, it's a time zone.
// Otherwise, other than special cases, it's not a time zone.
// GMT is special because it can have an hour offset.
func parseTimeZone(value string) (Length int, ok bool)
- 反例:不加注释,对于可能出现的问题拖到使用的时候再测
func parseTimeZone(value string) (Length int, ok bool)
2.代码规范
含义:主要包含代码格式和命名原则两个方面。其中代码格式主要是团队协作中提前统一风格,便于代码review;命名原则要求简洁胜于冗长。
命名的对象分类:
(1)变量
- 简洁更好的例子对比:
// Bad
for index := 0; index < len(s); index++ {
// do something
}
// Good
for i := 0; t< len(s); t++ {
// do something
}
- 错误使用该原则的例子:有特定含义的词不应该被省略
// Good
func (c *Client) send(req *Request, deadline time.Time)
// Bad
func (c *Client) send(req *Request, t time.Time)
最后要注意缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
- 例1:使用 ServeHTTP而不是ServeHttp
- 例2:使用 XMLHTTPRequest 或者 xmlHTTPRequest
(2)函数
函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
-
例子:当调用为http.serveHTTP()时,函数名中的http是冗余的
-
http包中创建的服务就可以用上面的方式而不是下面的方式:
✅)func Serve(l net.Listener, handler Handler) error
❌)func ServeHTTP(I net.Listener, handler Handler) error
函数名尽量简短
- 例子:当名为f00的包某个函数返回类型Foo时,可以省略类型信息而不导致歧义
- 例子:当名为f00的包某个函数返回类型T时(T并不是Foo),可以在函数名中加入类型信息
(3)包
只由小写字母组成。不包含大写字母和下划线等字符简短并包含一定的上下文信息。
- 例如 schema、task 等不要与标准库同名。
- 例如不要使用 sync或者 strings
以下规则尽量满足,以标准库包名为例
- 不使用常用变量名作为包名。例如使用 bufio而不是buf
- 使用单数而不是复数。例如使用 encoding而不是encodings
- 谨慎地使用缩写。例如使用 fmt在不破坏上下文的情况下比 format 更加简短
变量距离其被使用的地方越远,则需要携带越多的上下文信息 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义
3.控制流程
控制流程原则:避免嵌套,保持正常流程清晰
- 例子:去除冗余的else
// Bad
if foo {
return x
} else {
return nil
}
// Good
if foo {
return x
}
return nil
尽量保持正常代码路径为最小缩进:
- 例子:优先处理错误/特殊情况,尽早返回来减少嵌套
- 最常见的正常流程的路径被嵌套在两个if条件内
- 成功的退出条件是return nil,必须仔细匹配大括号来发现函数最后一行返回一个错误,需要追溯到匹配的左括号,才能了解何时会触发错误
- 如果后续正常流程需要增加一步操作,调用新的函数,则又会增加一层嵌套
// Bad
func OneFunc() error {
err := doSomething()
if err == nil {
err := doAnotherThing()
if err == nil {
return nil // normal case
}
return err
}
return err
}
// Good
func OneFunc() error {
if err := doSgmething(); err != ntl {
return err
}
if err := doAnotherThing(); err != ntl ({
return err
}
return nil // normal case
}
4.错误和异常处理
简单错误:简单错误指仅出现一次的错误,且在其他地方不需要捕获该错误。此时优先使用 errors.New 创建匿名变量来直接表示简单错误
panic:
- 不建议在业务代码中使用 panic
- 调用函数不包含recover会造成程序崩溃若问题可以被屏蔽或解决,建议使用 error 代替 panic
- 当程序启动阶段发生不可逆转的错误时,可以在 init或 main函数中使用 panic)
recover:
- recover 只能在被 defer的函数中使用
- 嵌套无法生效
- 只在当前 goroutine生效
- defer的语句是后进先出
- 如果需要更多的上下文信息,可以 recover 后在 log 中记录当前的调用栈
5.总结
在实践中,需要合理的归类并使用上述的规范。首先,高质量编码的目标包括正确可靠和简洁清晰,组成部分包括注释、代码规范、控制流程、错误和异常处理。其次,通过合理的注释可以提供额外的有效信息,解释复杂的实现过程,提供额外上下文以及解释代码的限制条件。最后,命名原则要求简洁胜于冗长,变量名应使用有意义的词汇,避免不必要的缩写。