Go编码规则与性能调优入门 | 青训营笔记

104 阅读5分钟

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

知识点总结与思考

编码规范

原则

  • 简单性
    • 消除多余的复杂性,以简单清晰的逻辑编写代码
    • 过于复杂的代码难以理解,持续修复改进较困难
  • 可读性
    • 要考虑开发成员能够读懂我们所写的代码
    • 编写可维护代码的第一步是确保代码可读
  • 生产力
    • 编码规范是提高团队整体开发效率的保障

注释

  • 公共符号 公共函数,公共变量等
    • 注释应该要详细说明成功执行后的内容或是执行失败的原因
    • 正确的注释不应该是告诉代码阅读者应该去阅读某个参照库
  • 注释规范
    • 解释代码作用
    • 解释代码原理
    • 解释如何做的 这么做是因为Go版本迭代?等等....
    • 解释出错场景

变量命名

  • 缩略词全大写
    • ServeHTTP (✔)
    • ServeHttp (×)
  • 命名携带上下文信息

函数命名

  • 函数名不携带上下文信息
    • http包内的Serve方法
    • 那么我们的Serve方法名有两种选择
      • 1.ServeHTTP()
      • 2.Serve()
    • 调用方在调用的时候会出现两种情况
      • 1.http.ServeHTTP()
      • 2.http.Serve()
    • 综上我们选择第二种使上下文关系清晰
      • 调用的过程中包名已经携带信息
      • 方法名不需要再携带上下文信息

控制流程

  • 优先处理错误
    • 遇错误直接返回
    • 所有错误考虑后
    • 再执行正确内容

错误处理

  • 作用
    • 定位错误
  • 简单错误
    • errors.New()
  • 错误跟踪链
    • Go1.13在errors新增了新的API
      • errors.Is
      • errors.As
        • 获取特定种类的错误
      • errors.Unwrap
    • Go1.13在format新增了关键字
      • fmt.Errorf()的 %w
      • %w关键字可以将错误关联至错误链中
  • panic
    • 不提倡在业务代码中使用panic
    • 不含recover的时候会造成程序崩溃
    • 尽量能够以error替代panic
  • recover
    • recover只能在被defer的函数中使用
    • 嵌套无法生效
    • 当前goroutine生效
    • defer的语句是后进先出规则的
      • 需要考虑到defer的执行顺序

高质量

  • 正确可靠、简洁清晰
    • 边界条件
    • 异常处理
    • 稳定性
    • 易读性
    • 易维护

性能优化策略

Benchmark

Go 自带的基准性能测试工具

运行指令

go test -bench = . -benchmem
// -benchmem:指定内存分配情况

优化建议

Slice - 优化策略

  • 背景
    • 从已有切片的基础上创建切片并不会创建一个新的底层数组 两个切片共用一块底层数组
  • 切片陷阱之动态扩容
    • 场景
      • 当不指定容量大小会导致频繁扩容造成频繁进行内存分配从而使得性能降低
    • 解决方案
      • 如果能够预知容量范围应尽可能在使用make()初始化切片时提供容量信息
      • 这样只会进行一次内存分配而不会进行多次扩容分配造成额外的性能开销
  • 切片陷阱之大内存无法释放
    • 场景
      • 原切片巨大,另一个切片在原切片基础上创建小段切片
      • 原切片的底层数组在内存中的引用计数不为0,内存无法释放
    • 解决方案
      • 使用copy()方法替代在原切片基础上创建小段切片

map - 优化策略

  • 背景
    • map的扩容机制会触发 Rehash;
      • 频繁的扩容会频繁触发 Rehash 造成资源损耗;
  • map陷阱之动态扩容
    • 场景
      • 当map的容量需求动态增长不足以满足需求时,map执行扩容,当扩容频率过高时,触发内存拷贝和 Rehash 的频率也随之增高,资源损耗变大;
    • 解决方案
      • 如果能够预知容量范围应尽可能在使用make()初始化map时提供容量信息

string - 优化策略

  • 背景
    • Go中的字符串是不可变类型,占用内存大小是固定的
  • 拼接方案
    • "+" operator
    • strings.Builder
    • bytes.Buffer
  • 拼接性能分析
    • "+" operator
      • 每次对字符串执行 "+" 进行拼接会触发内存重新分配
      • 性能极差
    • bytes.Buffer
      • 底层 byte[]数组,不需要重新分配内存
      • 性能稍微慢于strings.Builder
    • strings.Builder
      • 底层 byte[]数组,不需要重新分配内存
      • 性能最快
  • strings.Builder 稍快于 bytes.Buffer 原因

    • strings.Builder
      • 返回时,直接将 []bytes 转化为字符串类型
      func (b *Builder) String() string {
          return *(*string)(unsafe.Pointer(&b.buf))
      }
      
    • bytes.Buffer
      • 返回时,转化为字符串类型时重新申请了一块空间
      func (b *Buffer) String() string {
          return string(b.buf[b.off:])
      }
      
  • 进一步优化strings.Builder 与 bytes.Buffer

    • 若字符串长度可知,则进行提前分配,可以减少内存分配,提高效率
    • Builder,Buffer 都能够降低一定的时间损耗
    • strings.Builder的内存损耗则能够实现大幅度的降低
      • (降低70%左右)
    • bytes.Buffer有降低但是没有strings.Builder降低的幅度大
      • (降低50%左右)

其他的 优化策略

空结构体 struct{} 对特定场景进行内存优化

  • 背景
    • 空结构体struct{} 实例不占据任何的内存空间
    • 可作为各种场景下的占位符使用
  • 场景
    • 实现 set 的数据结构
      • 实现原理与区别
        • map + bool 或 map + struct{}
        • map + bool:
          • 使用map的键,map值用bool值占位会占一个字节的内存空间
        • map + struct{}:
          • 使用map的键,map值用struct{}占位不占用内存空间,节省内存空间

atomic package

  • 背景

    • 某些场景之下使用 atomic package 能够比 lock 的效率要高
  • atomic 与 lock 使用场景区分

    • atomic:适合保护一个变量的场景
      • 对于非数值操作,可以使用atomic.Value,能够承载一个interface{}
    • lock  :适合保护一段逻辑的场景
  • atomic package 与 lock 实现原理

    • atomic package
      • atomic 维护并发原子操作,时间复杂度远低于加锁
      • atomic 操作是通过硬件实现,效率比锁高
    • lock
      • lock 通过操作系统实现,属于系统调用,加锁解锁造成上下文切换导致资源损耗大