Golang Package 与 Module 简介

网友投稿 1445 2022-11-02

Golang Package 与 Module 简介

Golang Package 与 Module 简介

软件是由代码组成的。为了复用代码,代码的组织出现了不同层次的抽象和实现,如 Module(模块),包(Package),Lib(库),Framwork(框架)等。

通常一个Project(项目),会根据功能拆分很多 module,常用的软件会打包成一个个共享库。在开源社区分享软件包是一件十分 cool 的事儿。这些软件包也有可能引用了其他的开源包,因此开源项目上经常会有软件相互依赖,或依赖某个包,或依赖某个包的某个版本。

现代的语言都有很多好用的包管理工具,如 pip 之于 python,gem 之于 ruby,npm 之于 nodejs。然而 Golang 早期版本却没有官方的包管理器,直到 go1.5 才添加了实验性的 vendor。尽管官方的不作为(保守),还是无法阻止社区的繁荣。社区诞生了许多工具,比较有代表如 govender,glide,gopm,以及半官方的 dep 等。

百花齐放的另外一层含义也是工具链混乱的表现。

终于在 go1.12,发布了官方的包管理工具 Go Module。go module 不仅带来了统一的标准,而且对于旧软件包的处理,以及所坚持的包管理哲学都挺有意思。下面将会从 go module 之前的包管理方式和升级到 go module 方式做一个简单的说明。

下面的例子以常见的使用方式做介绍,并没有深入其背后的原理与实现。例如go.sum 以及一些最下化处理原则不会涉及。部分内容闲扯一下 go 的流派观点。

GOPATH

在 go1.12 之前,安装 golang 之后,需要配置两个环境变量----GOROOT 和GOPATH。前者是 go 安装后的所在的路径,后者是开发中自己配置的,用于存放go 源代码的地方。在 GOPATH 路径内,有三个文件夹,分别是

bin: go 编译后的可执行文件所在的文件夹pkg: 编译非 main 包的中间连接文件src: go 项目源代码

开发的程序源码则放在src里,可以在src里创建多个项目。每一个项目同时也是一个文件夹。

go1.12 之后,淡化了 GOPATH,因此也可以忽略这部分内容。

Packagemain package

下面看一个简单的例子:

➜ golang echo $GOPATH/Users/master/golang➜ golang pwd/Users/master/golang➜ golang tree.├── bin├── pkg└── src└── demo└── main.go

4 directories, 1 file

➜ demo cat main.gopackage mainimport ("fmt")func main() {fmt.Println("hello world")}➜ demo go run main.gohello world

虽然 main 包的文件名也是 main.go,其实包名和文件名没有直接关系。

自定义 package

go 使用 package 来管理源文件。package 必须在一个文件夹内,且一个文件夹内也只能有一个package,但是一个文件夹可以有多个文件。下面自定义一个 package。

➜ demo tree.├── main.go└── service└── directory, 2 files➜ demo cat service/api

import "fmt"

func HandleReq(){fmt.Println("api - Handle Request")}➜ demo cat main.gopackage main

import ("fmt""./service")

func main() {fmt.Println("hello world")api.HandleReq()}

输出结果如下:

➜ demo go run main.gohello worldapi - Handle Request

➜ demo cat service/rpc.gopackage apiimport "fmt"func HandleResp(){fmt.Println("api - rpc.go Handle Request")}

在 main 函数里同样使用 包名.包函数 ---- api.HandleResp() 调用。其实很好理解,即同样的一个包,文件内容太多,拆分成多个文件而已。文件名跟包名没有直接关系。如果只有一个文件,通常可以写成包名。但是导入的时候,必须导入包所在的文件夹的路径。其实可以这样理解,import 的是 path(路径),那么go就去那个路径下搜索,搜索当然是查找包名。只不过通常习惯是 文件名 和 包名一致。

这包名和文件夹名不一致,是反模式。主要是为了直观的表示包名和文件夹名没有直接关系。

内嵌 package

在 service 内再创建一个文件夹 api,此时再命名一个 api.go 文件。即 文件夹名 和 包名 一致。其内容如下:

➜ demo cat service/api/api.gopackage apiimport "fmt"func HandleError(){fmt.Println("api api.go Handle Error")}

import package 就是导入包所在的文件夹,因此 main.go 如下:

import ("fmt""./service""./service/api")

但是这样编译会报错:

➜ demo go run main.go# command-line-arguments./main.go:6:9: api redeclared as imported package nameprevious declaration at ./main.go:5:2./main.go:11:2: undefined: "_/Users/master/golang/src/demo/service/api".HandleReq./main.go:12:9: undefined: "_/Users/master/golang/src/demo/service/api".HandleResp

但是在 main 函数里,都是使用包名引用函数。由于名字冲突,无法确定到底使用那个 api 包,进而报错。解决方法也很简单,给包名增加一个别名即可。

import ("fmt""./service"apiNew "./service/api")func main() {fmt.Println("hello world")api.HandleReq()api.HandleResp()apiNew.HandleError()}

这样 api 就是 service 空间下的,apiNew 就是 service/api 空间下,两者相互隔离。

导入规则

前面 import 语句内,通过相对路径导入了包。对于*nix系统,相对路径 . 通常是可以省略的。下面就省略试试:

import ("fmt""service"apiNew "service/api")

此时会发现报错:

➜ demo go run main.gomain.go:5:2: cannot find package "service" in any of:/usr/local/go/src/service (from $GOROOT)/Users/master/golang/src/service (from $GOPATH)main.go:6:9: cannot find package "service/api" in any of:/usr/local/go/src/service/api (from $GOROOT)/Users/master/golang/src/service/api (from $GOPATH)

报错信息也简单明了。即 go 先从 $GOROOT/src 搜索 service 包,找不到然后从 $GOPATH/src 里搜索。也找不到,进而报错。

这里就涉及到 go 的包搜索方式。通常 standard pkg(标准库)都在$GOROOT/src 里,因此会从这里搜索。而用户自定义的包,或者三方包,go是统一从 $GOPATH/src 里搜索的。当不使用 . 显式表明相对导入,那么 go 就会相对于 $GOPATH/src 导入。解决上面的方法就是把 service 包放到 demo 外面

➜ src tree.├── demo│ ├── main.go│ └── service│ ├── api│ │ └── api.go│ ├── └── rpc.go└── service├── api│ └── api.go├── rpc.go5 directories, 7

通常,如果 service 是 demo 的一个内部模块,那么就放到 demo 内,使用 . 方式相对导入。如果它是一个可以共享的 package,就可以放到 GOPATH 下。为什么 go会使用 GOPATH 这样的集中文件夹呢?

此外,想要把这个 service 包发布出去,可以使用 github 管理源代码和包版本。

首先需要从 github 创建一个 repo(仓库)。常见的形式是 github.com/username/repo。因此软件包的名也是这个规则。

例子:

初始化 service

在 github上创建一个 repo 叫 service, 并打上了 v1.0.0 的版本 tag。那么在mian.go 里引用就是这样:

import ("fmt""github.com/rsj217/service")

上述的 import 语句,无非就是增加了包 servic e的文件夹层级 github.com/rsj217 而已。这样做的主要目的是为了让 go get 工具从网络上的 url 地址拉取包。go get 会自动从 github.com/rsj217/service 拉取源码,并 copy 到 $GOPATH/src 下。运行go get 或者 go run main.go ,go 都会从 import 语句中解析并-软件包。

➜ demo go get github.com/rsj217/service➜ demo tree ../../├── demo│ └── main.go└── github.com└── rsj217└── service├── LICENSE├── README.md├── api│ └── api.go├── rpc.go5 directories, 6

从前面包的搜索方式就很容易理解。import "github.com/rsj217/service" go 就从 $GOPATH/src 下搜索,正好能搜索到 github.com/rsj217/service。

进入到 service文件夹内,可以看到就是完整的 git repo 的clone。修改提交后,即使删掉这个包。再使用 go get 也能再次-。

Why GOPATH

前面我们抛了一个问题: 为什么 go 会使用 GOPATH 这样的集中文件夹呢?go 不像 python 有一个中央仓库 PyPI 用来管理存储软件包,以提供给包管理工具 pip -。而 python 也不像 go 那样有个本地的 GOPATH 用来搜索软件包。

对于 python 开发者而言,源码怎么管理都无所谓。但是只要想分发软件包,就可以打包然后传到 pypi 即可。想要使用软件包,从 pypi 搜索即可(或从github.com)。

但是go是另外一种开发方式。从一些朋友了解到Google的开发方式。他们本身就有一个中央集中式中心用来管理软件包。使用,构建都从那里获取。因为外人无法访问google的中央,但是这种引用中央的开发模式被保留下来,因此就抽象成本地的GOPATH。

至于软件的分发,就从源代码拉取即可。有名的源代码版本管理无非就是 github 之类。google的做法就是让用户自己维护一个在本地类似 pypi 的中央库。

自己怎么维护呢?就是通过 go get 从 github 或者其他类似的网站上获取,然后打包,处理依赖。这样的一个好处就是自己把分散式的 github 开源包进行了本地的集中式处理。处理依赖的时候会比较方便,毕竟本地什么东西都有了。

但是从上面的 case 可以看出,go get 就类似 git clone。即使每次 go get 拉取仓库都是最新的版本。如何处理好版本依赖呢?

在go module之前也有不少解决方案,但是都或多或少有优劣。下面就介绍一下 go module 的使用方式。

虽然引入了 module,但是并没有废弃 go get,go get 还是基于 GOPATH 运作的,并且 go 的开发方式也没有放弃。只是减少了用户的心智负担,但也提升了用户体验。go get 会将远程的软件包 download 在 $home/go/pkg/mod 目录里。

没有了显示的 GOPATH,开发者的项目就不用像之前一样放到 GOPATH/src 下面了。用户可以在任何一个文件创建自己的项目。下面是一个例子:

➜ demo go mod init demogo: creating new go.mod: module demo➜ demo lsgo.mod➜ demo echo $GOPATH➜ demo cat go.modmodule demogo 1.12➜ demo tree.├── go.mod└── main.go0 directories, 2 files➜ demo less main.gopackage mainimport ("fmt"// "github.com/rsj217/service")func main() {fmt.Println("hello world")// api.HandleReq()// api.HandleResp()

编译运行 main.go,会发现项目即使没有 $GOPATH/src 下,也能正常运行。下面把上面的main.go 的注释去掉。再次编译运行

➜ demo go run main.gogo: finding github.com/rsj217/service v1.0.0go: downloading github.com/rsj217/service v1.0.0go: extracting github.com/rsj217/service v1.0.0hello worldapi - Handle Requestapi -- rpc.go Handle Response➜ demo cat go.modmodule demogo 1.12require github.com/rsj217/service v1.0.0 // indirect➜ demo go list -m alldemogithub.com/rsj217/service v1.0.0

go 会检测 main.go 里的 import 语句。并尝试根据 go.mod 的依赖引用关系导入三方包。如果发现本地cache没有,就会从远程拉取。就像是 go get。当 go module-了远程包后,同时会自动更新 go.mod 。

如上面就追加了 require github.com/rsj217/service v1.0.0 // indirect 这样一句。require 指令后跟软件包名和版本号。

软件包被缓存在 $home/go/pkg/mod 目录里, mod 目录就像之前的 src 一样。里面也有 github.com 目录。最终可以看到在 github.com/rsj217/ 文件下有 service@v1.0.0文件夹,里面正好就是之前 service 的代码

➜ service@v1.0.0 pwd/Users/master/go/pkg/mod/github.com/rsj217/service@v1.0.0➜ service@v1.0.0 tree.├── LICENSE├── README.md├── api│ └── api.go├── rpc.go1 directory, 5

service@v1.0.0 与之前的 go get clone 的service 项目不一样。前者只-了版本号为 1.0.0 的仓库(如果没有打tag,就是指定的某个commit),而后者是整个 repo 的clone。由此可见,go module 是可以精确的拉取目标版本。

语义化版本

go module 同时引入了 semver (Semantic Versioning)。它定义的版本号格式是:

vMAJOR.MINOR.PATCH

v:表示版本MAJOR:主版本,通常是大版本升级,导致向前不兼容MINOR:次版本,通常是向下兼容的 feturePATCH:修订版本,如一些bugfix。

下面针对刚才的 service@v1.0.0 升级到1.1.0 版本。通过 git 修改 的 tag。

v1.0.0

然后使用 go get -u 升级版本

➜ demo go get -ugo: finding github.com/rsj217/service v1.1.0go: downloading github.com/rsj217/service v1.1.0go: extracting github.com/rsj217/service v1.1.0

要升级或降级到更具体的版本,go get 允许通过在 包参数中添加 @version 后缀搜索。如 go get service@v1.0.0,go get service@dsa1ewr,或者 go get service@

可以看到,mod 里多了一个 service@v1.2.0 的文件夹包,go.mod 也引用了 v1.2.0的软件。运行main.go ,也输出了 1.1.0 升级后的内容。

大版本升级

升级 MINOR 和 PATCH 都比较简单,升级 MAJOR 稍微复杂一点点。再次修改service 仓库,然后打一个 V2.1.0 的tag。推送到远程分支。再使用go get -u 升级

➜ demo go get -ugo: downloading github.com/rsj217/service v2.1.0+incompatiblego: extracting github.com/rsj217/service v2.1.0+incompatible➜ demo go list -m alldemogithub.com/rsj217/service v2.1.0+incompatible➜ demo cat go.modmodule demogo 1.12require github.com/rsj217/service v2.1.0+incompatible➜ demo go run main.gohello worldapi - Handle Requestv2.1.0api -- rpc.go Handle Response

可以看到,在-软件包的时候,版本号跟着有一个 incompatible 标记 。incompatible 表示是大版本升级,与之前的版本可能不兼容。但是运行main.go的时候,依然可以正确的找到 2.1.0的版本。并且 $home/go 里的软件包也多了一个 service@v2.1.0+incompatible。

旧包升级到 go module

然后更新-依赖

➜ demo go get github.com/rsj217/service@v2.2.0go: finding github.com/rsj217/service v2.2.0go: downloading github.com/rsj217/service v0.0.0-20190508051156-b9ee113ae63ego: extracting github.com/rsj217/service v0.0.0-20190508051156-b9ee113ae63e

查看本地的包会发现有之前的好几个版本,其中 v2.2.0 的被命名为 service@v0.0.0-20190508051156-b9ee113ae63e 。因为刚初始化的 module 认为没有打上 tag,对于没有打 tag 的,go.mod 的格式是 pseudo-version。它的含义是v0.0.0-yyyymmddhhmmss-abcdefabcdef

查看所-的软包

➜ rsj217 lsservice@v0.0.0-20190508051156-b9ee113ae63e service@v1.1.0service@v1.0.0 service@v2.1.0+incompatible➜ demo cat go.modmodule demogo 1.12require github.com/rsj217/service v0.0.0-20190508051156-b9ee113ae63e➜ demo go run main.gohello worldapi - Handle Requestv2.2.0api -- rpc.go Handle Response

go module 多版本共存

上述的 v2.2.0 没有被 go module 识别,是因为 go.mod 的 module github.com/rsj217/service 没有带上 MAJOR 版本号。将 go.mod 改成 module github.com/rsj217/service/v3,即新的 go.mod,然后再打上一个v3.0.0 的版本再更新。

由于(假设) v3.0.0 与 之前的版本都不兼容,但是demo项目又同时需要两个版本的包,此时就是 go module 比之前 go get 方式优越了。

正如 v3.0.0 的 go.mod 加上了版本号,因此对于使用者而言,其实可以理解为是不同命名空间包。就像之前 service 下的 api 和 service/api 下的 api 一样。需要用别名区别。

修改 go.mod 增加两个版本的包。v2.2.0 和 v3.0.0

➜ demo cat go.modmodule demogo 1.12require (github.com/rsj217/service v0.0.0-20190508051156-b9ee113ae63egithub.com/rsj217/service/v3 v3.0.0)➜ demo cat main.gopackage mainimport ("fmt""github.com/rsj217/service" // 使用 v2.2.0 的包apiv3 "github.com/rsj217/service/v3" // 使用 v3.0.0 的包)func main() {fmt.Println("hello world")api.HandleReq()api.HandleResp()apiv3.HandleReq()}➜ demo go run main.gogo: downloading github.com/rsj217/service/v3 v3.0.0go: extracting github.com/rsj217/service/v3 v3.0.0hello worldapi - Handle Requestv2.2.0api -- rpc.go Handle Responseapi - Handle Requestv3.0.0

可以看到 main.go 里的 import 语句,显示的区分了 v2 版本和 v3 版本。查看 home/go 下面的目录,会发现多了一个 service 文件夹,里面放了一个 v3 版本的包。

不管怎样,go module 通过 vMAJOR 让用户可以同时使用多个版本的软件包。但是需要修改使用者的代码,还是略麻烦。不过既然进行了向前不兼容,又得保证旧代码正常work,想来也没有更好的办法。

本地包

开源社区的最大好处就是共享智慧。可是实际开发中,开源的包也未必都十分可靠。通常可以提 PR 提 ISSUE 参与改进。但是原作者要是不维护就得需要自己 fork 修改。

还有一些涉及到商业方向的代码包,总不能扔到 github 上开源。因此本地构建也是一个需求。例如上面的 v3.0.0 有类似的 bug。可以在本地修改一个版本,然后通过 go module 的 replace 指令替换到 github 上的包。

➜ demo go mod vendor➜ demo lsgo.mod go.sum main.go vendor➜ demo cd vendor➜ vendor tree.├── github.com│ └── rsj217│ └── service│ ├── LICENSE│ ├── README.md│ ├── go.mod│ ├── ├── rpc.go│ └── v3│ ├── LICENSE│ ├── README.md│ ├── go.mod│ ├── └── rpc.go└── modules.txt4 directories, 11

使用 go vendor,可以将依赖包copy到项目的 vendor文件下。如上面的例子 vendor下的结构和之前的 GOPATH/src home/go 类似。修改 v3 下的 v3 cat apiimport "fmt"func HandleReq(){fmt.Println("api - Handle Request")fmt.Println(" vendor v3.0.0")}

然后修改 go.mod ,添加 replace指令

➜ demo cat go.modmodule demogo 1.12require (github.com/rsj217/service v0.0.0-20190508051156-b9ee113ae63egithub.com/rsj217/service/v3 v3.0.0)replace github.com/rsj217/service/v3 v3.0.0 => ./vendor/github.com/rsj217/service/v3➜ demo go run main.gohello worldapi - Handle Requestv2.2.0api -- rpc.go Handle Responseapi - Handle Requestvendor v3.0.0➜ demo

依赖关系

通过上面从一个普通的 go package 的改造成 go module 的例子。可以看到 go 在包管理工具上的努力。尤其是对不兼容版本的支持,还是有一定防范。但是需要改源文件,还是略显不够优雅。另外,go mod还提供了一些了命令。如 go list 可以看出当前 module 的依赖

➜ demo go list -m -json all{"Path": "demo","Main": true,"Dir": "/Users/master/demo","GoMod": "/Users/master/demo/go.mod","GoVersion": "1.12"}{"Path": "github.com/rsj217/service","Version": "v0.0.0-20190508051156-b9ee113ae63e","Time": "2019-05-08T05:11:56Z","Dir": "/Users/master/go/pkg/mod/github.com/rsj217/service@v0.0.0-20190508051156-b9ee113ae63e","GoMod": "/Users/master/go/pkg/mod/cache/download/github.com/rsj217/service/@v/v0.0.0-20190508051156-b9ee113ae63e.mod","GoVersion": "1.12"}{"Path": "github.com/rsj217/service/v3","Version": "v3.0.0","Replace": {"Path": "./vendor/github.com/rsj217/service/v3","Dir": "/Users/master/demo/vendor/github.com/rsj217/service/v3","GoVersion": "1.12"},"Dir": "/Users/master/demo/vendor/github.com/rsj217/service/v3","GoMod": "/Users/master/demo/vendor/github.com/rsj217/service/v3/go.mod","GoVersion": "1.12"}

从上面的 json 可以看出,demo 项目依赖 github.com/rsj217/service 的 "v0.0.0-20190508051156-b9ee113ae63e" 版本,即 git 的(v2.2.0)。还依赖 github.com/rsj217/service/v3 的 v3.0.0 也是 git 的(v3.0.0)

总结

golang 1.12 正式发布了 go module 特性。go module 作为官方的包管理解决方案。既然针对旧软件包能work,也可以将旧软件包升级为 go module。

go module 淡化了 GOPATH 的概念。用户不设置 GOPATH,golang会自动在 home/go/pkg/mod 中,并且可以存储多个版本的包。

总而言之,go module在不对原有的包管理带来破坏的情况下,引入了新的workflow,只要遵循这些约定,还是可以轻松的解决日常开发的包管理问题。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Hadouken是一个用于构建具有桌面体验的Web应用程序的开源运行时
下一篇:展示 Android 程序方法调用链的 gralde 插件,支持输出html文件和方法折叠
相关文章

 发表评论

暂时没有评论,来抢沙发吧~