依赖管理

Golang 依赖管理机制发展到现在,主要经历了 GOPATH、govendor、go mod三个时期。而 go mod 发展到现在,已经能够较好地解决依赖管理中的问题了。go mod 的核心原理主要包含三部分内容:

  • 版本的两种表达方式:semver (Semantic Versioning) 语义化版本、基于某一 commit 的伪版本号;
  • 管理 go.mod 的两个重要工具:go get & go mod;
  • 版本选择算法:Minimal Version Selection(MVS)。
    • 对所有依赖选择最高的一个版本

gomod的二种表达方式

  • 语义化版本 (semver) v${major}.${minor}.${patch} major 不同则认为是不同的两个仓库。
  • 基于某一个 commit 的 伪版本号 没有用 go mod 管理的仓库 基本前缀+时间+commit 前 12 位

管理gomod的2个工具

  • go mod tidy : 添加或者删除项目所需依赖
  • go get github/xxx/xxxx
@upgrade 默认行为若当前是 semver,更新到最新的 semver;若当前是伪版本,更新到最新的minor version(若有)或者当前base version 的最新commit 的
@latest 修改依赖至最新的minor version ,可能会降低版本(例如可能将v0.1.1-0.20240512124444-bcab2ddalyyy 修改为0.1.0
@patch 更新到最新的 patch 版本
@none 删除依赖
@v1.2.3 semver
@b23abcd 拉取特定 commit,至少 7 位
@master master 分支的最新 commit
  • 若 major 版本>1 ,go get 需要加版本后缀: go get github.com/pkg/xxx/v2
  • -u (=patch) 同时更新所有依赖中所参与编译的依赖到 minor(patch)版本
  • 若 go get 的 目标 package 为 main ,会下载安装二进制到$GOPATH/bin (go < 1.18)

版本选择算法

如果 x 项目依赖了 a,b 依赖,a,b依赖 了 c 项目的v1.1.2,v1.2.3,最终编译所使用的 c 项目版本是 v1.2.3

非理想状态

2 个标记 // indirect 标记 非本项目直接依赖,但在本项目指定该依赖的版本 可能原因:

  • 依赖项没有使用 gomod
  • 不是所有依赖都在 gomod中
  • 手动为依赖项指定较新的版本 +incompatible 该依赖未使用 go mod 管理
  • 不影响使用, 但该依赖的依赖未显示指定版本
  • 不再认同major 不同为不同项目

常用工具和方法

查询所有依赖项之间的依赖关系 go mod graph 获取参与编译的依赖项代码 go mod vendor

临时修改依赖库中的代码测试

  • clone仓库到本地&checkout 对应版本&replace
  • replace githubcom/foo/bar =>../../../github.com/for/bar 批量更新依赖
  • go get github.com/…

仅安装/运行二进制但不更新当前项目的依赖

  • go install example/cmd@latest(go >=1.16)
  • go run example.com/cmd@latest (go >=1.17)
  • go 1.18后 go get 将只用于更新依赖不安装二进制。

使用建议

  • 如果项目庞大,go.mod 臃肿,可以进行拆库,或者对子目录采用submodule(go kratos框架)
  • 库维护者,如何优雅地告知使用者某个版本存在 bug
    • retract标记 (go >=1.16)
  • 使用go mod 后已可以不将工程放在$GOPATH下,如何组织庞大的工程
    • 根据实践经验,强烈推荐依然使用$gopath按路径管理所有依赖项,why?
      • 各工程的组织结构清晰
      • 可以从工程的相对路径获取 go mod

经典案例分析

  1. 指定 go get -u github.com/xxx/xxxx 后,项目编译不通过

  2. -u 参数会更新依赖的依赖,可能导致不兼容,如无必须不加-u 参数

  3. 为什么一些工程不使用go mod ? require ( … github.com/form3tech-oss/jwt-go v3.2.5+incompatible … )

  4. 在 go mod 推广之前就已经 major 大于 1 了

  5. 便于 go get 直接拉到 v3 版本。

  6. 无论依赖库是否使用 go mod ,我们的项目还是使用go mod 特殊情况,可以手动指定indirect依赖

  7. 惨遭删除 tag,commit,branch 如何解决:?

  • 如果只是本项目依赖,删除go.mod 的条目,然后go get 更新到最新的 tag
  • 若项目依赖项也依赖,replace 该依赖至正常的 tag ;彻底解决需要go get 依赖链路上的所有项目到最新的 tag。
  • 更可怕的是删tag 后,在其他 commit 重新打了相同的 tag。 得删除本地缓存,重新拉取。
  1. 循环依赖陷进 package 之间是不能循环依赖的,编译就报错, 但是不同项目模块是可能产生循环依赖的。 公共库之间已经明确分工(不同的团队在维护的) ,避免大杂烩,产生循环依赖.

代码规范

这里只举例很小的一部分

  1. 包名 ConcurrentFeture (java风格)、concurrent_feture(python风格)、concurrentfeture(go 推荐用这个方式)
  2. 包名尽量不用复数,也尽可能简洁 。
  3. 包名和函数名避免重复出现
  4. 变量命名: 小驼峰、大驼峰(公共变量).
  5. 库升级
  • 如superdb从 1.1升级到2.0需要如何操作?
  • 新建目录 v2
  • 将原来的代码都移动到v2下,包括 go mod 文件,并加上新的修改。
  • v2 目录的 go mod ,modulepath 必须加上对应的版本后缀例如 github.com/foo/bar/v2 。
  • 打v2的 tag

Uber Go 语言编码规范

https://github.com/xxjwxc/uber_go_guide_cn

官方 lint :

  • gofmt: 代码整理,给结尾加空等
  • govet : 检测程序的正确性
    • Http body 是否被正确的 close
      • 错误,先 defer,应该先检测err, 这种可能造成程序的 panic。 resp, err := http.Get(…) defer resp.Body.Close() if err != nil { return err }
  • 原子操作的误用 var x uint64 x = atomic.AddUint64(&x,1) //错误用法 ,此并发原子的

// 正确用法 var x uint64 atomic.AddUint64(&x,1) //错误用法 ,此并发原子的

  • 检查 unsafe.Pointer 的正确错误
  • golint 已经废弃 。
    • false positives , 提示的问题不一定正确(就是对的也说错的)
    • 不同的功能分散在不同的工具,不够便捷。没有统一的配置。
    • 很多场景没有满足

golangci-lint

golangci-lint 是社区基于 Go team 的 https://github.com/golang/tools/tree/master/go/analysis 开发的 linter 工具(go vet 也是基于此开发的)

  • 解决了false positives问题 //onlint 用了屏蔽检测
  • 统一的配置文件,解决不同工具的配置文件问题
  • 大大加速,只解析一次ast 树
  • 可插拔linter配置,自由选配linters,自身携带大多的 linter

团队实践

  • 公司使用某个版本固定的 golangci-lint
  • 开发团队统一 golangci-lint 配置(也可以继承公司的基础配置)

参考配置

 1run:
 2  timeout: 30m
 3  go: "1.21"
 4  checks:
 5    - "all"
 6    - "-SA1019"
 7  goimports:
 8    # 设置哪些包放在第三方包后面,可以设置多个包,逗号隔开
 9    local-prefixes: rmq
10issues:
11  exclude-files:
12    - _test.go
13  exclude-dirs:
14    - examples
15    - doc
16
17
18linters:
19  disable-all: true
20  enable:
21#    - unused
22    - ineffassign
23#    - goimports
24    - gofmt
25    - misspell
26    - unparam
27    - unconvert
28    - govet
29#    - errcheck
30    - staticcheck

单元测试

尽早的使用单元测试。

准备、调用、断言, table 测试

不盲目追求覆盖率,只对核心需要进行测试即可

常用table测试

 1package yourpackage
 2 
 3import (
 4    "testing"
 5    "github.com/stretchr/testify/assert"
 6)
 7 
 8func Add(a, b int) int {
 9    return a + b
10}
11 
12func TestAdd(t *testing.T) {
13    testCases := []struct {
14        a        int
15        b        int
16        expected int
17    }{
18        {a: 1, b: 1, expected: 2},
19        {a: 2, b: 3, expected: 5},
20        {a: 0, b: 0, expected: 0},
21        // 可以添加更多的测试用例
22    }
23 
24    for _, tc := range testCases {
25        name := fmt.Sprintf("%d+%d", tc.a, tc.b)
26        t.Run(name, func(t *testing.T) {
27            actual := Add(tc.a, tc.b)
28            assert.Equal(t, tc.expected, actual)
29        })
30    }
31}

mock,断言框架选择

场景 项目 适用场景 地址
mock gomock 面相接口编程,把接口给 mock 调 github.com/golang/mock 现在被https://github.com/uber-go/mock 代替
mock monkey 适合非接口定义,如把一个 函数调用了 mysql 的结果值给 mock 调 github.com/agiledragon/gomonkey
断言 testify tdd 驱动,有 mock 功能 github.com/stretchr/testify

go test常见用法

go test

1可选参数
2-v 详细结果
3-conver  输出覆盖率
4-run 指定某一个测试用例
5其他如benchmark测试等

看单测哪行没覆盖

  • go test ./… -coverprofile=cover.out
  • go tool cover -html=cover.out

如果编写单元测试,可以看另外一篇文章:https://www.crblog.cc/go/go-unit-test.html

程序分析

pprof 详细见 https://www.crblog.cc/go/go-pprof.html

线上都开,好排查问题。

三方性能分析工具

statsviz

https://github.com/arl/statsviz, 如果需要监控可视化,可以看这个项目。可以在浏览器上将内存,cpu,goroutine 数等可视化出来。

pyroscope

pyroscope 是 go+node开发的一款可视化性能分析工具,支持很多语言进行性能分析。 https://github.com/grafana/pyroscope

errors 处理

基本准则

  • 业务代码处,禁止手动写panic,然捕获,panic并非php中的exception。
  • error只处理一次, 在最上层进行处理,如打印 logger,请别到处打印错误日志,重复的日志反而让问题更难排查。
  • 想要错误调用链的的堆栈,请使用包装错误包裹下,再返回。
  • 任何错误,可以跟错误码关联,没有关联,则表示是内部错误,给默认的错误 code 。
  • 异常程序无法控制的错误,需要 recover 中间件进行处理错误,记录 logger。

错误包地址: https://github.com/cr-mao/errors