go 高级和代码规范

167 阅读7分钟

go语言进阶和代码规范

语言进阶

go 可以充分发挥多核优势,高效运行 通过用户态的协程(goroutine),线程算内核态 协程即轻量级的线程,开销是kb级的,即可以同时开甚至上万协程。

go func(){
  //some code here
}()

协程通信

go提倡通过通信共享内存而不是通过共享内存实现通信,即channel (先入先出的通信队列)

make(chan [type],[buffer size] )

可以用通道实现生产消费模型 通道当然是并发安全的 消费者的队列带缓冲就可以减低因消费者消费速率慢对生产者生产的影响。

lock

若要使用共享内存,要加锁才能保证并发安全,go的锁是sync.Mutex(当然还有更多种锁)

sync.WithGroup

计数器,由Add(int),Done(),Wait()三个方法,由计数器实现并发。开始先由 Add 确定计数器大小,通过Done 来使计数器减1,在主线程里用 Wait 阻塞至计数器为0

依赖管理

管理依赖库

历史

gopath -> govendor -> gomodule 实现不同项目依赖版本不同,且不同依赖版本可以不同

gopath

相当于 go 里面的工作区

|-bin
|-pkg
 -src

弊端:无法实现package的不同版本

govendor

在项目下增加vendor文件夹,所有依赖包副本放在这下面,找不到的再去找gopath 弊端:无法控制依赖版本,更新项目可能又出现依赖冲突。

gomodule

终极目标:定义版本规则和管理项目依赖关系。

  1. 配置文件(描述依赖):go.mod
  2. 中心仓库管理依赖库:Proxy
  3. 本地工具:go get/mod

go.mod

module example/projects/app //依赖管理的基本单元

go 1.20 //原生库

require( //单元依赖
    example/lib1 v1.0.1 //indirect(此关键字表示间接依赖)
    example/lib2 v3.2.0+incompatible(没有go.mod文件且主版本2+的依赖)
)

依赖配置

语义化版本 MAJOR.{MAJOR}.{MINOR}.${PATCH} major是不兼容的 minor是新功能 patch是修复补丁 基于 commit 伪版本 vX.0.0-yyyymmddhhmmss-abcdefgh1234

版本选择

选择最低的兼容版本

依赖分发 Proxy

github等管理平台,SVN等 第三方仓库问题:

  1. 无法保证构建稳定性
  2. 无法保证依赖可用性
  3. 增加第三方压力 go的解决方法:通过官方的goProxy,直接在goProxy拉取依赖。 go mod通过GOPROXY环境变量,是个用逗号分割的url列表。 在设计缓存的场景是一致的。

go get 和 go mod

go get 默认拉取最新版本依赖,@none是删除依赖,@具体版本就是那一个版本的依赖 go mod 实际上是一个项目工具,init是初始化,download是下载模块到本地缓存,tidy是增加需要依赖,删除不需要的依赖。

测试

开发 -> 测试 -避免-> 事故

回归测试:测试人员上线时对app 集成测试:测试人员上线前对app,统一 单元测试:开发者编写时对函数

单元测试

package abs

import "testing"

func TestAbs(t *testing.T) {
    got := Abs(-1)
    if got != 1 {
        t.Errorf("Abs(-1) = %d; want 1", got)
    }
}

通过单元测试,分模块进行

保证代码质量,提升错误定位效率

对 go, 所有文件以 _test.go 结尾,函数为 TestXxx(*testing T),初始化逻辑在 TestMain 里面

assert 包进行判断

覆盖率

评估单元测试的指标,提升覆盖率 -> 提升单元测试完备度

一般覆盖率在50-60%,较高覆盖率80%+

测试分支相互独立,全面覆盖

测试单元粒度足够小,函数单一职责

Mock测试

外部(文件,包)依赖 => 稳定&幂等 -> mock 机制

github.com/bouk/monkey

为一个函数或变量打桩(用一个函数临时替换到另一个函数)

func main() {
  monkey.Patch(fmt.Println, func(a ...interface{}) (n int, err error) {
    s := make([]interface{}, len(a))
    for i, v := range a {
      s[i] = strings.Replace(fmt.Sprint(v), "hell", "*bleep*", -1)
    }
    return fmt.Fprintln(os.Stdout, s...)
  })
  fmt.Println("what the hell?") // what the *bleep*?
}

就可以不依赖本地文件(可以)

基准测试

优化代码,对当前代码性能分析,内置测试框架提供了基准测试的能力。

func BenchmarkSearch(b *testing.B) {
  for i := 0; i < b.N; i++ {
    Search("hello", "hello world")
  }
}
go test -bench=. -benchmem

高质量编程

简介

高质量:编写代码正确可靠,简洁清晰

  • 边界条件
  • 异常处理
  • 易读易维护

编程原则

  • 代码简洁 消除重复代码,保持代码简洁 不理解的代码不要随意修改
  • 可读性 代码是写给人看的,编写可维护的代码的第一步是保证代码的可读性
  • 生产力 团队整体效率是很重要的 保证一致的代码风格

go 编码规范

代码格式

使用 gofmt 自动格式化代码

注释

注释应该做的:

* 解释代码作用
* 解释代码算法
* 解释代码实现原因
* 解释代码的局限性
  • 既不明显也不简短的公共功能,需要注释
  • 包内声明的每个公共符号都需要注释
  • 无论长度或复杂程度,库中的每个函数都需要注释
  • 跟在之前注释好的类后面的方法可以不用冗余的注释

例外:不需要注释实现接口的方法

命名规范

变量
  • 简洁胜于冗长
  • 缩略词全大写,但位于变量开头且不需要导出时,全小写
  • 变量距离其使用的地方越远,需要携带越多的上下文信息

关键在于其信息量

函数

函数名不携带包名上下文信息

函数名尽量简短

当名为 foo 的包中有一个返回 Foo 的函数时,可以省略类型信息。

除此之外,函数名应该包含类型信息。

在实际调用方的角度考虑。

  • 只用小写字母
  • 简短并包含一定上下文信息
  • 不要与标准库同名

以下是尽量满足:

  • 不使用常用变量名为包名
  • 使用单数
  • 谨慎使用缩写
总结

核心目标是降低阅读理解成本

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

控制流程

  • 尽量避免分支嵌套

  • 保持正常代码路径为最小缩进,优先处理特殊/错误情况,尽早确定返回或继续

  • 线性原理,避免嵌套

  • 保持正常流程代码沿着屏幕向下流动

  • 提升代码可读性

  • 故障大多在复杂的条件与循环中

错误处理

  • 简单错误 简单错误是仅出现一次的错误,其他地方不需要捕获它 优先使用 errors.Newfmt.Errorf 创建简单错误
  • 错误的 Wrap 和 UnWrap 优先使用 errors.Wraperrors.Wrapf 包装错误 优先使用 errors.Unwrap 解包错误
  • 错误判定 使用 errors.Is 判断错误 使用 errors.As 获取错误
  • panic 尽量避免使用 panic 调用函数不包含 recover 会导致程序崩溃 如果问题可以被屏蔽或解决,使用 errors.Newerrors.Errorf 创建错误 如果是在程序初始化阶段,可以使用 panic
  • recover recover 只应该在 defer 函数中使用 嵌套无法生效 只在当前 goroutine 中生效 如果需要更多上下文信息,使用 recover 后用 log 记录

性能调优

性能调优的前提是保证代码质量,性能优化是综合评估

性能调优建议-Benchmark

  • 性能表现需要实际数据支持

实际建议

  • slice 尽可能在使用 make 时提供初始容量。
  • 使用 copy 代替 re-slice,在原有的大切片上。
  • map 也应当预分配内存。
  • 字符串拼接上,使用 strings.Builder。可以先使用 Grow 方法预分配内存,会更快。
  • 空结构体 struct{} 不占据任何内存空间,可以用作占位符。可以用于不需要值的 mapvalue
  • 使用 atomic 包的原子操作,而不是 mutex。因为 atomic 是通过使用 CPU 指令来实现的,而 mutex 是通过内核调度来实现的。而且 atomic 操作的是变量,而 mutex 保护的应该是一段代码逻辑