Go高质量编程与性能调优 | 青训营笔记

94 阅读4分钟

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

0、 前言

本节主要简要介绍了高质量编程的定义和原则,

分享了代码格式、注释、命名规范、控制流程、错误和异常处理五方面的常见编码规范。

课程重点如下:

  • 高质量编程

    • 编码规范
    • 性能优化建议
  • 性能调优实战(略)

1、高质量编程

编码规范

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

  • 注释的意义:

    • 注释应该解释代码作用
    • 注释应该解释代码如何做的
    • 注释应该解释代码实现的原因
    • 注释应该解释代码什么情况会出错
    • 公共符号始终要注释(但是不需要注释实现接口的方法)
  • 命名规范:

    • 简洁胜于冗长

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

      • 使用ServeHTTP而不是ServeHttp
      • 使用XMLHTTPRequest而不是xmlHTTPRequest
    • 变量距离其被使用的地方越远,则需要携带越多的上下文信息

    • 函数名不携带包名的上下文信息且尽量简短

    • 函数名应当尽量简短

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

    • 当名为 foo 包的某个函数返回类型为 T 而不是 Foo 时,应该在函数名中加入类型信息

    • package名只由小写字母组成

    • 简短并包含一定的上下文信息

    • 不要和标准库同名

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

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

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

  • 控制流程:

    • 避免嵌套,保持正常流程清晰,例如去掉不必要的else
    • 尽量保持正常代码路径为最小缩进,能对称就对称
    • 故障问题的大多出现在复杂的条件语句和循环语句中,尽量化简
  • 错误和异常处理:

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

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

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

    • // 一个例子
      func defaultCheckRedirect(req *Request, via []*Request) error {
          if len(via) >= 10 {
              // 使用errors.New
              return errors.New("stopped after 10 redirects.")
          }
          return nil  // 去掉不必要的else
      }
      复制代码
      
    • 错误的WrapUnnwrap

      • 错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成一个error跟踪链
      • fmt.Errorf中使用%w关键字来将一个错误关联至错误链
      • // 一个例子
        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) {
            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:比错误更严重,表示程序无法正常工作,在业务代码中不建议使用,故不展开介绍。

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

      生效条件:

      • 只能在被defer的函数中使

      • 嵌套无法生效

      • 只在当前goroutine生效

      • 注意defer是一个栈

性能优化建议

  • 使用 Benchmark 进行基准测试;

  • 尽可能为 slice 和 map 预分配内存(通过在 make 时指定容量信息);

  • 注意为切片创建切片不会创建新的底层数组,这可能会导致内存泄漏发生,此时可用 copy 代替 re-slice;

  • 多个字符串拼接时,使用 strings.Builder 比直接使用 + 或使用 bytes.Buffer 更快(这和 Java 倒是十分相似,Java 也推荐使用 StringBuilder 拼接多个字符串;其实他们的底层逻辑都是类似的);

  • 当需要占位符时,可使用空结构体(struct{})代替,其不会占据镇和内存空间;

  • 使用 atomic 包代替锁修改变量;