GO语言(高质量编程与性能调优实战)| 青训营笔记

一、编写更简洁清晰的代码

高质量编程概述

即编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码

大致有以下几点:

  1. 正确性:是否考虑各种边界条件,错误调用能否处理
  2. 可靠性:异常情况或者错误的处理策略是否明确,依赖的服务出现异常是否能够处理
  3. 简洁——简单性
  4. 清晰——可读性

编码规范

  1. 代码格式

    推荐使用gofmt自动格式化代码——自动格式化Go语言代码为官方统一格式

    goimports——实际等于gofmt加上依赖包工具,自动增删依赖的包引用,并将依赖包按字母顺序排序并分类

  2. 注释

    注释的要求

    • 解释代码作用

      说明公共符号,例如对外提供的函数注释描述它的功能和用途

    • 解释代码如何做到

      对代码复杂/不明显的逻辑进行说明,即注释实现过程

      image-20220508172411065

    • 代码实现的原因

      适合解释代码的外部因素/提供额外上下文

    • 代码什么情况会出错

      提醒使用者一些潜在的限制条件或者无法处理的情况

    • 公共符号始终要注释

      任何既不明显也不简短的公共功能必须予以注释

      例外:如果注释没有提供有用的消息亦或是通知看其他文档则可删除

    代码是最好的注释;注释需要提供代码来表达出上下文信息

  3. 命名规范

    • variable(变量)

      1. 简洁甚于冗长

      2. 缩略词大写名单当其位于变量开头且不需要导出时,使用全小写(例如:ServeHTTP/而不是ServerHttp,使用XMLHTTPRequest或者xmlHTTPRequest)

      3. 变量距离其被使用的地方越远则越需要更多的上下文信息

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

    • function(函数)

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

      1. 只有小写字母组成,不包含大写字母和下划线等字母

      2. 简短并包含一定的上下文信息(如,schema,task等)

      3. 不要与标准库重名(例如不使用sync、strings等)

      4. 以标准库包名为例

        1. 不使用常用变量名为包名(例如:使用bufio,而不是buf)
        2. 使用单数而不是复数(例如:encoding而不是encodings)
        3. 谨慎使用缩写(例如使用fmt比format更好)

    核心目标是降低阅读理解代码的成本;

    重点考虑上下文信息,设计简洁清晰的名称

  4. 控制流程

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

      例如:

image.png 如果两个分支中都包含return,则可以去除冗余的else

-   尽量保持正常代码路径为最小缩进——即优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套

**线性原理——处理逻辑尽量走直线,避免复杂的嵌套分支**

**正常流程代码沿着屏幕向下移动**

**提升代码的可维护性与可读性**

**故障问题大多出现在复杂的条件语句与循环语句**
  1. 错误和异常处理

    • 简单错误

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

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

      • 判定一个错误是否为特点错误——使用errors.Is
      • 不同于使用==,使用该方法可以判定错误链上的所有错误是否含有特定的错误
      • 在错误链上获取特定种类的错误,使用errors.As——as会提取调用链中指定类型的错误,并将错误赋值给定义好的变量,方便后续处理
    • panic

      • 不建议在业务代码使用panic
      • 当程序启动阶段发生不可逆转的错误,可以在init/main函数中使用panic
    • recover

      • recover只能在defer的函数中使用
      • 嵌套无法生效
      • 只在当前goroutine生效
      • defer语句后进先出

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

    panic用于真正的异常情况

    recover生效范围,在当前goroutine的被defer函数中生效

性能优化

  • 性能优化的前提是满足正确可靠,简洁清晰等质量因素
  • 性能优化是综合评价,有时时间效率和空间效率可能会对立
  • 针对Go语言特性,介绍Go相关性能优化建议

——支持基准性能测试的benchmark工具

image-20220509120351772

——性能优化建议Slice(预分配内存

尽可能在使用make()初始化切片提供容量信息

  • 切片本身是一个数组片段的描述(包括数组指针、片段长度、片段容量)

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

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

  • 如果是大内存来释放:

  • 因此很可能出现这么一种情况,原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小一段, 但底层数组在内存中仍然占据了大量空间,得不到释放 可使用 copy 替代 re - slice 两部分代码使用了不同的逻辑取 slice 的最后两位数创建新数组,同时统计输出了内存占用信息 结果差异非常明显, lastBySlice 耗费了100.14 MB 内存,也就是说,申请的100个1MB大小的内存没有被回收。因为 切片虽然只使用了最后2个元素,但是因为与原来1M的切片引用了相同的底层数组,底层数组得不到释放,因此,最终100 MB 的内存始终得不到释放。而 lastByCopy 仅消耗了3.14 MB 的内存。这是因为,通过 copy ,指向了一个新的底层数组,当 origin 不再被引用后,内存会被垃圾回收

  • Map预分配内存

    • 不断向map中添加元素的操作会触发map的扩容
    • 提前分配好空间可以减少内存拷贝和Rehash的消耗
    • 建议根据实际需求提前预估好需求的空间
  • 空结构体:(使用空结构体可以节省内存)

    • 空结构体struct{}实例不占据任何内存空间

    • 可作为各种场景下的占位符使用:

      • 既节省资源
      • 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符。
  • 避免常见的性能陷阱可以保证大部分程序的性能
  • 普通应用代码,不要一味的追求大部分程序的性能
  • 越高级的性能优化手段越容易出现问题
  • 在满足正确可靠、简洁清晰的质量要求前提下提高性能

二、性能调优实战

1. 概述

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

2. 性能分析工具——pprof

可以知道应用在什么地方耗费了多少CPU、Memory

pprof是用于可视化和分析性能分析数据的工具

功能简介:

image.png

posted @ 2022-05-12 20:54  Luciferpluto  阅读(1)  评论(0)    收藏  举报  来源