Go编码规范 | 青训营笔记

61 阅读6分钟

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

高质量编程与性能调优

高质量编程

我们编写的代码主要有三个主要的评判标准:

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

编程原则

简单性

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

可读性

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

生产力

  • 团队整体工作效率十分重要

编码规范

gofmt自动格式化代码,或者goimport(相当于gofmt加上包依赖管理)

注释

注释是用来:

  • 解释代码的作用

    注释公共符号,如对变量的注释,对函数的注释,或者是库中对外公开的函数的注释。

  • 解释代码如何运行

    注释功能的实现过程,对复杂的逻辑需要进行说明

  • 解释代码为什么要这么做

    解释代码的外部影响因素,并提供额外的上下文信息,帮助阅读者理解代码为什么要这么做

  • 解释代码何时会有错误

    解释代码的限制条件

对于公共符号:

  • 包中声明的每一个公共符号:变量常量函数以及结构都需要添加注释
  • 任何既不明显也不简短的公共功能必须注释
  • 无论长度或复杂度如何,库中对外的提供的任何函数也需要注释(文档)
  • 不需要注释实现接口的方法

总的来说,代码是最好的注释。注释要提供代码未表达出的上下文信息,来帮助阅读者了解代码所处于的位置功能,与实现细节。可以帮助阅读者对单个函数或者整个项目有更好的了解。

命名规范

变量:

  • 简介比冗长好

    例如在for循环中定义一个下标。使用iindex要好

  • 缩写词需要大写,如果缩写词在变量开头,且不需要导出时,可以使用小写

    ServerHTTP,xmlHTTPRequest

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

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

函数:

  • 函数名不需要携带包的上下文信息,因为包名和函数总是成对出现
  • 函数名尽可能简短

包:

  • 只由小写字母组成,不包含大写字母和下划线
  • 简短并且包含一定的上下文信息
  • 不用于标准库重名

包要以下要求尽量满足:

  • 不使用常用变量名作为包名
  • 使用单数而不是复数
  • 谨慎使用缩写

核心目标时降低阅读理解代码的成本,重点考虑上下文信息,设计简洁清晰的名称。我们的所有的名称,都是有意义的,尽管不同的命名所带有的信息不同。我们要根据,我们需要提供什么数据,以及对方需要什么数据,两方面出发。才能确定一个合理且明晰的名称。

流程控制

  • 避免流程的嵌套。保持正常的流程清晰

  • 尽量保持正常代码路径为最小缩进

    优先处理错误或者特殊情况,尽早返回或继续循环来减少嵌套。

     func OneFunc() error {
         err := doSomething()
         if err == nil {
             err := doAnother()
             if err := nil {
                 return nil
             }
             return err
         }
         return err
     }
    

    可以转化为:

     func OneFunc() error {
         if err := doSomething(); err != nil {
             return err
         }
         if err := doAnother(); err != nil {
             return err
         }
         return nil
     }
    

流程控制我们有以下的总结:

  • 线性原理,处理逻辑尽量走直线,避免复杂的分支嵌套
  • 正常流程代码沿着屏幕向下移动

    和第一点一块看的话,其实就是在流程图中,我们的从顶向下我们的正确路径是在树干上的。而错误处理是在支路上。

  • 提升代码的可维护性和可读性
  • 故障问题大多出现在复杂的条件语句和循环语句中

错误与异常

简单错误:

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

错误的Wrap与Unwrap:

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

错误判定:

  • 判定一个错误是否为特定的错误,使用errors.Is
  • 不同于使用==,该方法可以判断错误连上的所有错误中,是否有特定的错误
  • 在链上获取特定种类的错误,使用errors.As

panic:

  • 不建议在业务代码中使用panic
  • 调用函数不包含recover会导致程序崩溃
  • 如果问题可以被屏蔽或者解决,建议使用error代替panic
  • 当程序启动阶段发生不可逆转的错误的时,可以在init函数或者main函数中使用panic

recover:

  • recover只能在被defer的函数中使用
  • 嵌套无法生效
  • 只在当前的goroutine生效
  • defer的语句是先进后出
  • 如果需要更多的上下文信息,可以revcover后在log中记录当前的调用栈

error尽可能提供简明的上下文信息链,方便定位问题

panic用于真正异常的情况

recover生效范围,在当前的goroutine的defer生效

性能优化

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

Benchmark:

  • 性能表现需要实际数据衡量
  • benchmark是go语言的基准性能测试工具

能够预分配空间可以大大提高效率。

对于slice,多个slice可能会复用一块地址。可以使用copy来减小内存消耗。

字符串拼接 可以使用string.Builder来提升效率

空结构体可以节省内存。(map实现set)

多线程通信建议使用atomic包

实践:

  • 依靠数据而不是猜测
  • 定位最大瓶颈而不是细枝末节
  • 不过早优化
  • 不过度优化

主要使用pprof,可视化和分析性能分析数据工具。