程序优化 | 青训营笔记

114 阅读8分钟

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

高质量编程

高质量代码,编写的代码能够达到正确可靠、简洁清晰的目标。

正确,各种边界条件是否考虑完备; 可靠,异常情况能否处理,能否保证稳定性; 简洁清晰,逻辑简单,易读易维护;

编程原则

虽然实际应用场景,各种语言特性语法不同,但是高质量编程遵循的原则是相通的。

  • 简单性,消除“多余的复杂性”,以简单清晰的逻辑编写代码;不理解的代码难以修复改进
  • 可读性,代码是写给人看的,而不是机器;编写可维护代码的第一步是确保代码可读
  • 生产力,团队整体工作效率非常重要

实践规范

代码格式

官方提供的格式化工具

go fmt:自动格式化代码,常见IDE都支持配置

go imports:等于go fmt加上依赖包管理,可以自动增删依赖的包引用,将依赖包按字母排序并分类

注释

  • 注释应该解释代码作用。 适合注释公共符号,变量、常量、函数以及结构体
  • 注释应该解释代码如何做的。 适合注释实现过程
  • 注释应该解释代码实现的原因。 适合解释代码的外部因素,提供额外上下文
  • 注释应该解释代码什么情况下会出错。 适合解释代码的限制条件

无论长度或复杂度如何,任何函数都应该进行注释。实现接口的方法不需要注释

命名规范

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

    简洁;

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

    变量距离使用位置越远,则应携带越多的上下文信息,全局变量;

  2. 函数

    不携带包名的上下文信息;

    尽量简短;

    当函数提供外部调用时,签名信息应该详细

  3. 只由小写字母组成,不包含大写字母和下划线;

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

    不要与标准库同名;

    不使用常用变量名作为包名;

    使用单数而不是复数;

    谨慎使用缩写

  4. 控制流程

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

    尽量保持正常代码路径为最小缩进,优先处理错误情况/特殊情况

    线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支

  5. 错误和异常处理

    • 简单错误

      仅出现一次,且在其他地方不需要捕获该错误;

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

      如果有格式化要求,使用fmt.Errof()

    • 复杂错误

      错误的Wrap,提供一个error嵌套另一个error的能力,从而生成一个error的跟踪链,好处是每层调用方可以补充自己对应的上下文信息,方便跟踪排查问题;

      fmt.Errof中使用"%w"将一个错误关联至错误链中

      if err!=nil{
          return fmt.Errof("reading error:%w",err)
      }
      

      判断错误链上的所有错误是否含有特定错误,使用errors.Is()

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

  6. panic

    不建议在业务代码中使用panic,若问题可以被屏蔽或解决,建议使用error代替panic;

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

  7. recover

    出现panic后可使用reover记录上下文信息,可以在recover后在log中记录当前的调用栈debug.Stack(),方便分析定位问题

    只能在defer函数中使用;

    defer函数:嵌套无法生效;只在当前goroutine生效;多个defer函数后进先出

        func (t treeFS)Open(name string)(f fs.File,err error){
            defer func(){
                if e:=recover();e!=nil{
                    f=nil
                    err=fmt.Errof("gitfs panic:%v\n%s",e,debug.Stack())
                }
            }
        }
    
  8. 小结
    error 尽可能提供简明的上下文信息链,方便定位问题

    panic 用于真正异常的情况

    recover 在当前goroutine的defer函数中生效

性能调优

性能优化的前提是满足正确可靠、简洁清晰等质量因素

性能优化是综合评估,需要综合考虑时间效率和空间效率

评估性能

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

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

基准测试的代码文件必须以_test.go结尾

基准测试的函数必须以Benchmark开头,必须是可导出的

基准测试函数必须接受一个指向Benchmark类型的指针作为唯一参数

基准测试函数不能有返回值

func BenchmarkSprintf(b *testing.B){
    num:=10
    b.ResetTimer()
    for i:=0;i<b.N;i++{
        fmt.Sprintf("%d",num)
        }
   }

b.ResetTimer是重置计时器,这样可以避免for循环之前的初始化代码的干扰。最后的for循环很重要,被测试的代码要放到循环里。b.N是基准测试框架提供的,表示循环的次数,因为需要反复调用测试的代码,才可以评估性能。

go test -bench=. -benchmem //-bench=,接受一个表达式作为参数,.表示运行所有的基准测试,因为默认情况下 go test 会运行单元测试;-benchmem可以提供每次操作分配内存的次数,以及每次操作分配的字节数,找到根本原因。

优化建议

  • slice预分配内存

    尽可能在使用make()初始化切片时提供容量

    大切片基础上创建小切片,使用copy(des,src),避免大内存未释放

  • map预分配内存

    根据需求提前预估好需要的空间,因为map扩容会有内存拷贝和Rehash的消耗

  • 字符串处理

    使用strings.Buider;使用+拼接性能最差,strings.Builder和bytes.Buffer相近,bytes.Buffer更快;

    原因:字符串在 Go 语言中是不可变类型,占用内存大小是固定的,使用 + 每次都会重新分配内存。strings.Builder,bytes.Buffer 底层都是[]byte 数组,内部维护内存扩容策略,不需要每次拼接重新分配内存。bytes.Buffer 转化为字符串时重新申请了一块空间,strings.Builder 直接将底层的[]byte 转换成了字符串类型返回。

  • 空结构体

    使用空结构体节省内存,空结构体struct{}不占内存空间,可作为各种场景下的占位符使用

    map+空结构体可以实现set

  • atomic包

    多线程情境下,可用于保证计数安全,线程安全,比加锁时间性能好

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

小结

避免常见的性能陷阱就可以保证大部分程序的性能;

普通应用代码,不要一味地追求程序的性能;

越高级的性能优化手段越容易出现问题;

在满足正确可靠、简洁清晰的质量要求的前提下提高程序性能

调优实战

性能调优原则

要依靠数据而不是猜测;

要定位最大瓶颈而不是细枝末节;

不要过早优化;

不要过度优化

性能分析工具pprof

pprof是用于可视化和分析性能的工具,可以知道程序在什么地方耗费了多少CPU,memory

  1. 运行代码

    pprof浏览器查看指标,参考

  2. CPU排查

    命令行输入 go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10" 。 将一段时间采集到信息输出到文件中,采集10s

    • top命令:查看占用资源最多的函数。

      每列指标含义: flat,当前函数本身的执行耗时;flat%,flat占CPU总时间的比例;sum%,上面每一行的flat%总和;cum,当前函数本身加上其调用函数的总耗时;cum%,cum占CPU总时间的比例。 flat=cum,当前函数没有调用其他函数;flat=0,当前函数只有其他函数的调用。 1674120468072.png

    • list命令

      根据指定的正则表达式查找代码行

    • web命令

    调用关系可视化 需安装graphviz,把graphviz/bin目录配置到path环境变量,在IDE中使用cmd终端;

  3. Heap堆内存排查

    go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"

    web页面可视化,VIEW标签下的source可以看到每行代码占用内存;

    SAMPLE标签下alloc_objects: 程序累计申请的对象数;alloc_space: 程序累计申请的内存大小;inuse_objects: 程序当前持有的对象数;inuse_space: 程序当前占用的内存大小

  4. goroutine协程

    goroutine泄露也会导致内存泄漏

    浏览器打开http://localhost:6060/debug/pprof/ 可看到运行协程数

    go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine" 再查看火焰图,由上到下表示调用顺序;每一块代表一个函数,越长代表占用 CPU 的时间更长;火焰图是动态的,支持点击块进行分析

  5. mutux-锁

    go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex" 确定有问题的方法,source视图下确定出问题的代码

总结

了解了编码规范,注释,流程结构,命名规范等。学会使用性能分析工具pprof,可以通过命令或网页查看定位资源占用问题,进行性能优化。