Go语言工程实践入门(三)高质量编程与性能调优 | 青训营

44 阅读3分钟

高质量编程

代码格式

使用gofmtgoimports等工具自动格式化代码,让项目整体代码风格保持统一

注释

  1. 解释代码作用
    • 注释需要描述代码片段所使用的公共符号(变量、常量、函数、结构)的含义和基本的使用说明
    • 多处使用的公共功能
    • 对所有函数
  2. 解释代码如何做的:注释需要描述代码片段实现过程
  3. 解释代码实现原因:主要是提供额外的上下文解释影响代码实现的原因和思路
  4. 解释代码什么情况下会出错:描述代码的边界情况和限制条件

命名规范

  1. 在保持命名语义的前提下尽可能简洁fmt
  2. 缩略词全大写ServerHTTP,位于变量开头且不需要导出时全小写xmlHTTPRequest
  3. 距离被使用的地方越远命名时需要提供越多的上下文信息,特别是全局变量
  4. 对于函数名命名
    • 不用携带包名的上下文信息,因为使用时一般为package.function
    • 在保持命名语义的前提下尽可能简洁
    • 返回值类型和包名相同时命名可以省略类型信息
    • 返回值类型和包名不同时在函数名中加入类型信息
  5. 对于包名命名
    • 只包含小写字母(不包含大写字母和下划线)
    • 简洁但包含一定上下文信息
    • 不能与标准库同名
    • 尽量满足
      • 不使用常用变量名,如buf作为包名
      • 使用单数
      • 谨慎使用缩写

控制流程

  • 在条件判断分支较多时,需要先进入异常逻辑判断和处理,保证主流程的代码路径为最小缩进
  • 避免多层嵌套,保证代码流程清晰

错误和异常处理

  • 简单错误
    • 是指只在当前位置出现,不需要在其他地方捕获
    • 使用errors.New("")创建匿名变量
    • 有格式化要求使用fmt.Errorf
  • 错误链
    • fmt.Errorf中使用%w将一个错误关联至错误链中
    • 使用errors.Is()来比较一个错误是否是指定错误,这种方式比直接字符串比较要好,可以判定错误链上的所有错误是否含有特定的错误
    • 使用errors.As()在错误链上获取指定种类的错误
    • 使用errors.Unwrap检索错误链中的下一个错误,可用于遍历错误链
      package main
      
      import (
          "errors"
          "fmt"
      )
      
      func main() {
          err := foo()
          for err != nil {
              fmt.Println(err)
              // foo: bar: baz
              // bar: baz
              // baz
              err = errors.Unwrap(err)
          }
      }
      
      func foo() error {
          return fmt.Errorf("foo: %w", bar())
      }
      
      func bar() error {
          return fmt.Errorf("bar: %w", baz())
      }
      
      func baz() error {
          return errors.New("baz")
      }
      
  • panic
    • 调用函数不包含recover时调用panic()会中断程序运行
    • 如果错误可以跳过或者屏蔽,应尽量使用error代替panic
    • 在程序的启动阶段发生不可逆转的错误时,在initmain中使用panic
  • recover
    • recover只能在被defer的函数中使用
    • 嵌套无法生效
    • 只在当前的goroutine生效
    • defer语句是后进先出

性能优化

Benchmark

测试命令go test -bench=. -benchmem

测试结果

BenchmarkFib10-8    1855870    602.5 ns/op    0 B/op    0 allocs/op
  • 测试函数名(-8表示GOMAXPROCS,默认为CPU核数)
  • 总共执行测试次数,为b.N的值
  • 每次执行花费时间
  • 每次执行申请内存大小
  • 每次执行申请内存次数

优化建议

  • 在使用make()初始化切片时提供容量信息make([]int, 0, size)
  • 不要在已有大切片上新建小切片,这种方式不会创建新的底层数组,实际上是对元切片的引用,原切片得不到释放
  • 使用copy得到新切片copy(new_s, origin[len(origin)-2:])
  • 在使用make()初始化map时提供容量信息make(map[int]int, size)
  • 针对字符串处理,使用strings.Builder,下面是字符串拼接的例子
    func StrBuilder(n int, str string) string {
        var builder strings.Builder
        for i := 0; i < n; i++ {
            builder.WriteString(str)
        }
        return builder.String()
    }
    
  • strings.Builder也能提前指定容量
    var builder strings.Builder
    builder.Grow(n * len(str))
    
  • 使用空结构体可以节省内存
    • 空结构体struct{}实例不占据任何的内存空间
    • 可作为各种场景下的占位符使用,不需要任何值,仅作为占位符,比如使用map实现set时,只需要键,不需要值,可以使用空结构体占位
  • 针对单个变量的锁保护使用atomic包代替sync.Mutex
    type atomicCounter struct {
        i int32
    }
    func AtomicAddOne(c *atomicCounter) {
        atomic.AddInt32(&c.i, 1)
    }
    
    • 锁的实现通过操作系统,属于系统调用

    • atomic操作是通过硬件实现,效率比锁高

    • sync.Mutex应该用于保护一段逻辑,而不是单个变量

    • 对于非数值操作,可以使用atomic.Value,能承载一个interface{}

pprof工具调优实践

pprof工具调优过程

使用go tool pprof "http://localhost:6060/debug/pprof/profile? seconds=10"收拾数据

CPU指标优化

  • 使用topN命令查看占用资源最多的函数

    image.png

    • flat:当前函数本身的执行耗时
    • flat%flatCPU总时间的比例
    • sum%:上面每一行的flat%的总和
    • cum:当前函数本身加上其调用函数的总耗时
    • cum%cumCPU总时间的比例
  • 使用list xxx根据指定正则表达式查找代码行

  • 使用web实现调用关系可视化

Heap(堆内存)指标优化

  • go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"打开分析网页
  • 通过VIEW-Graph标签查看调用关系图
  • 通过VIEW-Source标签查看源码视图
  • 采样条件,一般查看alloc,因为程序可能释放,需要查看累计值
    • alloc_objects:程序累计申请的对象数
    • alloc_space:程序累计申请的内存大小
    • inuse_objects:程序当前持有的对象数
    • inuse_space:程序当前占用的内存大小

goroutine协程、mutex锁、block阻塞