本文详细介绍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内容如下:

package util

import(
        "fmt"
)

//注意首字母大写, 不然无法调用,大写表示允许被调用
func Test() {
        fmt.Println("I'm pkg/util/Test()")
} 

现在我们需要在main.go中调用这个Test()函数,main.go内容如下:

package main 

import(
        "golang_modoff_demo1/pkg/util"
)

func main(){
        util.Test()
}

执行如下命令运行:

# 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内容如下:

package util

import(
        "fmt"
)

//注意首字母大写, 不然无法调用,大写表示允许被调用
func Test() {
        fmt.Println("I'm pkg/util/Test()")
} 

main.go内容如下:

package main 

import(
        "golang_modauto_demo1/pkg/util"
)

func main(){
        util.Test()
}

这种情况下我们如何才能在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”:

package main 

import(
        "modauto/pkg/util"
)

func main(){
        util.Test()
}

代码中的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内容如下:

package util 


import(
        "fmt"
)

func SayHello(){
        fmt.Printf("hello, world\n")
}

回到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包:

package main 


import(
        "github.com/ivanzz1001/golang_modon_demo1/pkg/util"
)

func main(){
        util.SayHello()
}

显然github.com/ivanzz1001/golang_modon_demo1并不是github官网上的,而是我们本地的,所以我们需要修改golang_modon_demo2/go.mod文件:

module modon_demo2

go 1.18


require(
 github.com/ivanzz1001/golang_modon_demo1 v0.0.0
)

replace(
 github.com/ivanzz1001/golang_modon_demo1 => /opt/workspace/golang_modon_demo1
)

replace不仅可以这样做,比如你以前在github上引用的包,但时间长了可能作者改变了它的位置。举例:

replace github.com/gin-gonic/gin v1.0.1 => github.com/piannide/gin v1.0.2

当然上面版本号只是举例,不一定是这个版本。其实它的意思就是, 去把新位置(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内容如下:

package util

import(
        "time"
)

func UnixTime() int64{
        return time.Now().Unix()
}

此时,这个包名还是util,前面我们引用的golang_modon_demo1工程中的包也是util,那怎么区别呢?

去看看golang_modon_demo2工程下的go.mod内容,可以看到我们的项目名为modon_demo2,然后我们将golang_modon_demo2/main.go修改为如下:

package main 


import(
        "fmt"
        "modon_demo2/pkg/util"
        util2 "github.com/ivanzz1001/golang_modon_demo1/pkg/util"
)

func main(){
        now := util.UnixTime()
        fmt.Printf("now: %d\n", now)

        util2.SayHello()
}

执行如下命令运行:

# 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内容如下:

package main 

import(
        "net/http"
        "github.com/gin-gonic/gin"
)


func main(){
        r := gin.Default()
        r.GET("/ping", func(c *gin.Context) {
                c.JSON(http.StatusOK, gin.H{
                        "message": "pong",
                })
        })
        r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

把上面的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命令的。

此时,我们执行如下命令运行:

# go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080

另一个窗口向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文件内容

package calc

import(
        "modon_demo4/pkg/util"             //主要就是学习它怎么写
        "fmt"
)


func Add(x, y int) int {
        fmt.Println("我是calc, 我在这里调用了Say3()")
        util.Say3()

        return x + y
}

2) t1.go/t2.go/t3.go文件内容

  • t1.go文件
package util

import(
        "fmt"
)

func Say1(){
        fmt.Printf("I'm say1\n")
}
  • t2.go内容
package util

import(
        "fmt"
)

func Say2(){
        fmt.Printf("I'm say2\n")
}
  • t3.go文件
package util

import(
        "fmt"
)

func Say3(){
        fmt.Printf("I'm say3, I will use say1 and say2\n")

        Say1()
        Say2()
}

至此第一个目标实现。

由上可见,对于普通包,这里是util包,可以直接引用同包名下其他文件的函数,而main包有点区别main. go和 main_son.go都数据main包,我们去看一下他们的实现。

3) main_son.go文件内容

package main 

import(
        "fmt"
)

func test(){
        fmt.Printf("this is test in main package\n")
}

4) main.go文件内容

package main 

import(
        "fmt"
        "modon_demo4/pkg/util"
        "modon_demo4/calc"
)

func main(){
        fmt.Printf("this is main entry\n")

        fmt.Printf("===========================\n")
        util.Say1()

        fmt.Printf("===========================\n")
        util.Say2()

        fmt.Printf("===========================\n")
        util.Say3()

        fmt.Printf("==========================\n")
        sum := calc.Add(1, 2)
        fmt.Printf("sum(1,2) is %d\n", sum)

        fmt.Printf("===========================\n")
        //test()
}

可以看到我把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内容如下:

module modon_demo5

go 1.18

1) hello.go内容

package anotherpkg

import(
        "fmt"
)

func SayHello(){
        fmt.Printf("hello, world\n")
}

2) test.go内容

那么我们在test.go中如何调用SayHello()呢?参看如下test.go实现:

package main

import(
        "fmt"
        anotherpkg "modon_demo5/pkg"
)

func main(){
        fmt.Printf("this is the main entry\n")

        anotherpkg.SayHello()
}

执行如下命令运行:

# 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内容如下

package internalpkg

import(
        "fmt"
)

func SayXixi(){
        fmt.Printf("嘻嘻\n")
}

ps: 注意上面包名称

2) tt/go.mod内容

module util

go 1.18

3) tt/test.go内容

package anotherpkg


import(
        "fmt"
        internalpkg "util/pkg"
)

func Test(){
        fmt.Printf("this is test entry\n")

        internalpkg.SayXixi()
}

ps: 注意上面的包名

4) go.mod文件内容

module modon_demo6

go 1.18

require(
 "util" v0.0.1
)

replace(
 "util" => ./tt
)

5) main.go文件内容

package main

import(
        anotherpkg "util"
        internalpkg "util/pkg"
)

func main(){
        anotherpkg.Test()
        internalpkg.SayXixi()
}

我们注意上面anotherpkg别名,还记得上边说的吗,不管取啥都可以,但如果不取,则test.go里面必须写成package util 如果test.go里面写的是:

package util

那这里就根本不需要有anotherpkg这个别名(main函数里面的调用就改成util.Test()),但是目前test.go里面写的是:

package anotherpkg

所以需要加上这个别名,因为它默认会去找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



[参看]:

  1. go module官网

  2. go module官网(国内可访问)

  3. Using Go Modules

  4. 详解go语言包管理方式