这是《Go语言简易入门》系列内容第
6
篇,所有内容列表见:https://yishulun.com/books/go-easy/目录.html。所有源码及资料在“程序员LIYI”公号回复“Go语言简易入门”获取。
模块化是编程界的潮流,无论是前端Vue、微信小程序开发,还是后端Node.js、Golang开发,都讲究模块化。模块化的本质是分工协作,将功能相对独立完善的代码以模块方式发布,以便在其它程序中复用,这与汽车厂分别制造发动机、轮胎、车门等零件,然后再组装是一个道理。
那么在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下的代码了。这个设置,方便我们在本地进行模块代码,调试完成后再统一上传。
关于模块化编程,以上就是全部内部了。接下来我们补充了解一些相关的概念。
有时候我们需要临时修改这个变量的值,但并不需要永久修改。有两个方法:
go env -w GO111MODULE=on
export GO111MODULE=on
这是两种方式,以第二种效果最佳。第一种方式go env -w *
是一种Go语言提供的通用的编辑环境变量的方式。
可以的,在项目模块目录下,例如str,执行:
go mod vendor
这样就会在str目录下生成一个vendor子目录,它里面有所有的依赖包。
有三个值:
相关指令:
最常使用的子指令是init、download、tidy和vendor。
和原来是一样的。可以使用:
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"
三个网站的说明是这样的:
以逗号分隔。最后的direct代表到源地址下载。
我讲明白没有,欢迎留言讨论。
2021年1月23日
本文写作中参考了以下链接,一并致谢: