这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天
考虑方面
- 各种边界条件是否考虑完备
- 异常情况处理,稳定性保证
- 易读易维护
原则
- 简单性
- 消除"多余的复杂性",以简单清晰的逻辑编写代码
- 复杂的程序逻辑无法明确预知重构和优化带来的影响范围
- 不理解的代码无法修复改进
- 排查问题时难以定位,不知道如何修复
- 消除"多余的复杂性",以简单清晰的逻辑编写代码
- 可读性
- 代码是写给人看的,而不是机器
- 在项目迭代的过程中
- 大部分工作是对已有功能的完善或拓展,基本不会完全下线某个功能
- 意味着已上线的代码在其生命周期内会被不同的人阅读很多次
- 在项目迭代的过程中
- 代码是写给人看的,而不是机器
- 生产力
- 工程编程,大多是团队合作
- Go 语言通过工具强制统一所有的代码格式,降低了新成员上手项目的成本
如何编写高质量的 Go 代码
-
代码格式
- gofmt
- 使用 gofmt 自动格式化 Go 代码为官方统一风格
- goimports
- 等于 gofmt 加上依赖包管理
- 能够自动增删依赖的包引用,将依赖包按字母序排序并分类
- 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:获取错误链上特定种类的错误
- errors. Is :判定一个错误是否为特定错误
- Panic(比错误更严重,表示程序无法正常工作了)
- 不建议在业务代码中使用 panic
- panic 发生后,会向上传播至调用栈顶,如果当前 goroutine 中所有 deferred 函数都不包含 recover 就会导致程序崩溃
- 若问题可以被屏蔽或解决,建议使用 error 代替 panic
- 如果在程序启动阶段会发生不可逆转的错误时,可以在 init 函数或 main 函数中使用 panic,因为在这种情况下,服务启动起来也没有意义!
- 不建议在业务代码中使用 panic
- Recover
- recover 只能在被 defer 的函数中使用
- 嵌套无法生效
- 只在当前 goroutine 生效
- defer 的语句是后进先出
- 如果需要更多的上下文信息,可以 recover 后在 log 中记录当前的调用栈
- 简单错误
性能优化建议
- 概述
- 性能优化的前提是满足正确可靠、简洁清晰等质量因素
- 性能优化是综合评估,有时候时间效率和空间效率可能对立
- Go 自带的性能评估工具
- benchmark:
go test -bench=. -benchmem-benchmem表示统计内存的使用情况
- benchmark:
- Slice 的使用建议
- 预分配内存:尽量在 make 初始化切片时提供容量信息
- 切片操作并不复制切片指向的元素
- 创建新的切片会复用原来切片的底层数组
- 在已有切片基础上创建切片,如果原切片较大,即使新建切片较小,原数组在内存中有引用,无法被释放
- 可用 copy 代替
- 切片的结构
- 一个指向底层[]byte的指针
- 一个int表示容量
- 一个int表示长度
- 预分配内存:尽量在 make 初始化切片时提供容量信息
- Map 的使用建议
- 预分配内存
- 分析
- 不断向 map 中添加元素的操作会触发 map 的扩容
- 提前分配好空间可以减少内存拷贝和 Rehash 的消耗
- 建议根据实际需求提前预估好需要的空间
- 分析
- 预分配内存
- 字符串处理的使用建议
- 字符串拼接
- 结论:使用
strings.Builder - 拼接方式:
+strings.Builder:直接将底层的[]byte 转换成了字符串类型返回bytes.Buffer:转换为字符串时重新申请了一块空间strings.Builder+预分配bytes.Buffer+预分配
- 效果对比
- 分析
- 字符串在 Go 语言中是不可变类型,占用内存大小是固定的
- 使用+每次都会重新分配内存
- strings. Builder,bytes. Buffer 底层都是[]byte 数组内存
- 扩容策略,不需要每次拼接重新分配内存
- 结论:使用
- 字符串拼接
- 空结构体的使用建议
- 节省内存,空结构体实例
struct{}不占据任何内存,可作为占位符使用 - 节省资源,空结构体本身具备很强的语义
- 通过 map 和空结构体来实现 set,只需要使用键,不需要值,即使将 map 的值设置为 bool,也会多占据 1 个字节空间
- 节省内存,空结构体实例
- atomic 包的使用建议
- 锁的实现是通过操作系统来实现,属于系统调用
- atomic 操作是通过硬件实现,效率比锁高
- sync. Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
- 对于非数值操作,可以使用 atomic. Value,能承载一个 interface{}
- 使用例子