高质量编程 | 青训营笔记

83 阅读7分钟

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

01 本堂课重点内容

  • 高质量编程

    • 编码规范
    • 常用Go语言程序优化手段

02 详细知识点介绍

2.1 高质量编程

  • 写出更简洁清晰的代码

    • 简洁:尽可能简单的逻辑——便于后续调整、新增功能
    • 清晰:其余团队成员可以放心的重构、优化、新增功能
    • 在工作中,编程是团队合作的工程,好的代码让其他人更容易在你的基础上开发,同时出问题的概率更低,大家更乐于与你合作,也让团队更高效
    • 另外在面试的时候,也有编码环节,能不能用代码清晰的表达出你的思路,让面试官额外加分
  • 写出正确可靠的代码

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

2.1.1 Dave Cheney的编程原则

  • 简单性

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

    • 代码是写给人看的,而不是机器

    • 编写可维护代码的第一步是确保代码可读

    • 在项目不断迭代的过程中,大部分工作是对已有功能的完善或扩展,很少会完全下线某个功能,对应的功能代码实际会生存很长时间。

      • 已上线的代码在其生命周期内会被不同的人阅读几十上百次,听课时老师经常说的在课堂上不遵守纪律影响全班同学的时间,难以理解的代码会占用后续每一个程序员的时间
  • 生产力

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

      • 为了降低新成员上手项目代码的成本,Go语言甚至通过工具强制统一所有代码格式

      • 编码在整个项目开发链路中的一个节点,遵循规范,避免常见缺陷的代码能够降低后续联调、测试、验证、上线等各个节点的出现问题的概率,就算出现问题也能快速排查定位

2.1.2 实践编码规范

  • 代码格式

    • 使用gofmt自动格式化代码
    • goimports = gofmt + 依赖包管理
  • 注释

    • 任何既不明显也不简短的公共功能(常量、方法)必须注释

    • 无论长度或复杂度如何,库中的任何函数都必须进行注释

    • 有一个例外,不需要注释实现接口的方法

    • 注释应该做的

      • 解释代码作用

      • 代码实现的过程

      • 代码实现的原因

        • 外部因素
        • 上下文
      • 代码什么时候会出错

        • 解释代码的限制条件
  • 命名规范

    • 变量

      • 简洁胜于冗长

      • 缩略词全大写,但当其位于变量开头并且不需要导出的时候,全小写

        • e.g. 使用ServerHTTP而不是ServerHttp,使用 XMLHTTPRequest 或 xmlHTTPRequest
      • 变量距离被使用的地方越远,需要携带更多的context info

        • e.g. 全局变量——以便在不同的地方可以认出含义
        • image.png
        • image.png
    • func

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

      • 由小写字母组成,不包括下划线
      • 简短并包含一定上下文信息:e.g. schema、task
      • 不要与标准库同名
      • 不使用常用变量名作为包名:e.g. buffo而不是buf
      • 使用单数而不是复数
  • 控制流程

    • 避免嵌套,保证流程清晰(线性原理)
      • e.g. 如果两个分支中都包含return语句,则可以去除冗余的elseimage.png
    • 优先处理错误/特殊情况
      • 以尽早返回或继续循环来减少嵌套
      • e.g. image.png
  • 错误和异常处理

    • 简单错误
      • 仅出现一次的错误,且在其他地方不需要捕获该错误
      • 优先使用errors.New来创建匿名变量来直接表示简单错误image.png
      • 如果有格式化的需求,使用fmt.Errorf
    • 错误的Wrap和Unwrap
      • 错误的包装和解包
      • 错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链
      • fmt.Errorf中使用%w来将一个错误关联至错误链中image.png
    • 错误判定
      • 判定一个错误是否为特定错误:errors.Isimage.png
      • 判定错误链上的所有错误是否含有特定的错误: ==
      • 在错误链上获取特定种类的错误:errors.Asimage.png
    • panic
      • panic 用于主动抛出错误,recover 用来捕获panic 抛出的错误
      • 引发 panic 有两种情况,一是程序主动调用,二是程序产生运行时错误,由运行时检测并退出
      • 不建议在业务代码中使用panic
      • 如果问题可以被屏蔽或者解决,建议使用 error 替代 panic
      • 当程序启动阶段发生不可逆转的错误时,可以在 initmain 函数中使用 panicimage.png
    • recover
      • 只能在被defer的函数中使用
      • 只在当前goroutine生效
        • 嵌套无法生效
      • defer 是先进先出的

image.png

image.png

2.1.3 性能优化建议

  • 性能优化的前提是满足正确可靠、简洁清晰等质量因素
  • 性能优化是综合评估,有时候时间效率和空间效率可能对立
    • 此时应当分析哪个更重要,作出适当的折衷。例如多花费一些内存来提高性能
  • Benchmark
    • 性能表现需要实际数据衡量
    • go test -bench==. benchmem
      • image.png
  • Slice
    • 预分配内存(容量),减少分配内存次数image.png
      • 切片操作并不复制切片指向的元素
      • 创建一个 新的切片会复用原来切片 的底层数组
    • 大内存未释放
      • 在已有切片基础上创建切片,不会创建新的底层数组
      • 场景
        • 原切片较大,代码在原切片基础上新建小切片image.png
        • 原底层数组在内存中有引用,得不到释放
  • Map预分配内存
    • 建议根据实际需求提前预估好需要的空间
  • 字符串拼接
    • 使用+拼接性能最差,strings.Builder, bytes. Buffer相近,strings. Buffer更快
      • bytes. Buffer转化为字符串时重新申请了一块空间
      • strings. Builder直接将底层的[]byte转换成了字符串类型返回
    • 字符串在Go语言中是不可变类型,占用内存大小是固定的
    • 使用+每次都会重新分配内存
    • strings.Builder, bytes. Buffer底层都是[]byte 数组
    • 内存扩容策略,不需要每次拼接重新分配内存
  • 使用空结构体节省内存
    • 作为占位符
    • 使用map实现set,只会用到map的key,用不到value,value使用bool代替也有1个Byte
type Set map[string]struct{}

func (s Set) Has(key string) bool {
	_, ok := s[key]
	return ok
}

func (s Set) Add(key string) {
	s[key] = struct{}{}
}

func (s Set) Delete(key string) {
	delete(s, key)
}

func main() {
	s := make(Set)
	s.Add("Tom")
	s.Add("Sam")
	fmt.Println(s.Has("Tom"))
	fmt.Println(s.Has("Jack"))
}

  • atomic包
    • 在工作中迟早会遇到多线程编程的场景,比如实现一个多线程共用的计数器,如何保证计数准确,线程安全
    • atomic包比加锁的方式开销更小
      • 锁的实现是通过操作系统来实现,属于系统调用
      • atomic操作是通过硬件实现,效率比锁高
      • sync.Mutex应该用来保护一段逻辑,不仅仅用于保护一个变量
      • 对于非数值操作,可以使用atomic.Value,能承载一个interface{}

03 引用参考 & 文章收藏