Go 项目中代码组织的两种模式
阅读本文大概需要 15 分钟。
这是一篇基础文章,主要帮新手解决 GOPATH 和 Go Module 的问题。希望这篇文章能够为你彻底解惑。本文作者:Kade。
本文的行文风格跟普通的文章不一样,是一种沉浸式的、笔记式的、或者视频稿的风格。不知道你是否会喜欢。
注: 本文基于 go1.16, macOS 环境
01 相关概念梳理
注: 详细的可以完整阅读 Go 官方文档及 Wiki, 为了通俗一点, 文中某些描述可能不是很严谨!
首先需要清楚 Go 项目中包(package)
和模块(module)
的概念, 简单描述一下:
包(package)
是用来管理 .go 文件的, 相关概念:包目录
,包名
,包路径/包导入路径/导入路径
它是源代码的集合, 由一个或多个源文件组成: 一个目录最多只能有一个包, 一个包只能存在于一个目录
模块(module)
是用来管理包的, 相关概念:模块目录
,模块路径
它是包的集合, 由零个或多个包组成: 一个目录最多只能有一个模块, 一个模块只能存在于一个目录 ; 一个模块目录里必须要有
go.mod
文件
02 代码组织的两种模式
注: 文中描述
模式
时使用小写(强迫症); 相关的发展历史可在社区了解
GOPATH mode(gopath模式)
: 通过配置GO111MODULE=off
强制开启$GOPATH
默认为用户家目录下的go
目录, 即~/go
$GOPATH
可以设置多个目录, 可以实现依赖包存放在一个目录, 自己项目的包存放在另外一个目录包需要存放在 $GOPATH/src
下的子目录中, 包目录相对于$GOPATH/src
的相对路径则为包的导入路径
习惯上, 包所在的目录名与包名相同(不是必须) 使用 go get
下载的包也是存放在$GOPATH/src
目录中依赖包可以放在 vendor
目录中没有模块相关的概念 module mode(gomod模式)
: 通过配置GO111MODULE=on
强制开启$GOPATH
默认为用户家目录下的go
目录, 即~/go
模块目录可以是任何目录, 包必须在某个模块中 模块路径需要在模块目录下的 go.mod 文件中使用 module
指令指定习惯上, 模块下的包所在的目录名与包名相同(不是必须) 使用 go get
下载的包存放在$GOPATH/pkg/mod
下的相关目录中通过 go 命令的参数 -mod=vendor
可以支持 main 包下的vendor
目录有模块相关的概念及配置, 比如: GOPROXY
,GOPRIVATE
,GOSUMDB
等
注: GO111MODULE
配置还有一个值是auto
, 意思是具体 go 使用哪一种模式由 go 来判断并决定, 不同版本的判断不同, 效果不同, 所有建议使用 go 之前先明确设置GO111MODULE
的值为 off 或者 on
注: gomod 模式中只保留了部分的 vendor 特性支持, 不建议日常开发中使用, 一般用作依赖存档或 CI/CD 使用
注: gopath 模式基本废弃, 不建议再使用, 如果有老项目仍在使用, 建议着手迁移到 gomod 模式, 如果迁移有问题, 可以在社区交流讨论, 或向官方求助
03 两种模式的使用示例
gopath 模式(官方已经准备废弃,不建议使用)
开启 gopath模式
, 设置GO111MODULE
值为off
MacBook$ # 1. 设置
MacBook$ export GO111MODULE=off
MacBook$ # 需要永久配置的话, 需要修改相关的配置文件
MacBook$ # 比如: ~/.bash_profile 或 ~/.bashrc 等
MacBook$ #
MacBook$ # 建议使用下面的方法:
MacBook$ go env -w GO111MODULE=off
MacBook$ # 2. 验证
MacBook$ go env GO111MODULE
off
MacBook$
根据需要设置 GOPATH
, 默认值为~/go
, 建议使用默认(这里为了演示设置了其他目录)
MacBook$ # 1. 设置
MacBook$ export GOPATH=/Users/kadefor/examples/gopath_mode
MacBook$ # 同上建议:
MacBook$ go env -w GOPATH=/Users/kadefor/examples/gopath_mode
MacBook$ # 2. 验证
MacBook$ go env GOPATH
/Users/kadefor/examples/gopath_mode
MacBook$
日常开发(使用 labstack/echo
这个 web 开发框架为例)
MacBook$ go env GO111MODULE
off
MacBook$ go env GOPATH
/Users/kadefor/examples/gopath_mode
MacBook$ pwd
/Users/kadefor/examples/gopath_mode/src
MacBook$ tree .
.
└── github.com
└── myrepo
└── helloworld
└── main.go
3 directories, 1 file
MacBook$ cd github.com/myrepo/helloworld/
MacBook$
MacBook$ # 项目代码放在`GOPATH/src`下, 一般是放在某个子目录里
MacBook$ # 相对于`GOPATH/src`的相对目录路径即为包导入路径
MacBook$ # 比如说, 有一个包cc在src目录下的`aa/bb/cc`目录里
MacBook$ # 那它的导入路径就是"aa/bb/cc"
MacBook$
MacBook$ # 这里github.com/myrepo/helloworld目录下有个main包:
MacBook$ pwd
/Users/kadefor/examples/gopath_mode/src/github.com/myrepo/helloworld
MacBook$ ls
main.go
MacBook$ head -8 main.go
package main
import (
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"net/http"
)
MacBook$ # gopath模式下, go找包会在`GOROOT`, `GOPATH/src`, vendor目录下去找
MacBook$ # 比如这里导入的"github.com/labstack/echo"
MacBook$ # 运行看看:
MacBook$ go run .
main.go:4:2: cannot find package "github.com/labstack/echo" in any of:
/Users/kadefor/sdk/go/src/github.com/labstack/echo (from $GOROOT)
/Users/kadefor/examples/gopath_mode/src/github.com/labstack/echo (from $GOPATH)
main.go:5:2: cannot find package "github.com/labstack/echo/middleware" in any of:
/Users/kadefor/sdk/go/src/github.com/labstack/echo/middleware (from $GOROOT)
/Users/kadefor/examples/gopath_mode/src/github.com/labstack/echo/middleware (from $GOPATH)
MacBook$ # 现在就需要下载依赖的包
MacBook$ # 方法一般有:
MacBook$ # 1. go get github.com/labstack/echo 它还会同时下载相应的依赖包, 简单
MacBook$ # 但是, 某些包可能因为网络原因访问不了 - -! 可以挂代理
MacBook$ # 2. 想办法把包下载回来解压到`GOPATH/src`目录里, 并保留包目录的结构
MacBook$ # 比如git clone或者去github上点鼠标下载并解压到`GOPATH/src`目录里
MacBook$ # 3. 使用第三方的包管理工具, 比如dep, govendor等
MacBook$ # 第三方包管理工具一般是使用vendor特性, 并支持维护包的版本
MacBook$ # 这样的工具在gopath模式里使用比较多, 因为, go get的方法不支持包版本!
MacBook$ # 现在我挂代理使用go get
MacBook$ export https_proxy=http://127.0.0.1:7890 http_proxy=http://127.0.0.1:7890 all_proxy=socks5://127.0.0.1:7890
MacBook$ tree `go env GOPATH`/src -L 3
/Users/kadefor/examples/gopath_mode/src
└── github.com
└── myrepo
└── helloworld
3 directories, 0 files
MacBook$ go get -v -u -d github.com/labstack/echo
github.com/labstack/echo (download)
MacBook$ tree `go env GOPATH`/src -L 3
/Users/kadefor/examples/gopath_mode/src
├── github.com
│ ├── labstack
│ │ ├── echo
│ │ └── gommon
│ ├── mattn
│ │ ├── go-colorable
│ │ └── go-isatty
│ ├── myrepo
│ │ └── helloworld
│ └── valyala
│ └── fasttemplate
└── golang.org
└── x
├── crypto
├── net
├── sys
└── text
17 directories, 0 files
MacBook$ # 其他多出来的就是labstack/echo的依赖包
MacBook$ # 现在运行:
MacBook$ go run .
../../labstack/echo/middleware/jwt.go:9:2: cannot find package "github.com/dgrijalva/jwt-go" in any of:
/Users/kadefor/sdk/go/src/github.com/dgrijalva/jwt-go (from $GOROOT)
/Users/kadefor/examples/gopath_mode/src/github.com/dgrijalva/jwt-go (from $GOPATH)
../../labstack/echo/middleware/rate_limiter.go:9:2: cannot find package "golang.org/x/time/rate" in any of:
/Users/kadefor/sdk/go/src/golang.org/x/time/rate (from $GOROOT)
/Users/kadefor/examples/gopath_mode/src/golang.org/x/time/rate (from $GOPATH)
MacBook$ # 晕! 还有一个包没下载!
MacBook$
MacBook$ grep 'echo/middleware' main.go
"github.com/labstack/echo/middleware"
MacBook$ go get -v -d github.com/labstack/echo/middleware
github.com/dgrijalva/jwt-go (download)
get "golang.org/x/time/rate": found meta tag vcs.metaImport{Prefix:"golang.org/x/time", VCS:"git", RepoRoot:"https://go.googlesource.com/time"} at //golang.org/x/time/rate?go-get=1
get "golang.org/x/time/rate": verifying non-authoritative meta tag
golang.org/x/time (download)
MacBook$ # 再来:
MacBook$ go run .
⇨ http server started on [::]:1323
^Csignal: interrupt
MacBook$ # 项目已经运行起来了, 下面再演示一下自己项目下的其他包的使用
MacBook$ pwd
/Users/kadefor/examples/gopath_mode/src/github.com/myrepo/helloworld
MacBook$ ls
main.go
MacBook$ mkdir abc
MacBook$ pwd
/Users/kadefor/examples/gopath_mode/src/github.com/myrepo/helloworld
MacBook$ cd abc
MacBook$ pwd
/Users/kadefor/examples/gopath_mode/src/github.com/myrepo/helloworld/abc
MacBook$ # 怎么用这个包呢? 导入路径是什么?
MacBook$ # 相对于src的相对路径就是abc这个包的导入路径:
MacBook$ # github.com/myrepo/helloworld/abc
MacBook$ cat abc.go
package abc
import "fmt"
func Print() {
fmt.Println("GOPATH mode: Hello, ABC")
}
MacBook$ cd ..
MacBook$ cat main.go
package main
import (
"github.com/myrepo/helloworld/abc"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"net/http"
)
func main() {
abc.Print()
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/", hello)
e.Logger.Fatal(e.Start(":1323"))
}
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
MacBook$ cd ..
MacBook$ go run main.go
GOPATH mode: Hello, ABC
⇨ http server started on [::]:1323
^Csignal: interrupt
MacBook$ # 接下来演示一下vendor
MacBook$ pwd
/Users/kadefor/examples/gopath_mode/src/github.com/myrepo/helloworld
MacBook$ mkdir vendor
MacBook$ mkdir -p vendor/github.com/myrepo/helloworld/
MacBook$ cp -a abc vendor/github.com/myrepo/helloworld/
MacBook$ # 改一下vendor下abc那个包, 看看效果
MacBook$ vim vendor/github.com/myrepo/helloworld/abc/abc.go
MacBook$ tree .
.
├── abc
│ └── abc.go
├── main.go
└── vendor
└── github.com
└── myrepo
└── helloworld
└── abc
└── abc.go
6 directories, 3 files
MacBook$ cat abc/abc.go
package abc
import "fmt"
func Print() {
fmt.Println("GOPATH mode: Hello, ABC")
}
MacBook$ cat vendor/github.com/myrepo/helloworld/abc/abc.go
package abc
import "fmt"
func Print() {
fmt.Println("GOPATH mode vendor: Hello, ABC")
}
MacBook$ # 运行后输出什么呢?
MacBook$ go run .
GOPATH mode vendor: Hello, ABC
⇨ http server started on [::]:1323
^Csignal: interrupt
MacBook$ # 我们也可以把所有的依赖包都放到vendor目录下
MacBook$ # 这样的作用是:
MacBook$ # 1. 可以把依赖存档, 就算源仓库删除了, 我们的项目同样可以运行
MacBook$ # 2. 保存我们自己修改后的第三方包
MacBook$ #
MacBook$ # 但是手动去做太麻烦了, 所以在gopath模式中一般会使用第三方的包管理工具
MacBook$ # 使用主流的第三方包管理工具还有一个好处是: 迁移到gomod模式比较简单!
MacBook$
gomod 模式(官方建议使用)
开启 gomod模式
, 设置GO111MODULE
值为on
MacBook$ # 1. 设置
MacBook$ export GO111MODULE=on
MacBook$ # 需要永久配置的话, 需要修改相关的配置文件
MacBook$ # 比如: ~/.bash_profile 或 ~/.bashrc 等
MacBook$ #
MacBook$ # 建议使用下面的方法:
MacBook$ go env -w GO111MODULE=on
MacBook$
MacBook$ # 2. 验证
MacBook$ go env GO111MODULE
on
MacBook$
根据需要设置 GOPATH
, 默认值为~/go
, 建议使用默认(这里为了演示设置了其他目录)
MacBook$ # 1. 设置
MacBook$ export GOPATH=/Users/kadefor/examples/gomod_mode
MacBook$ # 同上建议:
MacBook$ go env -w GOPATH=/Users/kadefor/examples/gomod_mode
MacBook$
MacBook$ # 2. 验证
MacBook$ go env GOPATH
/Users/kadefor/examples/gomod_mode
MacBook$
设置 GOPROXY
MacBook$ # 1. 设置
MacBook$ export GOPROXY=https://goproxy.cn,direct
MacBook$ # 同上建议:
MacBook$ go env -w GOPROXY=https://goproxy.cn,direct
MacBook$
MacBook$ # 有官方的proxy, 但是网络原因访问不了
MacBook$ # 使用proxy的好处:
MacBook$ # 1. 一般公共的proxy都会上CDN, 模块下载速度快
MacBook$ # 2. proxy相当于一个中心化的模块版本镜像, 只要proxy上的缓存不删除
MacBook$ # 就算源仓库删除了, 项目还是可以构建
MacBook$ #
MacBook$ # 公司内部如果私有模块比较多, 比较复杂, 可以自建proxy
MacBook$ # 由自建的proxy去控制哪些模块是私有的, 哪些是公有的
MacBook$ # 这样对于公司内部的开发来说是透明的, 不需要再关注私有模块
MacBook$ #
日常开发(使用 labstack/echo
这个 web 开发框架为例)
MacBook$ pwd
/tmp/helloworld
MacBook$ # 使用gomod模式后, 项目就可以随便放在某个目录了, 但是, 项目必须在某个模块内
MacBook$ # 如果是新建的项目, 需要自己创建项目所在模块
MacBook$ go mod init github.com/myrepo/helloworld
go: creating new go.mod: module github.com/myrepo/helloworld
go: to add module requirements and sums:
go mod tidy
MacBook$ cat go.mod
module github.com/myrepo/helloworld
go 1.16
MacBook$ # 这里指定的模块路径为 github.com/myrepo/helloworld
MacBook$ # 也可以指定其他的, 比如 abc, xxx/ooo 等等
MacBook$ # 也有限制, 有几个特殊的不行, 哪些? 自己找找 :-)
MacBook$
MacBook$ # 这个模块路径一般和源码仓库的路径一致
MacBook$ # 这个模块路径会做为模块目录下包的导入路径的前缀
MacBook$ # 比如, 如果当前模块下有个包abc, 则这个包的导入路径为:
MacBook$ # github.com/myrepo/helloworld/abc
MacBook$ vim main.go
MacBook$ cat main.go
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/", hello)
e.Logger.Fatal(e.Start(":1323"))
}
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
MacBook$ cat go.mod
module github.com/myrepo/helloworld
go 1.16
MacBook$ # 使用gomod模式的话, 代码写好了, 可以执行下面命令, 自动下载依赖:
MacBook$ # 不需要手动一个一个去下载, 直接执行:
MacBook$ go mod tidy
go: finding module for package github.com/labstack/echo/v4/middleware
go: finding module for package github.com/labstack/echo/v4
go: downloading github.com/labstack/echo/v4 v4.2.0
go: found github.com/labstack/echo/v4 in github.com/labstack/echo/v4 v4.2.0
go: found github.com/labstack/echo/v4/middleware in github.com/labstack/echo/v4 v4.2.0
go: downloading github.com/labstack/gommon v0.3.0
go: downloading golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
go: downloading golang.org/x/net v0.0.0-20200822124328-c89045814202
go: downloading github.com/stretchr/testify v1.4.0
go: downloading github.com/dgrijalva/jwt-go v3.2.0+incompatible
go: downloading golang.org/x/time v0.0.0-20201208040808-7e3f01d25324
go: downloading github.com/mattn/go-colorable v0.1.7
go: downloading github.com/mattn/go-isatty v0.0.12
go: downloading gopkg.in/yaml.v2 v2.2.2
go: downloading golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6
go: downloading golang.org/x/text v0.3.3
MacBook$ # 使用gomod模式的话, 你用的依赖可能有不同的主版本号, 如果是大于等于2, 则在导入路径后面得加上 /v2 /v3 /v4 等
MacBook$
MacBook$ # 我想用labstack/echo的v4版本, 则导入路径为: github.com/labstack/echo/v4
MacBook$ # 现在看看go.mod
MacBook$ cat go.mod
module github.com/myrepo/helloworld
go 1.16
require github.com/labstack/echo/v4 v4.2.0
MacBook$ # 运行
MacBook$ go run .
v4.2.0
High performance, minimalist Go web framework
⇨ http server started on [::]:1323
^Csignal: interrupt
MacBook$ # 使用gomod模式还是简单, 只要你的依赖不奇葩 :D
MacBook$
MacBook$ # 如果你使用支持自动补全的编辑器或者IDE, 但它不会自动下载依赖(一般都会), 如果模块没有提前下载, 则自动补全无法正常使用
MacBook$ # 或者你需要使用模块特定的版本
MacBook\$ # 那就需要手动下载依赖了:
MacBook$ go get -v -d github.com/labstack/echo/v3
go get: module github.com/labstack/echo@upgrade found (v3.3.10+incompatible), but does not contain package github.com/labstack/echo/v3
MacBook$ # 这里需要特别说明一下, 在 gomod 模式出现之前, 有些模块已经有 v2,v3 等版本号了, 但不是模块, 所有就会有类似上面的错误
MacBook$ # 既然不是模块就不存在/v2,/v3这样的尾巴了
MacBook$ go get -v -d github.com/labstack/echo@v3.3.10
go get: added github.com/labstack/echo v3.3.10+incompatible
MacBook$ vim main.go # 改一下包导入路径
MacBook$ cat main.go
package main
import (
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"net/http"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/", hello)
e.Logger.Fatal(e.Start(":1323"))
}
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
MacBook\$ cat go.mod
module github.com/myrepo/helloworld
go 1.16
require (
github.com/labstack/echo v3.3.10+incompatible // indirect
github.com/labstack/echo/v4 v4.2.0
)
MacBook$ go mod tidy
MacBook$ # 变化
MacBook\$ cat go.mod
module github.com/myrepo/helloworld
go 1.16
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/labstack/echo v3.3.10+incompatible
github.com/labstack/gommon v0.3.0 // indirect
github.com/mattn/go-colorable v0.1.7 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
golang.org/x/net v0.0.0-20200822124328-c89045814202 // indirect
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 // indirect
golang.org/x/text v0.3.3 // indirect
)
MacBook\$ go run .
v3.3.10-dev
High performance, minimalist Go web framework
⇨ http server started on [::]:1323
^Csignal: interrupt
04 总结
gomod 模式相对于 gopath 模式来说还是比较新, 所以 gomod 模式下还有很多操作在本文中就没有写了, 如果有人喜欢这种沉浸式的、笔记式的、或者视频稿的风格, 那后面就再写写 gopath 模式迁移到 gomod 模式的操作, 以及 gomod 模式下模块的常见管理操作, 如果不喜欢这种风格, 那就算了 :D
欢迎关注我