编程规范 | 青训营

133 阅读5分钟

编程规范

简介

编程原则

实际应用场景千变万化,各种语言的特征和语法各不相同但是高质量编程遵循的原则是相通的

简单性

消除“多余的复杂性”,以简单清晰的逻辑编写代码

不理解的代码无法修复改进

可读性

代码是写给人看的,而不是机器

编写可维护代码的第一步是确保代码可读

生产力

团队整体工作效率非常重要

编码规范

代码格式

推荐使用gofmt自动格式化代码

gofmt

Go语言官方提供的工具,能自动格式化Go语言代码为官方统一风格

常见IDE都支持方便的配置

goimports

也是Go语言官方提供的工具

实际等于gofmt加上依赖包管理

自动增加依赖的包引用、将依赖包按字母序排序并分类

注释

注释应该解释代码作用

适合注释公共符号

// 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 O_RDONLY.
// If there is an error, it will be of type *PathError.
func Open(name string) (*File, error) {
    return OpenFile(name, O_RDONLY, 0)
}

注释应该解释代码如何做的

适合注释实现过程

// 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)
}

注释应该解释代码实现的原因

适合解释代码的外部因素

提供额外上下文

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
        // error, like we did in Go 1.7 and earlier
        shouldRedirect = false
    }
}

注释应该解释代码什么情况会出错

适合解释代码的限制条件

// parseTimeZone parses a time zone string and returns its length. Time zones
// are human-generated and unpredictable. We can't do precise 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 is a 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)

公共符号始终要注释

包中声明的每个公共的符号:变量、常量、函数以及结构都需要添加注释

任何既不明显也不简短的公共功能必须予以注释

无论长度或复杂程度如何,对库中的任何函数都必须进行注释

// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
func ReadAll(r Reader) ([]byte, error)

有一个例外,不需要注释实现接口的方法。具体不要像下面这样做

// Read implements the io.Reader interface
func(r *FileReader) Read(buf []byte) (int, error)

对于公共符号都有注释说明

尽管LimitedReader.Read本身没有注释,但他紧跟着LimitedReader结构的声明,明确他的作用

// LimitReader returns a Reader that reads from r
// but stops with EOF after n bytes.
// The underlying implementation is a *LimitedReader.
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
​
// A LimitedReader reads from R but limits the amount of
// data returned to just N bytes. Each call to Read
// updates N to reflect the new amount remaining.
// Read returns EOF when N <= 0 or when the underlying R returns EOF.
type LimitedReader struct {
    R Reader // underlying reader
    N int64  // max bytes remaining
}
​
func (l *LimitedReader) Read(p []byte) (n int, err error) {
    if l.N <= 0 {
        return 0, EOF
    }
    if int64(len(p)) > l.N {
        p = p[0:l.N]
    }
    n, err = l.R.Read(p)
    l.N -= int64(n)
    return
}

命名规范

variable

简洁胜于冗长

缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写

例如使用ServeHTTP而不是ServeHttp

使用XMLHTTPRequest或者xmlHTTPReques

变量距离其被使用的地方越远,则需要携带越多的上下文信息

全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义

i和index的作用域范围仅限于for循环内部时index的额外冗长几乎没有增加对于程序的理解
// Bad
for index := 0; index < len(s); index++ {
    // do somrthing
}
​
// Good
for i := 0; i < len(s); i++ {
    // do somrthing
}
将deadline替换成t降低了变量名的信息量
t常代指任意时间
deadline指截止时间,有特定的含义
// Good
func (c *Client) send(req *Request, deadline time.Time)
​
// Bad
func (c *Client) send(req *Request, t time.Time)

function

函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的

函数名尽量简短

当名为foo的包某个函数返回类型Foo时,可以省略类型信息而不导致歧义

当名为foo的包某个函数返回类型T时(T并不是Foo),可以在函数名中加入类型信息

package

只有小写字母组成。不包含大写字母和下划线等字符

简短并包含一定的上下文信息。例如schema、task等

不要与标准库同名。例如不要使用sync或者strings

以下规则尽量满足,以标准库包名为例

不使用常用变量名作为包名。例如使用bufio而不是buf

使用单数而不是复数。例如使用encoding而不是encodings

谨慎地使用缩写。例如使用fmt在不破坏上下文的情况下比format更加简短

控制流程

避免嵌套,保持正常流程清晰

如果两个分支中都包含return语句,则可以去除冗余的else
// Bad
if foo {
    return x
} else {
    return nil
}
​
// Good
if foo {
    return x
}
return nil

尽量保持正常代码路劲为最小缩进

优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套

// Bad
func OneFunc() error {
    err := doSomething()
    if err == nil {
        err := doAnotherThing()
        if err == nil {
            return nil // normal case
        }
        return err
    }
    return err
}

最常见的正常流程的路径被嵌套在两个if条件内

成功的退出条件是return nil,必须仔细匹配大括号来发现

函数最后一行返回一个错误,需要追溯到匹配的左括号,才能了解何时会触发错误

如果后续正常流程需要增加一步操作,调用新的函数,则又会增加一层嵌套

调整后
// Good
func OneFunc() error {
    if err := doSomething(); err != nil {
        return err
    }
    if err := doAnotherThing(); err != nil {
        return err
    }
    return nil // normal case
}

错误和异常处理

简单错误

简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误

优先使用errors.New来创建匿名变量来直接表示简单错误

如果有格式化的需求,使用fmt.Errorf

func defaultCheckRedirect(req *Request, via []*Request) error {
    if len(via) >= 10 {
        return errors.New("stopped after 10 redirects")
    }
    return nil
}

错误的Wrap和Unwrap

错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链

在fmt.Errorf中使用:%w关键字来将一个错误关联至错误链中

Go1.13在errors中新增了三个新的API和一个新的format关键字,分别是errors.ls、errors.As,errors.Unwrap以及fmt.Errorf的%w。如果项目运行在小于Go1.13的版本中,导入golang/x/xerrors来使用

list, _, err := c.GetBytes(cache.Subkey(a.actionID,"srcfiles"))
if err != nil {
    return fmt.Errorf("reading srcfiles list: %w", err)
}

错误判定

判定一个错误是否为特定错误,使用errors.ls

不同于使用==,使用该方法可以判定错误链上的所有错误是否含有特定的错误

data, err = lockedfile.Read(targ)
if errors.Is(err, fs.ErrNotExist) {
    // Treat non-existent as empty, to bootstrap the "latest" file
    // the first time we connect to a given database
    return []byte{}, nil
}
return data, err

在错误链上获取特定种类的错误,使用errors.As

if _, err := os.Open("non-existing"); err != nil {
    var pathError *fs.PathError
    if errors.As(err, &pathError) {
        fmt.Println("Failed at path:", pathError.Path)
    } else {
        fmt.Println(err)
    }
}

panic

不建议在业务代码中使用panic

调用函数不包含recover会造成程序崩溃

若问题可以被屏蔽或解决,建议使用error代替panic

当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic

func main() {
    // ...
    ctx, cancel := context.WithCancel(context.Background())
    client, err := sarama.NewConsumerGroup(strings.Split(brokers, ","), group, config)
    if err != nil {
        log.Panicf("Error creating consumer group client: %v", err)
    }
    // ...
}
​
// Panicf is equivalent to Printf() followed by a call to panic()
func Panicf(format string, v ...interface{}) {
    s := fmt.Sprintf(format, v...)
    std.Output(2, s)
    panic(s)
}

recover

recover只能在被defer的函数中使用

嵌套无法生效

只在当前goroutine生效

defer的语句是后进先出

func (s *ss) Token(skipSpace bool, f func(rune) bool)
(tok []byte, err error) {
    defer func() {
        if e := recover(); e != nil {
            if se, ok := e.(scanError); ok {
                err = se.err
            } else {
                panic(e)
            }
        }
    }()
    // ...
}

如果需要更多的上下文信息,可以recover后在log中记录当前的调用栈

func (t *treeFS) Open(name string) (f fs.File, err error) {
    defer func() {
        if e := recover(); e != nil {
            f = nil
            err = fmt.Errorf("gitfs panic: %v\n%s", e, debug.Stack())
        }
    }()
    // ...
}