第一个Go语言类库:启用、创建并发布第一个模块
这是《Go语言简易入门》系列内容第
6
篇,所有内容列表见:https://yishulun.com/books/go-easy/目录.html。所有源码及资料在“程序员LIYI”公号回复“Go语言简易入门”获取。
模块化是编程界的潮流,无论是前端Vue、微信小程序开发,还是后端Node.js、Golang开发,都讲究模块化。模块化的本质是分工协作,将功能相对独立完善的代码以模块方式发布,以便在其它程序中复用,这与汽车厂分别制造发动机、轮胎、车门等零件,然后再组装是一个道理。
GO111MODULE的由来
那么在Go语言开发中,如何进行模块化开发呢?
默认在官方教程《如何使用Go编程》中是不讲这一块的,环境变量GO111MODULE默认是关闭的,运行官方示例也不会受到影响。但模块化确实是非常重要的概念,是任何想认真使用这门语言的开发者都避不开的。
上面我们提到了GO111MODULE,什么是GO111MODULE?
这个名称中有三个数字一,不是字母“l”,是数字“1”,它表示在Go语言1.11版本中加入的环境变量。单从这个名称来看,它很有可能被干掉,但事实上一真没有。
在以前最早2009年Go语言发布的时候,源码都是通过GOPATH管理的。怎么理解呢?在代码中我们通过import关键字引入一个第三方类库,Go程序会依次向GOPATH、GOROOT这两个总目录下去查找,哪个先查到,就用哪个。
但是我们知道,位于github上的类库,master分支是最新源码,这个源码经常变动,有时候我们使用的仅是历史上的某个版本。有的开发者注意到了这一点,所以当类库重构的时候,会将旧代码打一个Release版本,这样即使源码修改了,只要我们找到历史版本,也不影响我们程序的正常运行。
但是问题起来,有的程序需要用某个类库的新版本,有的需要用旧版本,GOPATH只有一个,怎么处理这个矛盾呢?
那个时候我用的是最笨的方法,起新项目的时候,我将GOPATH目录复制一份,并修改GOPATH变量为复制后的新目录。一个项目对应一个GOPATH,这样不同项目的类库版本就不会相互掣肘了。
可能不止我一个人这么使用。Go语言在1.5版本的时候,推出了一个vendor特征,它充许我们将当前项目所用的所有第三方类库,全部自动拷贝到一个叫做vendor的子目录下。Go程序在编译的时候,会首先向vendor目录查找,如果没找到,再向GOPATH、GOROOT目录查找。
但是这种方式并没有从根本上在Go语言中解决模块化编程的问题,项目在共享和分发时,随身携带许多第三方类库的源码,既占空间,又不利于统一升级类库。如果第三方类库在新版本中修复了一个bug,而我们需要更新,在多个项目中更新将是一件麻烦事。
后来,在Go语言1.11版本中,Go语言推出了GO111MODULE环境变量,及mod子指令,基于这个变量和子指令,可以完美模块化编程了。接下来我们看看,一般是怎么做的。
创建并发布自己的第一个模块
首先我们在GOPATH路径外面创建一个目录:
rixingyike/
first
main.go
str
reserve.go
这是两个示例。first目录是测试代码,用于测试我们发布的模块。str是我们准备创建和发布的模块。模块位于多级目录下,这是我们故意为之的。go语言的类包都是单名一级引入,但在实际的项目开发中,我们的模块往往处于多级目录下,我们看看这种情况一般是怎么处理的。
先看一下模块str/reserve.go的源码:
// go-easy/rixingyike/str/reserve.go
package str
import(
"fmt"
"github.com/kataras/iris/v12"
)
// Reverse 将其实参字符串以符文为单位左右反转
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
fmt.Println(string(r))
return string(r)
}
// StartServer ...
func StartServer() {
app := iris.New()
app.Handle("GET", "/user/{id:uint64}", func(ctx iris.Context) {
id, _ := ctx.Params().GetUint64("id")
ctx.JSON(id)
})
app.Listen(":8080")
}
我们在这个文件中引入了iris框架。我们需要在这个模块中启用go mod,执行如下指令:
cd go-easy/rixingyike/str/
go mod init gitee.com/rxyk/go-easy/rixingyike/str
go mod指令后面是我们模块的名称,注意这里分部分,前面gitee.com/rxyk/go-easy
是我们的仓库地址,后面/rixingyike/str
是仓库中模块的相对路径。
这里有一个问题值得注意下,就是我们的module name是gitee.com/rxyk/go-easy/rixingyike/str
,但是reserve.go文件中的package名称却是str,后者是引入以后在源码中使用的单名,这两个名称是不需要也不能一致的。
接下来是关键,接着执行指令:
go env -w GOPRIVATE="gitee.com"
git tag rixingyike/str/v1.0.0
git push origin rixingyike/str/v1.0.0
第一行指令,是将gitee.com这个域名添加进GOPRIVATE变量中,GOPRIVATE这个变量的值可以用逗号分隔添加多个值,但这里我们不需要添加多个。这一步的环境变量设置,是为了跳过对gitee.com域名的网络代理。这是国内网站,是不需要代理的。
第二行和第三行指令是创建一个新的tag,并提交到远端仓库里。这里的关键是tag名,前面rixingyike/str/
是模块在仓库中的相对路径,后面v1.0.0
才是模块的版本号。默认情况下,如果类库在根目录下是不需要这样处理的,直接写一个像v1.0.0
这样的版本号就可以了。
使用自己的第一个模块并在本地调试
现在模块已经在线上发布了,接下来我们看一下怎么使用。
现在我们切换到first例目录,并进行module初始化,执行如下指令:
cd go-easy/rixingyike/first/
go mod init gitee.com/rxyk/go-easy/rixingyike/first
vim main.go ...
go get gitee.com/rxyk/go-easy/rixingyike/str@v1.0.0
第二行指令中这个module的名称,因为不需要对外发布,其实是无所谓的。接下来编辑main.go的源码:
// go-easy/rixingyike/first/main.go
package main
import (
"fmt"
"gitee.com/rxyk/go-easy/rixingyike/str"
"github.com/nleeper/goment"
)
func main() {
fmt.Printf("%s\n",str.Reverse("hi,ly"))
var g,_ = goment.New("2021-01-23 09:30:26")
println(g.ToString())
str.StartServer()
}
在这个测试示例中,我们引入了goment和str这两个模块,其中后者是我们自己定义的。
我们看一下自动生成的go mod文件:
// go-easy/rixingyike/first/go.mod
module gitee.com/rxyk/go-easy/rixingyike/first
go 1.15
// replace gitee.com/rxyk/go-easy/rixingyike/str v1.0.0 => ../str
require (
gitee.com/rxyk/go-easy/rixingyike/str v1.0.0
github.com/nleeper/goment v1.4.0
)
输出是这样的:
yl,ih
yl,ih
2021-01-23 09:30:26 +0000 UTC
Now listening on: http://localhost:8080
Application started. Press CMD+C to shut down.
在这个文件中,第三行代码replace,是将依赖包替换。有两个作用:
如果某个类库因为网络原因,不能下载,可以用这个功能
我们自己开发的模块,需要在本地调试
我们将这行配置反注释一下,而main.go中的import引入代码不需要修改,再运行代码,调用的就是本地的str下的代码了。这个设置,方便我们在本地进行模块代码,调试完成后再统一上传。
关于模块化编程,以上就是全部内部了。接下来我们补充了解一些相关的概念。
如何临时修改GO111MODULE变量?
有时候我们需要临时修改这个变量的值,但并不需要永久修改。有两个方法:
go env -w GO111MODULE=on
export GO111MODULE=on
这是两种方式,以第二种效果最佳。第一种方式go env -w *
是一种Go语言提供的通用的编辑环境变量的方式。
开启go mod后,还能再使用vendor统一打包源码吗?
可以的,在项目模块目录下,例如str,执行:
go mod vendor
这样就会在str目录下生成一个vendor子目录,它里面有所有的依赖包。
GO111MODULE有哪些有效值?
有三个值:
GO111MODULE=off,不支持module功能,此时查找依赖包的次序是:vendor、GOPATH、GOROOT。
GO111MODULE=on,支持使用modules,会从vendor目录下查找,但不会去GOPATH、GOROOT目录下查。
GO111MODULE=auto,是默认值,自动性取决于上下文目录。$GOPATH/src之中的项目继续使用GOPATH模式;$GOPATH/src之外的项目使用模块化模式。
go mod指令,除init外,还有哪些子指令?
相关指令:
download download modules to local cache (下载依赖的module到本地cache))
edit edit go.mod from tools or scripts (编辑go.mod文件)
graph print module requirement graph (打印模块依赖图))
init initialize new module in current directory (在当前文件夹下初始化一个新的module, 创建go.mod文件))
tidy add missing and remove unused modules (增加丢失的module,去掉未使用的module)
vendor make vendored copy of dependencies (将依赖复制到vendor下)
verify verify dependencies have expected content (校验依赖)
why explain why packages or modules are needed (解释为什么需要依赖)
最常使用的子指令是init、download、tidy和vendor。
启用go mod后,如何查询和安装指定版本的依赖包?
和原来是一样的。可以使用:
go get github.com/kataras/iris/v12@latest
@符号后面是版本号,latest代表最新。这个版本就是git网站上的发行版标签。可以用如下指令查询所有可用标签名:
go list -m -versions github.com/kataras/iris/v12
输出:
v12.0.0 v12.0.1 v12.1.0 v12.1.1 v12.1.2 v12.1.3 v12.1.4 v12.1.5 v12.1.6 v12.1.7 v12.1.8 v12.2.0-alpha v12.2.0-alpha2
其中地址中的v12是什么?它是该仓库的一个分支。它还有另一个分支:v0.0.1。
引入国外的一些类库,如何设置代理?
使用GOPROXY变量。我的设置是这样的:
export GOPROXY="https://goproxy.io,https://mirrors.aliyun.com/goproxy/,https://goproxy.cn,direct"
三个网站的说明是这样的:
https://goproxy.io 最早的Go模块镜像代理网站
https://mirrors.aliyun.com/goproxy/ 阿里镜像代理网站
https://goproxy.cn 七牛云赞助支持的代理网站 |
以逗号分隔。最后的direct代表到源地址下载。
我讲明白没有,欢迎留言讨论。
2021年1月23日
本文写作中参考了以下链接,一并致谢:
https://blog.csdn.net/yptsqc/article/details/105270530
https://morven.life/notes/the_go_language/
https://github.com/goproxy/goproxy.cn/issues/9