Go语言性能调优|青训营伴学笔记

77 阅读5分钟

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

代码规范

代码格式

  • 使用gofmt自动格式化代码
  • 使用goimports格式化代码,同时也会自动增删依赖的包引用、将依赖包按字母序排序并分类

注释

注释的内容:

  • 解释代码的作用

    • 解释公共符号
    • 解释返回值
  • 解释代码如何做的

    • 注释代码实现的过程
  • 解释代码实现的原因

    • 解释代码的外部因素
    • 提供额外的上下文(注释应该提供代码未表达出的上下文信息
  • 解释代码什么情况会出错

    • 解释代码的限制条件

命名规范

变量名:

  • 简洁胜于冗长

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

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

    • 全局变量在其名字需要更多的上下文信息

函数名:

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

包名:

  • 全部小写,不使用大写和下划线
  • 简短并包含一定的上下文信息
  • 不与标准库同名
  • 不使用常用的变量名
  • 使用单数而不是复数(使用encoding而不是encodings)
  • 谨慎使用缩写

控制流程

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

    • //bad
      if foo {
          return x
      }else {
          return nil
      }
      //good
      if foo {
          return x
      }
      return nil
      
  • 尽量保持正常代码路径为最小缩进

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

错误和异常处理

  1. 简单错误

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

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

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

  2. 复杂错误:使用Wrap和Unwrap

    Wrap提供一个error嵌套到另外一个error的能力,从而生成一个error的跟踪链

    fmt.Errorf中使用%w来将一个错误嵌套到另外一个错误链中

    if err!= nil {
        return fmt.Errorf("error from %w",err)
    }
    
  3. 错误判定

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

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

      data,err = lockedfile.Read(targ)
      if errors.Is(err,fs.ErrNotExist) {
          ...
      }
      
    • 获取特定种类的错误,使用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)
          }
      }
      
  4. panic

    出现不可逆转的错误,使用panic退出

  5. recover

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

    嵌套无法生效

    只能在当前的goroutine生效

    defer的语句是后进先出的

性能优化

go语言提供了支持基准性能测试的benchmark工具

性能表现需要实际的数据衡量

go test -bench=. -benchmem

image-20230126175550310.png

Slice

  1. 内存预分配

    //未预分配
    func NoPreAlloc(size int) {
        data := make([]int, 0)
        for k := 0; k < size; k++ {
            data = append(data, k)
        }
    }
    ​
    //预分配内存
    func PreAlloc(size int) {
        data := make([]int, 0, size)
        for k := 0; k < size; k++ {
            data = append(data, k)
        }
    }
    ​
    

image-20230126180051469.png

性能差异原因:

-   Slice本质是数组片段的描述(数组指针+片段长度+片段容量)
-   Slice操作不复制Slice指向的元素
-   创建性的Slice会复用原来的Slice

**即预先分配内存可以不用扩容**

0. 大内存未释放

在已有的Slice基础上创建很小的Slice,此时不会创建新的Slice,此时原来的Slice得不到释放。

解决方法:使用`copy`复制创建新的Slice

```
func GetLastBySlice(origin []int) []int {
    return origin[len(origin)-2:]
}
​
func GetLastByCopy(origin []int) []int {
    result := make([]int, 2)
    copy(result, origin[len(origin)-2:])
    return result
}
```

image-20230126180422313.png

Map

内存预分配

func BenchmarkNoPreAlloc(b *testing.B) {
    for n := 0; n < b.N; n++ {
        NoPreAlloc(1000)
    }
}
​
func BenchmarkPreAlloc(b *testing.B) {
    for n := 0; n < b.N; n++ {
        PreAlloc(1000)
    }
}

image-20230126180659443.png

String字符串

  1. 字符串拼接方式(strings.Builder

    //直接使用+
    func Plus(n int, str string) string {
        s := ""
        for i := 0; i < n; i++ {
            s += str
        }
        return s
    }
    //使用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()
    }
    //使用bytes.Buffer
    func ByteBuffer(n int, str string) string {
        buf := new(bytes.Buffer)
        for i := 0; i < n; i++ {
            buf.WriteString(str)
        }
        return buf.String()
    }
    //使用strings.Builder预分配内存
    func PreStrBuilder(n int, str string) string {
        var builder strings.Builder
        builder.Grow(n * len(str))
        for i := 0; i < n; i++ {
            builder.WriteString(str)
        }
        return builder.String()
    }
    //使用bytes.Buffer预分配内存
    func PreByteBuffer(n int, str string) string {
        buf := new(bytes.Buffer)
        buf.Grow(n * len(str))
        for i := 0; i < n; i++ {
            buf.WriteString(str)
        }
        return buf.String()
    }
    ​
    ​
    

    image-20230126181344040.png 性能差异原因:

    • string是不可变类型,每次拼接的都要重新分配内存
    • bytes.Buffer转为字符串会重新申请空间,strings.Builder直接将底层[]byte转为string
    • strings.Builderbytes.Buffer底层都是[]byte数组,根据内存扩容策略使得不需要每次拼接都扩容,所以预分配内存会更快

Struct 结构体

  1. 空结构体不占据任何内存空间,在某些场景下可以单做占位符来使用。

    可以用来实现set

    func EmptyStructMap(n int) {
        m := make(map[int]struct{})
    ​
        for i := 0; i < n; i++ {
            m[i] = struct{}{}
        }
    }
    //设置为bool会多占据一个字节
    func BoolMap(n int) {
        m := make(map[int]bool)
    ​
        for i := 0; i < n; i++ {
            m[i] = false
        }
    }
    

    image-20230126182000829.png

多线程使用atomic

使用atomic包自动实现变量的原子性,比使用加锁来实现效率高很多。

package benchatomic
​
import (
    "sync"
    "sync/atomic"
)
​
type atomicCounter struct {
    i int32
}
​
func AtomicAddOne(c *atomicCounter) {
    atomic.AddInt32(&c.i, 1)
}
​
type mutexCounter struct {
    i int32
    m sync.Mutex
}
​
func MutexAddOne(c *mutexCounter) {
    c.m.Lock()
    c.i++
    c.m.Unlock()
}
​

image-20230126182239743.png 原因:

  • 锁的实现是操作系统的系统调用来实现的,atomic操作是通过硬件实现,效率更高
  • sync.Mutex应该用来保护一段逻辑,保护变量使用atomic
  • 非数值操作可以使用atomic.Value,能承载一个interface{}

性能优化实践

  • 依靠数据而不是猜测
  • 定位最大瓶颈而不是细枝末节
  • 不要过早优化
  • 不要过度优化

使用分析工具pprof

  • 知道在什么地方耗费了多少CPU、内存
  • 可视化

flat == Cum没有调用其他函数

flat == 0函数中只调用了其他函数且没什么消耗

定位到消耗最大的函数是Eat

使用list命令来定位:list Eat

命令web可视化调用关系