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

51 阅读7分钟

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

考虑方面

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

原则

  • 简单性
    • 消除"多余的复杂性",以简单清晰的逻辑编写代码
      • 复杂的程序逻辑无法明确预知重构和优化带来的影响范围
    • 不理解的代码无法修复改进
      • 排查问题时难以定位,不知道如何修复
  • 可读性
    • 代码是写给人看的,而不是机器
      • 在项目迭代的过程中
        • 大部分工作是对已有功能的完善或拓展,基本不会完全下线某个功能
        • 意味着已上线的代码在其生命周期内会被不同的人阅读很多次
  • 生产力
    • 工程编程,大多是团队合作
    • Go 语言通过工具强制统一所有的代码格式,降低了新成员上手项目的成本

如何编写高质量的 Go 代码

  • 代码格式

    • gofmt
      • 使用 gofmt 自动格式化 Go 代码为官方统一风格
    • goimports
      • 等于 gofmt 加上依赖包管理
      • 能够自动增删依赖的包引用,将依赖包按字母序排序并分类
  • 注释

    • 原则
      • 代码就是最好的注释
      • 注释应该提供代码未表达出的上下文信息
    • 提供的信息
      • 解释代码作用
      • 解释代码如何做的
        • 对复杂的,并不明显的逻辑进行说明
        • 显而易见的流程是不需要的
      • 解释代码实现的原因
        • 解释代码的外部因素(脱离上下文后很难理解的因素)
        • 在只通过代码无法确定为什么这样实现的情况下,给出上下文说明
      • 解释代码什么情况会出错
        • 提示一些潜在的使用条件,或者无法处理的情况
    • 公共符号始终要注释
      • 包中声明的每个公共的符号:变量,常量,函数以及结构都需要添加注释
      • 任何既不明显也不简短的公共功能必须予以注释
      • 无论长度或复杂程度如何, 对库中的任何函数都必须进行注释
      • 例外
        • 不需要注释实现接口的方法
  • 命名规范

    • 变量
      • 简洁
        • 额外冗长没有增加对程序的理解时,替换,比如循环内部可以直接使用i,而不是index
      • 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
        • 例如使用 ServeHTTP 而不是 ServeHttp
        • 使用 XMLHTTPRequest 或者 xmlHTTPRequest
      • 变量距离其被使用的地方越远,则需要携带越多的上下文信息
        • 全局变量往往需要更多的上下文信息
        • 函数提供给外部调用时,签名的信息很重要,有助于帮助使用者理解函数
    • 函数
      • 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的
        • 比如调用 http 包中的 Server 方法时,使用 http. Server 而不是 http. ServerHTTP
      • 函数名尽量简短
      • 当名为 foo 的包某个函数返回类型 Foo 时,可以省略类型信息而不导致歧义
      • 当名为 foo 的包某个函数返回类型 T 时 (T 并不是 Foo),可以在函数名中加入类型信息
      • 只由小写字母组成。不包含大写字母和下划线等字符
      • 简短并包含一定的上下文信息。例如 schema、task 等
      • 不要与标准库同名。例如不要使用 sync 或者 strings
      • 其他规则
        • 不使用常用变量名作为包名。例如使用 bufio 而不是 buf
        • 使用单数而不是复数。例如使用 encoding 而不是 encodings
        • 谨慎地使用缩写。例如使用 fmt 在不破坏上下文的情况下比 format 更加简短
  • 控制流程

    • 原则
      • 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支
      • 正常流程代码沿着屏幕向下移动
      • 提升代码可维护性和可读性
      • 故障问题大多出现在复杂的条件语句和循环语句中
    • 避免嵌套,保持正常流程清晰
      • 如果两个分支都有 return ,可以删去冗余的 else
    • 尽量保持正常代码路径未最小缩进
      • 优先处理错误情况/特殊情况,尽早返回来减少嵌套
  • 错误和异常处理

    • 简单错误
      • 只在一个地方出现且仅出现一次,使用 error. New 来创建匿名变量表示简单错误
      • 如果需要格式化,可以使用 fmt. Errorf
    • 错误的 Wrap 和 Unwrap
      • 错误的 Wrap 实际上是提供了一个 error 嵌套另一个 error 的能力,从而生成一个 error 的跟踪链
      • 在 fmt. Errorf 中使用: %w 关键字来将一个错误关联至错误链中
    • 错误判定
      • errors. Is :判定一个错误是否为特定错误
        • == 不同,该方法可以判定错误链上的所有错误是否含有特定的错误
      • errors. As:获取错误链上特定种类的错误
    • Panic(比错误更严重,表示程序无法正常工作了)
      • 不建议在业务代码中使用 panic
        • panic 发生后,会向上传播至调用栈顶,如果当前 goroutine 中所有 deferred 函数都不包含 recover 就会导致程序崩溃
      • 若问题可以被屏蔽或解决,建议使用 error 代替 panic
      • 如果在程序启动阶段会发生不可逆转的错误时,可以在 init 函数或 main 函数中使用 panic,因为在这种情况下,服务启动起来也没有意义!
    • Recover
      • recover 只能在被 defer 的函数中使用
      • 嵌套无法生效
      • 只在当前 goroutine 生效
      • defer 的语句是后进先出
      • 如果需要更多的上下文信息,可以 recover 后在 log 中记录当前的调用栈

性能优化建议

  • 概述
    • 性能优化的前提是满足正确可靠、简洁清晰等质量因素
    • 性能优化是综合评估,有时候时间效率和空间效率可能对立
  • Go 自带的性能评估工具
    • benchmark:go test -bench=. -benchmem
      • -benchmem 表示统计内存的使用情况
  • Slice 的使用建议
    • 预分配内存:尽量在 make 初始化切片时提供容量信息
      • 切片操作并不复制切片指向的元素
      • 创建新的切片会复用原来切片的底层数组
        • 在已有切片基础上创建切片,如果原切片较大,即使新建切片较小,原数组在内存中有引用,无法被释放
        • 可用 copy 代替
      • 切片的结构
        • 一个指向底层[]byte的指针
        • 一个int表示容量
        • 一个int表示长度
  • Map 的使用建议
    • 预分配内存
      • 分析
        • 不断向 map 中添加元素的操作会触发 map 的扩容
        • 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
        • 建议根据实际需求提前预估好需要的空间
  • 字符串处理的使用建议
    • 字符串拼接
      • 结论:使用 strings.Builder
      • 拼接方式:
        • +
        • strings.Builder :直接将底层的[]byte 转换成了字符串类型返回
        • bytes.Buffer :转换为字符串时重新申请了一块空间
        • strings.Builder +预分配
        • bytes.Buffer +预分配
      • 效果对比 Pasted image 20230119154757.png
      • 分析
        • 字符串在 Go 语言中是不可变类型,占用内存大小是固定的
        • 使用+每次都会重新分配内存
        • strings. Builder,bytes. Buffer 底层都是[]byte 数组内存
        • 扩容策略,不需要每次拼接重新分配内存
  • 空结构体的使用建议
    • 节省内存,空结构体实例 struct{} 不占据任何内存,可作为占位符使用
    • 节省资源,空结构体本身具备很强的语义
    • 通过 map 和空结构体来实现 set,只需要使用键,不需要值,即使将 map 的值设置为 bool,也会多占据 1 个字节空间
  • atomic 包的使用建议
    • 锁的实现是通过操作系统来实现,属于系统调用
    • atomic 操作是通过硬件实现,效率比锁高
    • sync. Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
    • 对于非数值操作,可以使用 atomic. Value,能承载一个 interface{}
    • 使用例子Pasted image 20230119165730.png