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

60 阅读5分钟

高质量编程

什么是高质量?

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

  • 各种边界条件是否考虑完备
  • 异常情况处理,稳定性保证
  • 易读易维护

编程原则

简单性

  • 消除多余的复杂性,以简单清晰的逻辑编写代码
  • 不理解的代码无法修复改进

可读性

  • 代码是写给人看的,而不是机器
  • 编写可维护代码的第一步是确保代码可读

生产力

  • 团队整体工作效率非常重要

编程规范

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

代码格式

  1. gofmt Go语言官方提供的工具,能自动格式化Go语言代码为官方统一风格。
  2. goimports Go语言官方的另一个工具,实际等于gofmt加上依赖包管理,自动增删依赖的包引用、将依赖包按照字母排序并分类

注释

  1. 注释应该解释代码作用
  2. 注释应该解释代码如何做的
  3. 注释应该解释代码实现的原因
  4. 注释应该解释代码什么情况会出错

命名规范

  1. 简洁胜于冗长
  2. 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
  3. 变量距离其被使用的地方越远,则需要携带更多的上下文信息

function

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

package

  1. 只由小写字母组成。不包含大写字母和下划线等字符。
  2. 简短并包含一定的上下文信息。
  3. 不要与标准库同名

控制流程

  1. 避免嵌套,保持正常流程清晰。
  2. 尽量保持正常代码路径为最小缩减

错误和异常处理

简单错误

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

错误的Wrap和Unwrap

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

Go1.13在errors中新增了三个新API和一个新的format关键字,分别为errors.Is、errors.As、errors.Unwrap以及fmt.Errorf的**%w**

错误判断

  1. 判定一个错误是否为特定错误,使用errors.Is
  2. 不同于使用==,使用该方法可以判断错误链上的所有错误是否含有特定的错误

错误判定

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

Panic

  1. 不建议来业务代码中使用panic
  2. 调用函数不包含recover会造成程序崩溃
  3. 若问题可以被屏蔽或解决,建议使用error替代panic
  4. 当程序启动阶段发送不可逆转的错误时,可以在init或main函数中使用panic

recover

  1. recover只能在被defer的函数中使用
  2. 嵌套无法生效
  3. 只能在当前goroutine生效
  4. defer的语句是后进先出

性能优化

  • 性能优化的前提是满足正确可靠、简洁清晰等质量因素
  • 性能优化是综合评估,有时候时间效率和空间效率可能对立

Benchmark

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

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

go test -bench=. -benchmem

Slice

slice预分配内存,尽可能在使用make()初始化切片时提供容量信息

切片本质是一个数组片段的描述,包括数组指针、片段的长度、片段的容量(不改变内存分配情况下的最大长度)

切片操作并不会复制切片指向的元素

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

type slice struct{
    array unsafe.Pointer
    len int
    cap int
}

Slice的陷阱:大内存未释放

Map

map预分配内存

  1. 不断向map中添加元素的操作会触发map的扩容
  2. 提前分配好空间可以减少内存拷贝和Rehash的消耗

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

字符串处理

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

原因

  1. 字符串在Go语言中是不可变类型,占用内存大小是固定的
  2. 使用+每次都会重新分配内存
  3. strings.Builder,bytes.Buffer底层都是[]byte数组
  4. 内存扩容策略,不需要每次拼接重新分配内存

结构体

使用空结构体可以节省内存,空结构体struct{}实例不占据任何的内存空间,可以作为各种场景下的占位符使用,这样有两个好处

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

atomic包

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

总结

  • 避免常见的性能陷阱可以保证大部分程序的性能
  • 普通应用代码,不要一位追求程序的性能
  • 越高级的性能优化手段越容易出现问题
  • 在满足正确可靠、简洁清晰的质量的前提下提高程序性能