Go语言高质量编程 | 青训营笔记

53 阅读5分钟

这是我参与「第五届青训营 」笔记创作活动的第 6 天

前言

本文主要介绍高质量编程相关内容,首先简单说明什么是高质量代码,之后详细介绍常用编码规范。

重点内容

  • 高质量编程简介
  • 编码规范

知识点介绍

简介

  • 编写的代码能够达到正确可靠、简洁清晰、无性能隐患的目标就能称之为高质量代码

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

  • 高质量的编程需要注意以下原则:简单性、可读性、生产力

    • 简单性:消除多余的复杂性,以简单清晰的逻辑编写代码
    • 可读性:代码是写给人看的,编写可维护代码的第一步是确保代码可读
    • 生产力:团队整体工作效率非常重要

编码规范

代码格式

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

  • gofmt:Go 语言官方提供的工具,能自动格式化 Go 语言代码为官方统一风格
  • goimports:Go 语言官方提供的工具,自动增删依赖包的引用、将依赖包按字母序排序并分类

注释

代码是最好的注释,注释应该提供代码未表达出的上下文信息。

注释应该做的:

  • 注释应该解释代码的作用:适合注释公共符号

    // 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) (length int, ok bool)
    
  • 公共符号始终要注释:包中声明的每个公共的符号:变量、常量、函数以及结构都需要添加注释

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

    首先 LimitReader 的功能有注释说明,然后是 LimitedReader 结构体的说明,就在使用它的函数之前,LimitedReader.Read 的声明遵循 LimitedReader 本身的声明,里面已经包含。

命名规范

核心目标是降低阅读理解代码的成本,重点考虑上下文信息,设计简洁清晰的名称。

  • variable

    • 简洁胜于冗长
    • 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
    • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
    • 全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义

    两个简单的例子

    // bad
    for index := 0; index < len(s); index++ {
        
    }
    // good
    for i := 0; i < len(s); i++ {
        
    }
    
    // bad
    func(c *Client) send(req * Request, t time.Time)
    // good
    func(c *Client) send(req * Request, deadline time.Time) // deadline指截止时间,有特定含义
    
  • function

    • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
    • 函数名尽量简短
    • 当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
    • 当名为 foo 的包某个函数返回类型 T 时(T 并不是 Foo),可以在函数名中加入类型信息

    例如 HTTP 包中创建服务的函数

    func Serve(I net,Listener, handler Handler) // good
    func ServeHTTP(I net,Listener, handler Handler) // bad 调用时使用 http.Serve 已经包含http,无须额外添加
    
  • package

    • 只由小写字母组成。不包含大写字母和下划线等字符
    • 简短并包含一定的上下文信息。例如 schema、task 等
    • 不要与标准库同名。例如不要使用 sync 或者 strings

控制流程

线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支,提高代码的可读性,故障问题大多出现在复杂的条件语句和循环语句中。

  • 避免嵌套,保持正常流程清晰
    • 如果两个分支中都包含 return 语句,则可以去除冗余的 else
  • 尽量保持正常代码路径为最小缩进
    • 优先处理错误情况/特殊情况,并尽早返回或继续循环来减少嵌套,增加可读性

错误和异常处理

  • 简单错误处理

    • 简单错误是指仅出现一次的错误且在其他地方不需要捕获该错误
    • 优先使用 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 关键字来将一个错误 wrap 至其错误链中
    • Go1.13 在 errors 中新增了三个新 API 和一个新的 format 关键字,分别是 errors.Is、errors.As 、errors.Unwrap 以及 fmt.Errorf 的 %w
    list, _, err := c.GetBytes(cache.Subkey(a.actionID, "srcfiles"))
    if err != nil {
    	return fmt.Errorf("reading srcfiles list: %w", err)
    }
    
  • 错误判定

    • 使用 errors.Is 可以判定错误链上的所有错误是否含有特定的错误

      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
      }
      
    • 在错误链上获取特定种类的错误,使用 errors.As

  • panic

    • 不建议在业务代码中使用 panic
    • 如果当前 goroutine 中所有 deferred 函数都不包含 recover 就会造成整个程序崩溃
    • 当程序启动阶段发生不可逆转的错误时,可以在 init 或 main 函数中使用 panic
    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)
    }
    
  • recover

    • recover 只能在被 defer 的函数中使用,嵌套无法生效,只在当前 goroutine 生效

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

总结

本文主要介绍了高质量编程相关内容,重点介绍了编码规范,从代码格式、注释、命名规范、控制流程、错误和异常处理等方面进行了详细的阐述,通过 Go 的官方源码具体展示了高质量的代码。规范的代码书写对自己及团队其他人都是非常重要的。