关于后端:Go-Module-Package-Workspace-参考笔记

2次阅读

共计 6603 个字符,预计需要花费 17 分钟才能阅读完成。

这篇笔记整顿记录了在浏览 go 官网文档中对于依赖治理、包引入、多模块开发时的工作区等相干内容。

module path

module path 能够惟一标识一个 module,也是定位一个 module 下的 package 时的前缀。

module path 应该能够表明该 module 是做什么的以及去哪里能够下载到,个别由代码仓库中的地位以及主版本号组成,当主版本号是 v1 时可省略,从 v2 之后须要明确指出。

module main

go 1.20

package 命名标准

go 的源码被组织成 package 包的模式,包名在 go 源文件中定义,反过来说每一个 go 源文件都须要表明其所在的包。

包名标准

包名应该尽量简短清晰,且应用小写字母,不蕴含下划线,也不应用驼峰表示法,例如 time 包提供工夫相干工具,list 包是对双向链表的实现,http 包提供了 HTTP 客户端和服务端的实现。反观 computeServiceClient 和 priority_queue 都不是一个好的包名。包名能够适当做简略缩写:strconv (string conversion)、syscall (system call)、fmt (formatted I/O)

包中的函数名或变量名不要反复包名,因为当内部客户端援用包内的函数或变量时,个别都是通过包名调用的,所以包内的内容也就不须要再反复一遍包名了。例如 http 包中如果有一个名为 HTTPServer 的办法就不是很好,而应间接叫做 Server 办法就能够了。

如果某个叫 pkg 的包中的办法返回值类型是 pkg.Pkg 或者是 *pkg.Pkg,那么办法名能够简写,例如

start := time.Now()                                  // start is a time.Time
t, err := time.Parse(time.Kitchen, "6:06PM")         // t is a time.Time
ctx = context.WithTimeout(ctx, 10*time.Millisecond)  // ctx is a context.Context
ip, ok := userip.FromContext(ctx)                    // ip is a net.IP

绝对的如果函数返回的是 pkg.T,而 T 不是 Pkg 的时候,函数名最好可能体现返回值是什么

d, err := time.ParseDuration("10s")  // d is a time.Duration
elapsed := time.Since(start)         // elapsed is a time.Duration
ticker := time.NewTicker(d)          // ticker is a *time.Ticker
timer := time.NewTimer(d)            // timer is a *time.Timer

防止应用抽象的、无实际意义的名字

无实际意义的名字如 util, common,让使用者无奈一眼看出包具体是做什么的。

防止让一个包大而全,尽量拆成独立的小包

这个名字叫 util 很抽象,而外部办法有一些是解决 string set 的,那么应该将 string set 的解决独自提出来成为一个独立的包

package util
func NewStringSet(...string) map[string]bool {...}
func SortStringSet(map[string]bool) []string {...}

这样更加清晰,性能也更聚焦

package stringset
func New(...string) map[string]bool {...}
func Sort(map[string]bool) []string {...}

package 导入门路

经常将 import aa/bb/cc/pkg 说成是导入包, 但实际上 import 前面的门路是 module path + 该包内的 go 源文件绝对于其 go.mod 的相对路径 ,所以这里仅仅指向到了门路而并没有具体指向到源文件上,所以确切的说应该叫导入包门路。不过因为默认倡议包名与路径名的最初一个元素雷同,所以将其称之为“导入包”如同也能够。

举个例子,如上面构造中,go.mod 中指明整个 module 叫做 aaa/bbb/ccc/pkg

x_file.go 绝对 go.mod 同级别,能够复用 module path 最初一位间接叫 pkg package pkglib/url.go 门路绝对 go.mod 位于子目录 lib 中,所以用 package lib 示意

这里的 x_file.go 文件名次要是体现文件名对包没有影响,重要的是门路

.
├── go.mod
├── lib
│   └── url.go
└── x_file.go
// x_file.go
package pkg

import "fmt"

func Func() {fmt.Println("This is pkg package")
}
// lib/url.go
package lib

func Url() string {return "www.baidu.com"}

应用的时候这样 import

package main

import (
    "aaa/bbb/ccc/pkg"
    "aaa/bbb/ccc/pkg/lib"
)

func main() {pkg.Func()
    url := lib.Url()
    println(url)
}

也正是因为包名应用包门路中最初一个元素作为包名,这就可能存在包名反复的状况,如 runtime/pprof 和 net/http/pprof 包,包名都是 pprof,遇到反复的状况只须要给其中一个包或者两个包别离起个别名。

import (
    "context"                // package context
    "fmt"                    // package fmt
    "golang.org/x/time/rate" // package rate
    "os/exec"                // package exec
)

main.go 同级 package 导入问题

main.go 应用同一个包下另一个文件中的函数,运行 go run main.go 报未定义函数

src/main/main.go:12:2: undefined: LibFunc

.
├── lib.go
└── main.go
---

// lib.go
package main

func LibFunc() {println("lib func exec")
}

// main.go
package main

func main() {LibFunc()
}

因为 go run main.go 只编译了 main.go 文件所以找不到 LibFunc 的函数定义,能够应用 go run main.go lib.go 指定编译 main.go 和 lib.go,也能够 go run . 编译当前目录下所有文件。

对于 module 版本号的标准

版本号由三个非负整数,从左到右的次要、主要和补丁版本,用点分隔。

例如 v0.0.0v1.12.134v8.0.5-pre 和 v2.0.9+meta 都是无效版本号。

对于版本号降级的标准

版本号每个局部实际上代表着版本是否稳固,以及是否与老版本兼容,产生如下几种情景时须要扭转版本号

  • 大版本升级:新版本的接口和性能向后不兼容的时,必须递增次要版本,并且主要版本和修补程序版本必须设置为零。
  • 小版本升级:新版本的接口和性能能够向后兼容时,只须要减少主要版本即可,而补丁版本必须设置为零。即小版本升级。
  • 补丁版本升级:新版本的接口和性能没有变动,只是批改谬误和优化,只须要降级补丁版本即可。
  • 预公布版本:预发行后缀示意版本是预发行版本。预公布版本在相应的公布版本之前排序。例如,v1.2.3-pre 在 v1.2.3 之前。
  • 伪版本 pseudo-version:对于不恪守上述标准的状况,go 会对该 module 生成伪版本,伪版本是非凡格局的预发行版本,它对版本控制存储库中无关特定订正的信息进行编码。例如,v0.0.0-20191109021931-daa7c04131f5 是伪版本。基本上就是由虚构的三段版本号 +UTC 日期 + 仓库提交哈希的前 12 个字符

module v2 及以上版本的非凡解决

假如有一个 module 的 v1 版本的 module path 是 example.com/mod,有一天该 module 升级成了 v2,其 v2 版本的 module path 必须在是 example.com/mod/v2。这里强调一下,辨别不同 module 就是依附 module path 来的,module path 不同,也就意味着是两个不同的 module。

这样一来版本 v1 和 v2 就能在同一个我的项目中共存,且反对同时被援用,这一点可能在有着其余编程语言教训的人看来有点奇怪,至多在 Python 中默认不反对一个我的项目中同时应用某个库的不同版本,要想反对须要本人解决包引入,手动做一些 track。

不过换个角度来说,依照上一节中规定的版本号降级标准,如果一个 module 降级了大版本,意味着有些性能曾经不向后兼容,v1 和 v2 的 module path 曾经不同了,说的更彻底一点,能够了解成 v1 和 v2 是两个不同 module,那么如果把 v2 版本当成一个全新的 module 来应用,这样的话 v1 v2 能够共存如同也说得过去。

不过总感觉这种做法有点怪怪的,版本依赖的事儿就应该交给版本管理工具来做,手动在 module path 中人为退出 v2 算咋回事。有的 module 开发者看不惯,会把不兼容的版本换个名字从头再来,或者罗唆始终都在 v1 外面开发永远不降级 v2。

让我不太能了解或不好承受的一个本源在于,go 强制把一个一般的版本号 v2 赋予了非凡的含意,使得任意一个 module 降级到 v2 本质上就变成了另一个新 module,而降级到 v3 则又将是另一个新 module。这种将某一符号特殊化的做法很相似魔数或固定写死一段代码的做法,会让我感觉不具备合理性。

这里有一篇吐槽应该说出了不少开发者的心声:https://colobu.com/2021/06/28/dive-into-go-module-2/

go.mod 文件中的 replace 指令

replace 指令能够将依赖的某个 module 替换为来自另一处的 module,这里的“另一处”指的是另一个近程仓库,也能够是本地某个文件门路。

replace 个别用于两种状况,第一种就是替换掉近程仓库 A 里的 module,改用从仓库 B 中获取,也包含替换掉版本号。另一种状况就是将 module 指向本地某个门路。

replace golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5

replace (
    golang.org/x/net v1.2.3 => example.com/fork/net v1.4.5
    golang.org/x/net => example.com/fork/net v1.4.5
    golang.org/x/net v1.2.3 => ./fork/net
    golang.org/x/net => ./fork/net
)

MVS Minimal Version Selection

Go 应用一种称为最小版本抉择 (MVS) 的算法,通过 module 的 go.mod 文件,一层层沿着依赖关系图找出符合要求的各个依赖 module 的版本,组成构建列表 build list。

MVS 从 main module 开始并遍历该图,跟踪每个模块依赖的最高版本,在遍历完结后,所需的最高版本所组成的就是满足整个 module 要求的最低依赖版本了。下图 MVS 最终返回的构建列表有 A 1.2、B 1.2、C 1.4 和 D 1.2

如果依赖治理中存在 replace 的状况,replace 进来的新 module 可能会有不同的依赖关系,MVS 也会把这种状况思考进去,如图,本来的 C1.4 版本被 replace 成了 R,而 R 依赖 D1.3,这样综合下来最初的构建列表后果就是 A 1.2、B 1.2、R 和 D 1.3

如果依赖治理中有 exclude 的状况,那么会将被移除的 module 的版本要求顺延到下一个更高的版本,如图,C1.3 被 exclude 了,对 C module 的要求就变成了 1.3 下一个高版本即 1.4,最初的构建列表为:A 1.2、B 1.2、C 1.4 和 D 1.2

go get 能够用来降级一组 module,如果做降级操作,MVS 也会做出相应调整

go get 也能够用来降级 module,如发现 C1.4 有问题须要回退到 C1.3,那么依赖 C1.4 的下层 module 也会被回退。

workspaces go.work 工作空间 / 工作区与多 module 援用治理

说实话 golang 的包治理真的很绕,一开始没有包治理,起初遇到点问题解决点问题,逐渐改良,导致包治理相干标准或实际改变了好几版。

一开始没有包治理,全都要求将依赖包放到 GOPATH 环境变量上面。

在 go 1.11 版本中引入了 module 的概念,通过 go mod 命令来治理依赖,不再强制要求将包放在 GOPATH 下,算是一个飞跃式的提高。

go mod 还是有一些不不便的中央,例如依赖一个未公布的包,或者本地测试中的长期包,须要通过 replace 指令将包名替换为本地门路,既然是本地门路,这就导致一个问题,多人合作开发时,不同的人可能开发零碎不同,环境不同,导致本地门路也不同,这就给代码仓库不统一问题的产生提供了可能。

go work 工作区提出另一个计划,将本地多个 module 组成一个工作区,上述情况中如果在工作区内援用,则不再须要指定 replace,而是交给工作区自行解析解决。

举个例子,首先看没有 workspaces 的状况,一个 example 包想要援用另一个 pkg 包的做法

.
├── example
│   ├── go.mod
│   └── main.go
├── x_path
│   ├── go.mod
│   └── x_file.go

x_path/x_file.go

package pkg

func Func() {println("This is pkg package")
}

x_path/go.mod

module aaa/bbb/ccc/pkg

go 1.20

x_path 中定义了一个名叫 aaa/bbb/ccc/pkg 的模块,其中含有一个 package 名叫 pkg。

example/main.go 来援用 pkg 的 Func

package main

import "aaa/bbb/ccc/pkg"

func main() {pkg.Func()
}

目前运行 cd example && go run main.go 可定会报错,因为 golang 把 aaa/bbb/ccc/pkg 当成了零碎包,在 GOROOT 环境变量指向的门路中找不到这个包

main.go:3:8: package aaa/bbb/ccc/pkg is not in GOROOT (/usr/local/go/src/aaa/bbb/ccc/pkg)

假如将 x_path 的包名改成 xxx.com/ddd/eee/pkg 则 go 默认会将其视为一个仓库地址,会让你 go get 拉一下,不过基本问题跟下面一样,都是阐明 go 找不到合格 pkg 在哪,此时就须要用 replace 指令了。

在 example/go.mod 中指定,应用 go mod edit -replace aaa/bbb/ccc/pkg=../x_path 来生成替换指令,应用 go mod tidy 来指定一个 Pesudo 版本号

module main

go 1.20

replace aaa/bbb/ccc/pkg => ../x_path

require aaa/bbb/ccc/pkg v0.0.0-00010101000000-000000000000

之后再执行 go run main.go 就能够失去后果了。

$ go run main.go       
This is pkg package

到了 workspace 这边,只须要申明 example 和 x_path 两个文件夹处于同一个工作空间中,那么 go 会主动解决搜寻 package 和导入 package 的工作。

首先把 example/go.mod 中 require 和 replace 都删掉不再须要了,之后在 example 和 x_path 级别的目录中执行 go work init ./example ./x_path,会生成一个 go.work 文件,其中

go 1.20

use (
    ./example
    ./x_path
)

接着就能够执行 go run example/main.go 或者 cd example && go run main.go 了

参考

https://go.dev/blog/package-names

https://go.dev/ref/mod

本文由 mdnice 多平台公布

正文完
 0