详解go语言包管理方式
本文详细介绍go语言包管理方式(go mod), 分析多目录多文件下的管理,不同工程下包的相互调用。 ps: 文章主要来自详解go语言包管理方式,对其中的内容稍加修改。
1. 使用go mod
很多时候go程序找不到包导致无法运行的问题, 都是因为没有搞懂当前的包管理方式。
1) 以前的默认模式,必须将项目放在GOPATH/src下
2) 使用go mod包管理方式,项目可以放在任意位置,这样目录下需要有go.mod文件
PS1: 如果你是初学者, 建议看完, 学懂包管理方式是深入学习go语言的基础 PS2: 在文章最后会介绍在vscode中当弹出某个提示包不存在, 但点击install all总是会超时失败的问题
本文主要从以下三点展开分析:
1) GO111MODULE
的三种模式
2) 将项目放在GOPATH/src下,但使用go.mod包管理的方式
3) 多文件、多目录下,go mod包管理的使用细节
通过go env
命令 可以看到GO111MODULE
字段。可以通过export GO111MODULE=”“来修改,当然这种命令的方式是在linux下,若是windows平台,直接去设置环境变量即可。
1.1 GO111MODULE三种状态
GO111MODULE有三种状态:
1) auto状态
-
如果在GOPATH/src下,但存在go.mod文件, 就采用go mod管理方式。
-
如果在GOPATH/src下,但没有go.mod文件, 采用以前默认的方式。
-
若未在GOPATH/src下,自然是采用go mod包管理方式(前提是需要go.mod文件存在)
2) on状态
不管在不在GOPATH/src下,都采用go mod包管理方式。
3) off状态
就是以前的默认方式(这时候项目必须放在GOPATH/src下),若需要引用外部包文件,使用go get命令下载下来。
比如在一个.go
文件中import(“github.com/gin-gonic/gin”), 那么使用go get github.com/gin-gonic/gin,并且这个下载下来的资源会放在GOPATH/src
下,而使用go mod包管理的方式,下载下来的资源会放在GOPATH/pkg下,后面会用测试案例详细介绍如何操作。
1.2 小细节
采用go mod包管理方式, 虽然不会去GOPATH/src
下找资源,但是会去GOPATH/pkg
下找资源, 同时还会去GOROOT/src
下寻找(回忆一下, 最常用的fmt.Println(), fmt等等那些包就在那里);
采用以前的默认方式, 就会去GOPATH/src
, 以及GOROOT/src
下寻找,不管是哪种方式都需要去GOROOT/src
,因为fmt等包是在安装go的时候就下载好的资源。
2. 实例演示
先用go env
查看go的环境变量,重点记住GOPATH
路径:
# go env GOPATH="/opt/extra" GOPATH="/opt/extra" GOPRIVATE="" GOPROXY="https://proxy.golang.org,direct" GOROOT="/usr/local/go"
ps: 如果上述GOPROXY访问不通,换成国内七牛云代理go env -w GOPROXY=https://goproxy.cn,direct
2.1 采用以前默认方式
这里我们先展示以前的默认方式,执行如下命令:
# go env -w GO111MODULE=off # go env GO111MODULE="off" GOARCH="arm64" GOBIN="/usr/local/go/bin"
执行上述命令后,表示关闭go mod包管理方式,采用默认的模式,那么我们的工程就必须放在GOPATH/src
下(我的GOPATH是/opt/extra)。
为了更好的演示为什么以前默认的方式必须放在GOPATH/src下,这里我们在GOPATH/src(即/opt/extra/src)下再创建一个文件夹:
# pwd /opt/extra/src # mkdir golang_modoff_demo1 # cd golang_modoff_demo1 # touch -p mkdir -p pkg/util # touch main.go pkg/util/test.go # tree . ├── main.go └── pkg └── util └── test.go 2 directories, 2 files
其中test.go内容如下:
现在我们需要在main.go中调用这个Test()函数,main.go内容如下:
执行如下命令运行:
# go run main.go I'm pkg/util/Test()
这里我们主要来分析”golang_modoff_demo1/pkg/util”,对于以前的默认方式,也就是项目必须放到GOPATH/src的原因,其他是它会在”golang_modoff_demo1/pkg/util”的前面自动加上GOPATH/src
路径,完整的写出来其实是:opt/extra/src/golang_modoff_demo1/pkg/util。
2.2 采用go mod包管理方式
首先执行如下命令:
# go env -w GO111MODULE=auto # go env GO111MODULE="auto" GOARCH="arm64" GOBIN="/usr/local/go/bin"
这里为什么使用auto
而不使用on
呢?这是因为想给大家分析在GOPATH/src下却使用auto模式
时,golang到底会采用go mod包管理方式还是以前的默认方式。答案是如果存在go.mod文件就会用go mod包管理方式,如果没有go.mod就使用以前默认方法,当然前提是放在GOPATH/src目录下。
执行如下命令:
# pwd /opt/extra/src # mkdir golang_modauto_demo1 # cd golang_modauto_demo1 # go mod init modauto go: creating new go.mod: module modauto # mkdir pkg/util # touch main.go pkg/util/test.go
其中test.go内容如下:
main.go内容如下:
这种情况下我们如何才能在main.go中调用pkg/util下的Test()函数呢?我们尝试直接go run main.go
:
# go run main.go main.go:4:9: package golang_modauto_demo1/pkg/util is not in GOROOT (/usr/local/go/src/golang_modauto_demo1/pkg/util)
可以看到以上报错,其实很细节,为什么没去GOPATH/src下找呢? 因为我们此时是go mod包管理方式,那么又为什么要去GOROOT(GOROOT/src)下找呢? 因为像fmt那些包都在那,所以不管是否开启go mod包管理模式都会去GOROOT/src找。
正确的方式是将上面import “golang_modauto_demo1/pkg/util”换成import “modauto/pkg/util”:
代码中的modauto
就是我们上面执行go mod init modauto命令生成的项目模块名称,我们可以在go.mod中看到。此时我们再次运行:
# go run main.go I'm pkg/util/Test()
可以看到项目执行成功。这里注意到我们最开始使用的go mod init modauto的重要性没有?modauto
代替了当下的绝对路径,这里modauto表示的是:/opt/extra/src/golang_modauto_demo1,所以它并不依赖GOPATH/src。你将项目移到其他位置,modauto
就会表示那个位置的绝对路径,modauto可以换成任意字符,比如你最开始用的是go mod init demo_test,那么这里就要import “demo_test/pkg/util”,我们可以在go.mod中对它(modauto
)进行修改。
以上就是默认方式
以及go mod包管理方式的简单使用。
2.3 go mod使用扩展1
本节我们展示采用go mod包管理方式,如何调用不同工程中的包?
首先执行如下命令:
# go env -w GO111MODULE=on # go env GO111MODULE="auto" GOARCH="arm64" GOBIN="/usr/local/go/bin"
在我们前面的例子中,所有的文件都是在同一个工程下,接下来我们创建两个工程,我直接给出方法以及如何写代码,建议自行放到电脑上运行查看以加深理解。
在任意位置创建两个文件夹:
# pwd /opt/workspace # mkdir golang_modon_demo1 golang_modon_demo2 # ls golang_modon_demo1 golang_modon_demo2
我们的目标是在golang_modon_demo2
工程中调用golang_modon_demo1
中的util包,使用该包中的SayHello()
函数。
ps: 上面我们故意没有将两个文件夹放在GOPATH/src目录下
1)golang_modon_demo1工程
在golang_modon_demo1
工程中执行如下命令:
# pwd /opt/workspace/golang_modon_demo1 # mkdir -p pkg/util # touch pkg/util/hello.go
其中hello.go
内容如下:
回到golang_modeon_demo1工程根目录(/opt/workspace/golang_modon_demo1), 执行如下命令:
# go mod init github.com/ivanzz1001/golang_modon_demo1 go: creating new go.mod: module github.com/ivanzz1001/golang_modon_demo1 go: to add module requirements and sums: go mod tidy
上面为什么这样命名呢?主要是方便你后续可以把包提交到github上供他人调用。就像我们上边说的go mod init后边的名字是自己取的,这里的’github.com/ivanzz1001/golang_modon_demo1’就代表的是golang_modon_demo1文件夹的绝对路径,相当于/opt/workspace/golang_modon_demo1
2) golang_modon_demo2工程
现在我们进入golang_modon_demo2工程,执行如下命令:
# go mod init modon_demo2 go: creating new go.mod: module modon_demo2
然后在golang_modon_demo2根目录下创建main.go,调用golang_modon_demo1工程中的util包:
显然github.com/ivanzz1001/golang_modon_demo1并不是github官网上的,而是我们本地的,所以我们需要修改golang_modon_demo2/go.mod文件:
replace
不仅可以这样做,比如你以前在github上引用的包,但时间长了可能作者改变了它的位置。举例:
当然上面版本号只是举例,不一定是这个版本。其实它的意思就是, 去把新位置(github.com/piannide/gin)的包下载下来放到了老位置(GOPATH/pkg/github.com/gin-gonic)下,这样就可以继续使用了,而不用做太大改动。
回到我们的golang_modon_demo2工程根目录,执行如下命令:
# go run main.go hello, world
可以看到工程执行成功。
3) 处理包名字重复
这里还有一个小的细节,比如某个包的名字重复了。我们在golang_modon_demo2工程下添加pkg/util/datetime.go文件:
# pwd /opt/workspace/golang_modon_demo2 # mkdir -p pkg/util # touch pkg/util/datetime.go
datetime.go内容如下:
此时,这个包名还是util
,前面我们引用的golang_modon_demo1工程中的包也是util
,那怎么区别呢?
去看看golang_modon_demo2工程下的go.mod内容,可以看到我们的项目名为modon_demo2
,然后我们将golang_modon_demo2/main.go修改为如下:
执行如下命令运行:
# go run main.go now: 1694091563 hello, world
4) 引用github上面的包
上面的内容都是对本地包的引用,那么如果想引用github上的包该怎么操作呢?我们以github.com/gin-gonic/gin为例子。
首先创建golang_modon_demo3工程:
# pwd /opt/workspace # mkdir golang_modon_demo3 && cd golang_modon_demo3 # go mod init modon_demo3
上面我们使用go mod包管理方式: 执行go mod init modon_demo3生成go.mod文件。接着添加main.go内容如下:
把上面的main.go编写好后,执行go mod tidy
,它就会自动去查找工程下所有.go
文件引用的外部资源,并自动下载下来。下载下来后可以去’GOPATH/pkg/mod/github.com/’中看到gin-gonic。
# go mod tidy go: finding module for package github.com/gin-gonic/gin modon_demo3 imports github.com/gin-gonic/gin: module github.com/gin-gonic/gin: Get "https://proxy.golang.org/github.com/gin-gonic/gin/@v/list": dial tcp 172.217.163.49:443: i/o timeout
上面我们将GOPROXY修改为七牛云代理(https://goproxy.cn,direct
):
# go env -w GOPROXY=https://goproxy.cn,direct # go mod tidy go: finding module for package github.com/gin-gonic/gin go: downloading github.com/gin-gonic/gin v1.9.1 go: found github.com/gin-gonic/gin in github.com/gin-gonic/gin v1.9.1 go: downloading github.com/gin-contrib/sse v0.1.0 go: downloading github.com/mattn/go-isatty v0.0.19 go: downloading golang.org/x/net v0.10.0 go: downloading github.com/stretchr/testify v1.8.3 go: downloading google.golang.org/protobuf v1.30.0 go: downloading github.com/bytedance/sonic v1.9.1 go: downloading github.com/goccy/go-json v0.10.2 go: downloading github.com/json-iterator/go v1.1.12 go: downloading github.com/pelletier/go-toml/v2 v2.0.8 go: downloading github.com/ugorji/go/codec v1.2.11 go: downloading gopkg.in/yaml.v3 v3.0.1 go: downloading github.com/go-playground/validator/v10 v10.14.0 go: downloading golang.org/x/sys v0.8.0 go: downloading github.com/davecgh/go-spew v1.1.1 go: downloading github.com/pmezard/go-difflib v1.0.0 go: downloading github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd go: downloading github.com/modern-go/reflect2 v1.0.2 go: downloading github.com/gabriel-vasile/mimetype v1.4.2 go: downloading github.com/go-playground/universal-translator v0.18.1 go: downloading github.com/leodido/go-urn v1.2.4 go: downloading golang.org/x/crypto v0.9.0 go: downloading golang.org/x/text v0.9.0 go: downloading github.com/go-playground/locales v0.14.1 go: downloading github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 go: downloading golang.org/x/arch v0.3.0 go: downloading github.com/klauspost/cpuid/v2 v2.2.4 go: downloading github.com/twitchyliquid64/golang-asm v0.15.1 go: downloading github.com/go-playground/assert/v2 v2.2.0 go: downloading github.com/google/go-cmp v0.5.5 go: downloading gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 go: downloading golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 # ls /opt/extra/pkg/mod/ cache github.com golang.org google.golang.org gopkg.in
PS: 对比以前的默认方式,以前是使用’go get github.com/gin-gonic/gin’, 然后这个资源会下载到GOPATH/src中, 当然go mod包管理方式也是可以使用go get命令的。
此时,我们执行如下命令运行:
另一个窗口向8080端口发起ping请求:
# curl -X GET http://localhost:8080/ping {"message":"pong"}
2.4 go mod使用扩展2
go mod的方式如何在多文件中应用呢? 比如我们有如下工程结构:
├── calc │ └── calc.go ├── go.mod ├── main.go ├── main_son.go └── pkg └── util ├── t1.go ├── t2.go └── t3.go
我们的目标是:
1) 如何在calc.go中调用pkg/util中的包函数;
2) main包实现的功能如何拆分在不同文件中;
这里我们又会学习到一个新的小知识,比如这里的t1.go,t2.go和 t3.go,只要包名一样(main包有点区别,后边说),他们的功能实现可以在不同文件中。
ps: 上面的go.mod 是通过go mod init modon_demo3生成的
执行如下命令创建golang_modon_demo4
工程:
# pwd /opt/workspace # mkdir golang_modon_demo4 && cd golang_modon_demo4 # go mod init modon_demo4 go: creating new go.mod: module modon_demo4
如下各文件均按上述目录结构创建:
1) calc.go文件内容
2) t1.go/t2.go/t3.go文件内容
- t1.go文件
- t2.go内容
- t3.go文件
至此第一个目标实现。
由上可见,对于普通包,这里是util包,可以直接引用同包名下其他文件的函数,而main包有点区别main. go和 main_son.go都数据main包,我们去看一下他们的实现。
3) main_son.go文件内容
4) main.go文件内容
可以看到我把test()注释了,因为他是在main_son.go中实现的,在这种情况下我们使用go run main.go
程序是可以正常执行的, 但当你打开注释,会提示:
go run main.go # command-line-arguments ./main.go:26:2: undefined: test
此刻的正确方式是将main_son.go 放到命令行参数中,如go run main.go main_son.go
,此刻即可正常执行。
# go run main.go main_son.go this is main entry =========================== I'm say1 =========================== I'm say2 =========================== I'm say3, I will use say1 and say2 I'm say1 I'm say2 ========================== 我是calc, 我在这里调用了Say3() I'm say3, I will use say1 and say2 I'm say1 I'm say2 sum(1,2) is 3 =========================== this is test in main package
2.5 go mod使用扩展3
这里讲述以下包名和目录名不同的情况下如何调用。我们有如下目录:
. ├── go.mod ├── pkg │ └── hello.go └── test.go
执行如下命令创建golang_modon_demo5
工程:
# pwd /opt/workspace # mkdir golang_modon_demo5 && cd golang_modon_demo5 # go mod init modon_demo5 go: creating new go.mod: module modon_demo5
go.mod
内容如下:
1) hello.go内容
2) test.go内容
那么我们在test.go中如何调用SayHello()呢?参看如下test.go实现:
执行如下命令运行:
# go run test.go this is the main entry hello, world
超级重点:引入包的时候,默认的就是把路径的最后一截路径名当作包名,如果不加上这个anotherpkg,就会去找pkg包(因为引入的是modon_demo5/pkg),但并没有这个包,就会报错,同时还需要注意,pkg这个目录下不能出现两种包名(也就是说如下面的目录,hello.go和xixi.go属于同一个路径下,那么他们的包名必须是一致的)
. ├── go.mod ├── pkg │ ├── hello.go │ ├── util │ └── xixi.go ├── test.go └── xx.go
上边取的别名不一定是anotherpkg
,你可以这样想,不管你取什么,它都是为了去顶替这个路径下的包
ps: 刚才已经说了一个路径下(嵌套的路径是单独的,比如这里的util相对于pkg路径来说就是嵌套的)只能存在一种包名
所以至于这个包名是啥已经不重要了,但如果你不写别名,并且包名不是pkg那么就会出错,因为它默认会去找以路径的最后一截命名的包(这里对应pkg)
2.5 go mod使用扩展4
go.mod路径下还有go.mod文件,怎么去调用?比如我们有如下目录:
. ├── go.mod ├── main.go └── tt ├── go.mod ├── pkg │ └── xixi.go └── test.go
嵌套了go.mod的工程已经不能使用以前普通的方法了,这种情况和不同工程下包的相互调用一样。
执行如下命令创建golang_modon_demo6
工程:
# pwd /opt/workspace # mkdir golang_modon_demo6 && cd golang_modon_demo6 # go mod init modon_demo6 # touch main.go # mkdir -p tt && cd tt # mkdir -p pkg # go mod init util # touch test.go pkg/xixi.go
1) tt/pkg/xixi.go内容如下
ps: 注意上面包名称
2) tt/go.mod内容
3) tt/test.go内容
ps: 注意上面的包名
4) go.mod文件内容
5) main.go文件内容
我们注意上面anotherpkg
别名,还记得上边说的吗,不管取啥都可以,但如果不取,则test.go里面必须写成package util
如果test.go里面写的是:
那这里就根本不需要有anotherpkg这个别名(main函数里面的调用就改成util.Test()),但是目前test.go里面写的是:
所以需要加上这个别名,因为它默认会去找util包,但该包并不存在。
最后,我们执行如下命令验证程序的运行情况:
# go run main.go this is test entry 嘻嘻 嘻嘻
3. go build是什么?
主要用于编译代码,输出可执行文件,比如将源码打包成可执行文件部署线上服务 //如果是普通包(非main包), 只做检查, 不产生可执行文件 //如果是main包,生成可执行文件, 默认生成的可执行文件名为项目名(go mod里面) //命令: go build main.go // -o 参数指定可执行文件名称 //交叉编译 在linux生成window需要的 exe文件 GOOS=windows GOARCH=amd64 go build -o demo.exe main.go 反之 GOOS=linux GOARCH=amd64 go build -o demo main.go
4. 其他
vscode中, 点击install all总是超时失败。原因是当你安装好go时,默认go的代理环境是这样的配置
# go env -w GOPROXY=https://goproxy.io,direct
上面的地址被墙了,需要改成如下方式, 直接在控制台执行命令后, 重新打开vscode即可:
# go env -w GOPROXY=https://proxy.golang.com.cn,direct
[参看]: