专业的编程技术博客社区

网站首页 > 博客文章 正文

Go 项目中代码组织的两种模式

baijin 2025-03-04 10:46:58 博客文章 6 ℃ 0 评论

这是一篇基础文章,主要帮新手解决 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 模式(官方已经准备废弃,不建议使用)

  1. 开启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$
  1. 根据需要设置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$
  1. 日常开发(使用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 模式(官方建议使用)

  1. 开启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$
  1. 根据需要设置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$
  1. 设置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$?#
  1. 日常开发(使用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



Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表