day3-高质量编程与性能调优实战 | 青训营笔记

67 阅读6分钟

这是我参加青训营的第 3 天

高质量编程

代码规范

变量命名规范

· 简洁胜于冗长

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

· 例如使用 ServerHTTP 而不是 ServerHttp

· 使用XMLHTTPRequest 或者 xmlHTTPRequest

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

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

函数命名规范

· 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现

· 函数名尽量简短

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

· 当名为 foo 的包某个函数返回类型 T 时(T 并不是 Foo),可以在函数名中加入类型信息

package命名规范

· 只由小写字母组成。不包含大写字母和下划线等字符

· 简短包含一定的上下文信息。例如 schema、task等

· 不要与标准库同名。例如不要使用 sync 或者 strings

尽量满足:

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

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

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

错误和异常处理

Go1.13errors 中新增了三个新API和一个新的 format 关键字,分别是 errors.ls errors.Aserrors.Unwrap 以及 fmt.Errorf 的 %w

如果项目运行在小于Go1.13的版本中,导入 golang.org/x/xerrors 来使用

fmt.Errorf

在 fmt.Errorf 中使用: %w 关键字将一个错误关联至错误链中,示例:

return fmt.Errorf("reading srcfiles list: %w", err)

errors.Is

判断一个错误是否为特定错误

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

示例:

if errors.Is(err, fs.ErrNotExist) {
    return []byte{}, nil
}

errors.As

在错误链上获取特定种类的错误,示例:

if errors.As(err, &pathError)   // 获取pathError的特定错误

panic

用于真正的异常情况

· 不建议在业务代码中使用 panic

· 调用函数不包含 recover 会造成程序崩溃

· 若问题可以被屏蔽或解决,建议使用 error 代替 panic

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

// Paincf is equivalent to Printf() followed by a call to panic().
func Panicf(format string, v ...interface{}) {
    s := fmt.Sprintf(format, v...)
    std.Output(2, s)
    panic(s)
}

recover

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

· 嵌套无法生效

· 只在当前 goroutine 生效

· defer 的语句是后进先出

重要一点:如果需要更多的上下文信息,可以 recover 后在 log 中记录当前的调用栈

func (t *treeFS) Open(name string) (f fs.File, err error) {
    defer func() {
        if e := recover(); e != nil {
            f = nil
            err = fmt.Errorf("gitfs panic: %v\n%s", e, debug.Stack())
        }
    }()
    //……
}

defer

1. defer 语句会在函数返回前调用

2. 多个 defer 语句都是后进先出

func main() {
    if true {
        defer fmt.Println("1")
    } else {
        defer fmt.Println("2")
    }
    defer fmt.Println("3")
}       // 输出结果为
        // 3
        // 1

性能优化

Benchmark

运行语句:

go test -bench=. -benchmem
func Fib(n int) int {
    if n < 2 {
        return n
    }
    return Fib(n-1) + Fib(n-2)
}
​
func BenchmarkFib10(b *testing.B) {
    for n := 0; n < b.N; n++ {
        Fib(10)
    }
​
}

slice

预分配内存,在使用make() 初始化切片时提供容量信息。这样可以使代码效率提高百分之三十往上

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

大内存未释放

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

· 可使用 copy 代替 re-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
}
// 内存使用情况可以减轻将近97%

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()
}
​
// 当预先知道字符串长度时,使用预处理来优化
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()
}

空结构体

· 空结构体 struct{} 实例不占据任何的内存空间

· 可作为各种场景下的占位符使用

· 节省资源

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

func BoolMap(n int) {
    m := make(map[int]bool)
    
    for i := 0; i < n; i++ {
        m[i] = false
    }
}
​
// 使用空结构体改进
func EmptyStructMap(n int) {
    m := make(map[int]struct{})
    
    for i := 0; i < n; i++ {
        m[i] = struct{}{}
    }
}

atomic 包

当单变量进行变化的时候,一般会选择来加锁对变量数值进行保护

但是加锁主要是保护一段逻辑,单纯保护一个变量的变化情况会过于浪费

使用 atomic 包可以解决这个问题,并且效率还高于加锁

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

// 高效率,不浪费
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()
}

性能优化工具

pprof

运行命令:

go tool pprof URL

示例:

go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"    // 获取指标

命令: topN

查看占用资源最多的函数

· Flat == Cum,函数中没有调用其他函数

· Flat == 0, 函数中只有其他函数的调用

命令:list

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

list Eat

业务优化

流程

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

性能评估手段

· 服务性能评估方法

· 单独 benchmark 无法满足复杂逻辑分析

· 不同负载情况下性能表现差异

· 请求流量构造

· 不同请求参数覆盖逻辑不同

· 线上真实流量情况

· 压测范围

· 单击器压测

· 集群压测

· 性能数据采集

· 单机性能数据

· 集群性能数据

分析性能数据,定位性能瓶颈

· 使用库不规范

· 高并发场景优化不足

重点优化项改造

· 正确性是基础,在优化代码的情况下,一定要保证正确性为前提

· 响应数据 diff

· 线上请求数据录制回放

· 新旧逻辑接口数据 diff

优化效果验证

· 重复压测验证

· 上线评估优化效果

关注服务监控

逐步放量

收集性能数据

服务整体链路分析

· 规范上游服务调用接口,明确场景需求

· 分析链路,通过业务流程优化提升服务性能

AB实验SDK的优化

分析基础库核心逻辑和性能瓶颈

设计完善改造方案

数据按需获取

数据序列化协议优化

内部压测验证

推广业务服务落地验证