第一个Go语言类库:启用、创建并发布第一个模块

共 5771字,需浏览 12分钟

 ·

2021-01-25 16:43

这是《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

浏览 56
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报