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

138 阅读8分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记

高质量编程

本文主要针对高质量的Go编程。

如何编写高质量的Go代码

人们普遍认可的“高质量”:

  • 正确性: 是否考虑各种边界条件,错误的调用是否能够处理
  • 可靠性: 异常情况或者错误的处理策略是否明确,依赖的服务出现异常是否能够处理
  • 简洁: 逻辑是否简单,后续调整功能或新增功能是否能够快速支持
  • 清晰: 其他人在阅读理解代码的时候是否能清楚明白,重构或者修改功能是否不会担心出现无法预料的问题

代码格式

  1. 注释
  2. 命名规范
  3. 控制流程
  4. 错误和异常处理

自动格式化工具

gofmt

  • Go语言官方提供的工具,能自动格式化Go语言代码为官方统一风格

goimports

  • 也是Go语言官方提供的工具实际等于gofmt加上依赖包管理

注释

  1. 注释应该解释代码作用:适合注释公共符号
  2. 注释应该解释代码如何做的:适合注释实现过程
  3. 注释应该解释代码实现的原因:适合解释代码的外部因素、提供额外上下文
  4. 注释应该解释代码什么情况会出错:适合解释代码的限制条件

注意:公共符号始终要注释包中声明的每个公共的符号:

  • 变量、常量、函数以及结构都需要添加注释
  • 任何既不明显也不简短的公共功能必须予以注释
  • 无论长度或复杂程度如何,对库中的任何函数都必须进行注释

命名规范

  1. 简洁胜于冗长

  2. 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写·例如使用ServeHTTP而不是ServeHttp

    ·使用XMLHTTPRequest或者xmlHTTPRequest

  3. 变量距离其被使用的地方越远,则需要携带越多的上下文信息

    ·全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义

  4. 变量名有特定的含义

控制流程

  1. 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
  2. 正常流程代码沿着屏幕向下移动
  3. 提升代码可维护性和可读性
  4. 故障问题大多出现在复杂的条件语句和循环语句中

错误和异常处理

  1. 简单错误:

    简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误优先使用errors.New来创建匿名变量来直接表示简单错误;如果有格式化的需求,使用fmt.Errorf

  2. 错误的Wrap和Unwrap:

    错误的Wrap 实际上是提供了一个error嵌套另一个error 的能力,从而生成一个error的跟踪链;在fmt.Errorf中使用: %w关键字来将一个错误关联至错误链中

  3. 错误判定:

    判定一个错误是否为特定错误,使用errors.ls;不同于使用==,使用该方法可以判定错误链上的所有错误是否含有特定的错误;在错误链上获取特定种类的错误,使用errors.As; error尽可能提供简明的上下文信息链,方便定位问题

  4. panic:

    不建议在业务代码中使用panic;调用函数不包含recover会造成程序崩溃若问题可以被屏蔽或解决,建议使用error代替panic;当程序启动阶段发生不可逆转的错误时,可以在init或main 函数中使用panic;panic用于真正异常的情况

  5. recover: recover只能在被defer的函数中使用;嵌套无法生效;只在当前goroutine生效defer的语句是后进先出; recover生效范围,在当前goroutine的被defer 的函数中生效

性能优化建议

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

go test -bench=. -benchmem

slice预分配内存

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

func NoPreAlloc( size int ) {
    data := make( [ ]int0)
    for k := 0; k < size; k++ {
        data = append( data,k )}
    }
func PreAlloc(size int ) {
    data := make( [ ]int0, size)
    for k := 0; k < size; k++ {
        data = append( data,k)}
    }

切片本质是一个数组片段的描述

  • 包括数组指针

  • 片段的长度

  • 片段的容量(不改变内存分配情况下的最大长度)切片操作并不复制切片指向的元素

创建一个新的切片会复用原来切片的底层数组

type slice struct i
    array unsafe.Pointerlen int
    cap int
}

另一个陷阱:大内存未释放

在已有切片基础上创建切片,不会创建新的底层数组

场景

  • 原切片较大,代码在原切片基础上新建小切片

  • 原底层数组在内存中有引用,得不到释放

可使用copy替代re-slice

map预分配内存

不断向map中添加元素的操作会触发map的扩容

提前分配好空间可以减少内存拷贝和Rehash的消耗

建议根据实际需求提前预估好需要的空间

func PreAlloc( size int ) {
    data := make( map[ int]int, size)for i := 0; i < size; i++ {
    data[i] = 1
    }
}

字符串处理

+ / strings.Builder / bytes.Buffer

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

分析

字符串在Go语言中是不可变类型,占用内存大小是固定的

使用+每次都会重新分配内存

strings.Builder,bytes.Buffer 底层都是[]byte 数组

bytes.Buffer 转化为字符串时重新申请了一块空间

strings.Builder 直接将底层的[]byte转换成了字符串类型返回

进一步优化 :Grow( n * len( str ) )

内存扩容策略,不需要每次拼接重新分配内存

空结构体

使用空结构体节省内存

空结构体struct(}实例不占据任何的内存空间可作为各种场景下的占位符使用

节省资源
空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符

使用atomic包

锁的实现是通过操作系统来实现,属于系统调用

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

sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量

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

性能调优

原则

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

性能分析工具pprof

  1. 希望知道应用在什么地方耗费了多少CPU、Memory
  2. pprof是用于可视化和分析性能分析数据的工具

C3.1.png

CPU

命令:topN

查看占用资源最多的函数

flat
   当前函数本身的执行耗时
flat %
    flat占CPU总时间的比例
sum %
    上面每一行的flat%总和cum,指当前函数本身加上其调用函数的总耗时
cum %
    cum占CPU总时间的比例

原理

操作系统
    每10ms向进程发送一次SIGPROF信号
进程
    每次接收到SIGPROF会记录调用堆栈
写缓冲
    每100ms读取已经记录的调用栈并写入输

命令:list

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

命令: web

调用关系可视化

Heap-堆内存

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

原理

采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
采样率:每分配512KB记录一次,可在运行开头修改,1为每次分配均记录
采样时间:从程序运行开始到采样时
采样指标: alloc_space, alloc_objects,inuse_space, inuse_objects
计算方式: inuse= alloc- free

goroutine-协程

goroutine泄露也会导致内存泄露

原理

Goroutine
    ·记录所有用户发起且在运行中的goroutine(即入口非runtime开头的)runtime.main的调用栈信息
ThreadCreate
    ·记录程序创建的所有系统线程的信息

C3.4.png

pprof-采样过程

C3.2.png

业务服务

基本概念

  • 服务:能单独部署,承载一定功能的程序依赖:Service A的功能实现依赖
  • ServiceB的响应结果,称为Service A依赖Service B
  • 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
  • 基础库:公共的工具包、中间件

流程

  • 建立服务性能评估手段
  • 分析性能数据,定位性能瓶颈
  • 重点优化项改造
  • 优化效果验证

建立服务性能评估手段

服务性能评估方式

  • 单独benchmark无法满足复杂逻辑分析
  • 不同负载情况下性能表现差异请求流量构造
  • 不同请求参数覆盖逻辑不同
  • 线上真实流量情况 压测范围
  • 单机器压测
  • 集群压测 性能数据采集
  • 单机性能数据
  • 集群性能数据

参考文献

[1] Go语言圣经(中文版) books.studygolang.com/gopl-zh/ch0…

[2] 青训营PPT