Golang依赖管理工具: go module

大多数语言都会有包管理工具,像Node有npm,PHP有composer,Java有MavenGradle

可是,Go语言一直缺乏一个官方的包管理(之前有个Dep被称为官方试验品official experiment)。

终于,在2018年发布的go1.11 版本中,新增了go module管理模块功能,用来管理依赖包。

要知道,在这个之前,想要对go语言包进行管理,只能依赖第三方库实现,比如Vendor,GoVendor,GoDep,Dep,Glide等等,对于初学者来说,真的是选择困难症。

关于Workspaces和GOPATH

go1.11 之前,如果不使用第三方包管理工具可行,就是直接使用go get安装第三方包。

工作空间Workspaces,是Go项目的根目录,也就是 GOPATH是GO项目必备的环境变量,用来存放Go的开发代码和第三方包代码,代码需要按照一定的目录安排。

指定GOPATH路径:

$ echo $HOME
/Users/wangtom

$ export GOPATH=$HOME/go

查看GOPATH:

$ go env |grep GOPATH                    
GOPATH="/Users/wangtom/go"

开启 GO111MODULE:

要使用 go module, 需要先设置 GO111MODULE 环境变量,即设置GO111MODULE=on。如果没设置,执行命令的时候会有提示。

# 使用 go 命令设置
$ go env -w GO111MODULE=on

# linux / mac
$ export GO111MODULE=on

$ go env |grep GO111MODULE
GO111MODULE="on"

从 Go1.13 版本开始,go module 成为了Go语言默认的依赖管理工具,不需要再手动设置 GO111MODULE=on 了。

这是因为,默认设置的GO111MODULE=auto, 导致 modules 默认在 GOPATH/src 路径下是不启用的。 如果需要在 GOPATH/src 也能使用modules, 需要把 GO111MODULE 环境变量设置为 on.

$ export GO111MODULE=on  
$ go mod init github.com/cnwyt/mylib   
go: creating new go.mod: module github.com/cnwyt/mylib

创建一个公用模块 mylib 模块

创建一个公共模块mylib:

模块命名为 github.com/cnwyt/mylib,就是我们创建的GitHub仓库的路径,方便我们以后提交代码、供别人的调用。

(1)按照原来GOPATH开发模式,创建新的包需要放置在 $GOPATH/src/github.com

创建一个目录mylib目录,并进入该命令:

$ mkdir -p $GOPATH/src/github.com/cnwyt/mylib
$ cd $GOPATH/src/github.com/cnwyt/mylib

(2)使用 go mod init 命令初始化:

随便找一个目录,比如在 ~/dev/go-demo:

$ mkdir mylib
$ cd mylib
$ go mod init github.com/cnwyt/mylib
go: creating new go.mod: module github.com/cnwyt/mylib

$ ls 
go.mod

$ cat go.mod
module github.com/cnwyt/mylib
go 1.19

初始化后,会生成一个go.mod文件,类似 npm 里的 package.json 或者 composer 的 composer.json 的一个文件。

创建一个名为util的公共包,公共包我们一般放置放在项目的 pkg 目录下:

# mylib/pkg/util/hello.go
package util

import "fmt"

func SayHello() {
    fmt.Println("nihao -- from mylib")
}

在main函数中调用,

package main

import (
    "fmt"

    // <----- 这里引入本项目的 utl 包
    // 路径与使用 go mod init初始化时的 module 名称一致
    "github.com/cnwyt/mylib/pkg/util"
)

func main() {
    fmt.Println("Hello, mylib!")

    // <----- 这里调用本项目的 utl 包的方法
    util.SayHello()
}

常用的go mod命令如下表所示:

命令 作用
go mod init 初始化当前文件夹,创建 go.mod 文件
go mod edit 编辑 go.mod 文件
go mod tidy 增加缺少的包,删除无用的包
go mod download 下载依赖包到本地(默认在 GOPATH/pkg/mod 目录)
go mod graph 打印模块依赖图
go mod vendor 将依赖复制到 vendor 目录下
go mod verify 校验依赖
go list -m all 查看所有的依赖

创建一个本地模块 appdemo , 并调用 mylib:

在 GOPATH 以外的目录里,创建一个 appdemo 的应用目录,作为自己的项目目录。

因为作为应用目录不需要别人调用,不需要放到网上,因此不加 github.com 域名,直接起个 mod 名字即可,这里起的名字是 appdemo,和该项目的目录同名。

$ mkdir appdemo && cd appdemo

$ go mod init appdemo
go: creating new go.mod: module appdemo

$ vi main.go

创建一个 main.go 文件:

package main

import "fmt"

func main() {
    fmt.Println("Hello, appdemo!");
}

在 appdemo 项目,我们也创建一个名为 util 的公共包(该包仅供该项目里共用),公用包我们一般放置放在项目的 pkg 目录下:

# mylib/pkg/util/hello.go
package util

import "fmt"

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

在 main.go 里调用:

package main

import (
    "fmt"

    // <----- 这里引入本项目的 utl 包
    // 路径与使用 go mod init初始化时的 module 名称一致
    "pkg/util"
)

func main() {
    fmt.Println("Hello, mylib!")

    // <----- 这里调用本项目的 utl 包的方法
    t := util.UnixTime()
    fmt.Println("timestamp: ", t)
}

调用前边的公用模块 mylib:

初始化该模块,引入github.com/cnwyt/mylib模块,指定版本为latest:

$ go mod init appdemo
$ go mod edit -require github.com/cnwyt/mylib@latest

查看 go.mod 文件。

$ cat go.mod
module appdemo

go 1.19

require github.com/cnwyt/mylib v0.0.0

这样直接执行 go build 会报错:

$ go build
# <---- 可能是这个报错:
build appdemo: cannot find module for path github.com/cnwyt/mylib

# <---- 可能是这个,找不到这个mylib,报错:
go: errors parsing go.mod:
Confirm the import path was entered correctly.

这是为啥呢?

这是因为我们虽然创建了一个名为 github.com/cnwyt/mylib 公用模块,在GOPATH路径里没有这个模块。go mod 去 Github 去找这个模块找不到(还没推送到远程)。

那该怎么办呢? 有两个解决办法: 第一个办法,很简单,就是直接将cnwyt/mylib模块推送的 GitHub上。但是,如果我要修改cnwyt/mylib里的代码,每次都得先推送到GitHub上,才能生效,实在太麻烦了。

那就直接使用第二个办法, 使用 go replace 临时替代:

可以使用 go mod edit 命令修改。或直接修改 go.mod 文件,新增一行 replace:

module helloworld

require github.com/cnwyt/mylib v0.0.0

replace github.com/cnwyt/mylib => /Users/wangtom/dev/go-example/go-mod-demo/mylib

注意版本号必须填写,格式为 v1.2.3, 可以填v0.0.0.

package main

import (
    "appdemo/pkg/util"
    "fmt"

    // <----- 这里引入 公共模块 mylib 的 util 包
    // <----- 因为与项目里的util包重名了,所以重命名为 util2
    util2 "github.com/cnwyt/mylib/pkg/util"
)

func main() {
    fmt.Println("hello app demo")
    timestamp := util.UnixTime()

    fmt.Println("timestamp: ", timestamp)

    // <----- 这里调用 公共模块 mylib
    util2.SayHello()
}

然后重新运行,可以看到可以正常调用 mylib 里的方法:

$ go run main.go
hello app demo
timestamp:  1666147320
nihao -- from mylib

调用已存在的第三方模块

这里演示调用第三方模块 Redis 操作库 go-redis/redis。

使用 go get 下载安装 go-redis/redis:

$ go get github.com/go-redis/redis/v8

go: added github.com/cespare/xxhash/v2 v2.1.2
go: added github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
go: added github.com/go-redis/redis/v8 v8.11.5

运行 go buildgo test 会自动从GitHub下载模块,并会修改 go.mod 文件。

如果下载不下来代码,可以设置goproxy模块代理(七牛云提供)。

# Go 1.13 及以上(推荐)
# 打开你的终端并执行

$ go env -w GO111MODULE=on
$ go env -w GOPROXY=https://goproxy.cn,direct

查看 go.mod 文件变化:

module appdemo

go 1.19

require github.com/cnwyt/mylib v0.0.0

require (
    github.com/cespare/xxhash/v2 v2.1.2 // indirect
    github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
    github.com/go-redis/redis/v8 v8.11.5 // indirect
)

replace github.com/cnwyt/mylib => /Users/wangtom/dev/go-example/go-mod-demo/mylib

使用 go mod 下载安装的模块,代码会放到 $GOPATH/pkg/mod/ 目录下:


$ ls $GOPATH/pkg/mod/github.com/go-redis
redis                      
redis@v6.15.9+incompatible
import (
    "context"
    "github.com/go-redis/redis/v8"
    "fmt"
)

var ctx = context.Background()

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // no password set
        DB:       0,  // use default DB
    })

    err := rdb.Set(ctx, "key", "value", 0).Err()
    if err != nil {
        panic(err)
    }

    val, err := rdb.Get(ctx, "key").Result()
    if err != nil {
        panic(err)
    }
    fmt.Println("key", val)

    val2, err := rdb.Get(ctx, "key2").Result()
    if err == redis.Nil {
        fmt.Println("key2 does not exist")
    } else if err != nil {
        panic(err)
    } else {
        fmt.Println("key2", val2)
    }
    // Output: key value
    // key2 does not exist
}

END