高质量编码笔记| 青训营

68 阅读6分钟

本篇笔记总结提炼了如何进行高质量编码的注意事项,从概述、组成分类、到各个部分如何参与实践都给出了明确的总结,并依次列举实践中的正反例来说明。

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.总结

在实践中,需要合理的归类并使用上述的规范。首先,高质量编码的目标包括正确可靠和简洁清晰,组成部分包括注释、代码规范、控制流程、错误和异常处理。其次,通过合理的注释可以提供额外的有效信息,解释复杂的实现过程,提供额外上下文以及解释代码的限制条件。最后,命名原则要求简洁胜于冗长,变量名应使用有意义的词汇,避免不必要的缩写。