每天go一点点,只记录学习中, 自己用到和迷惑的东西, 记录精华部分, 不求大而全.
package
package是啥
package(俗称的"包"):一个包含了多个 .go 文件的目录。当前目录下(不包子目录下的文件)的所有文件的包名必须一致.
每一段 Go 程序都 必须 属于一个包。
正如 Getting started with Go 中所说的,一个标准的可执行的 Go 程序必须有
package main的声明。有func main函数的文件是入口文件。
默认的规范
package 和 package所在的目录名, 一般是一致的. 当然也可以不一致.
理解一下package的依赖导入:import后面跟的是什么
import "path/to/package" 的理解
这里需要理解, import后面的, 是package所在的目录的路径, 俗称"导入路径 import path". 跟要导入的package包名没有半毛钱关系.
还记得上一个内容说的"默认规范"么, package的包名 和 package所在的目录名一致, 这只是个巧合, 可以不一致.
举个例子:
import "fmt" // 这里的"fmt"是标准库package fmt所在的路径"$GOROOT/src/fmt/" 不是指package fmt本身包名(所谓的巧合,正好一致而已)
如何引入包?
首先要理解, import 需要依赖package的源码包.
那源码包都在哪里呢?
- 标准库包: 源码在
$GOROOT/src下 - 第三方依赖包: 源码在
$GOPATH/src下
其次, import会从哪里找呢?
- 从
$GOROOT/src下, 找源码包 - 从
$GOPATH/src下, 找源码包 - 如果没有使用
go module版本管理工具 (推荐使用), 根据相对路径, 找源码包. (如果使用了go module, 这里会有个坑, 往下看Module部分内容).
// 注意理解: import引入的都是目录名
import (
"fmt" // $GOROOT/src/fmt/
"my/testpackage" // $GOPATH/my/testpackage/
"./api" // ./api/
)
自定义import进来的package别名
NOTE: 上面说了, import后面加的是path/to/package, 但是别名呢? 别名绑定的就是包的真实名字.
可以这么理解一下, 通过别名, 就可以直接使用了包的函数了.
import myfmt fmt // myfmt 就是'$GOROOT/src/fmt/'下的package fmt的别名
func TestAlias(t *testing.T) {
myufmt.Println("hello world")
}
为啥要用别名呢?
当引用包的时候,Go 会根据包的声明来创建包的变量。如果同时引用多个重名的包(包的路径不同, 但是包名一样),就会导致冲突。
// 引用了一个重复的包名, 包名都叫rand, 重名了
import (
"crypto/rand"
"math/rand"
)
import (
_ "crypto/rand" // 没有用到crypto/rand/的rand包内的类型和数值,但是执行了crypto/rand/ rand包的init函数
mrand "math/rand" // 将math/rand/下的rand包名, 替换为mrand避免冲突
)
go程序的初始化执行顺序
go run *.go
├── 执行 Main 包
├── 初始化Mian包所有引用的包
| ├── 初始化引用包的引用包 (recursive definition, 最后导入的包会最先初始化)
| ├── 初始化全局变量
| └── 同一个package如果有多个文件,则以文件名的顺序调用各个文件的init 函数()
└── 初始化 Main 包
├── 初始化全局变量
└── 以文件名的顺序调用main包中的init 函数
└── main函数
"可导出"是啥意思
不管是变量/函数/方法, 只要名字的开头是大写字母, 就表示这个 变量/函数/方法是"可导出的", 就是包外也是可以被访问到的.
Go Module
先提上原汁原味的链接 :Using Go Modules
Module是啥
还记得一个package是.go文件的集合么? 一个Module就是package(go 包)的集合. 即: 一个Module--多个package; 一个package--多个.go文件.
在一个module的树状存储结构的根部, 必须有一个go.mod文件, 该文件定义了如下内容:
- 此module的
module path(也是根目录里的package的import path) - 该module的编译所需的依赖module. 每个依赖项的结构都是
依赖module的module path+版本semantic versioin.
Go 1.11中, 只要 ①该项目中(当前目录/任意上层目录)含有go.mod文件, ②该项目在$GOPATH/src范围外, 就可以使用modules.
Go 1.13中, 只要含有go.mod, 那么就可以使用modules了, 即使在$GOPATH/src范围内.
GO111MODULE是个啥
每个入门者都会对GO111MODULE这个词感到恐惧, 但是这个环境变量没有那么神秘, 其实就是用来设置是否使用go module版本管理工具的开关.
如果感兴趣, 可以去看下GO111MODULE的三种状态(开:on, 关:off, 自动判断:auto).
在这里我翻译一下这篇文章, 对理解GO111MODULE有帮助. why is GO111MODULE everywhere?
GOPATH 与 GOMODULE
聊GOMODULE之前, 首先聊一下GOPATH.
当GO在2009年发布的时候, 没有pacakge的包版本管理工具. 所以当你通过go get命令去获取由import path指定的包的时候, 会将该包master分支的最新版放到$GOPATH/src/下, 没有多个版本的概念. master分支表示该包的稳定版.
在Go 1.11中, 引入了Go Modules(以前成为vgo -- 有版本的Go)的概念. 有了Go Module之后, 我们就可以同时存储一个package的多个版本了(tagged versions). 并且我们通过go.mod文件去标记当前使用的版本.
此时, 就引入了GO111MODULE环境变量, 用于区分两种方式(有版本概念的Go Module/ 无版本概念的GOPATH).
Go 1.11 和 1.12 中的 GO111MODULE
GO111MODULE=on 会强制使用Go modules, 即使你的项目在GOPATH/下. 当然, 项目需要有go.mod文件.
GO111MODULE=off 强制去使用无版本的GOPATH方式获取包, 即使你在GOPATH/的范围外.
GO111MODULE=auto
- 当处于
GOPATH外的时候GO111MODULE=on. 当然, 项目需要有go.mod文件. - 当你处于
GOPATH内的时候, 即使存在go.mod文件,GO111MODULE=off.
所以, 当你处于GOPATH内部, 并且你想通过Go modules方式获取一个指定版本的包时, 你需要执行下面的命令:
GO111MODULE=on go get github.com/golang/mock/tree/master/mockgen@v1.3.1
Go 1.13中的 GO111MODULE
Go 1.13中, GO111MODULE=auto 时, 含义有变化:
- 当处于
GOPATH外, 或者存在go.mod文件的时候, 相当于GO111MODULE=on. 所以在Go 1.13中, 你可以把所有的项目都放到GOPATH/下. - 当处于
GOPATH内, 并且没有go.mod文件的时候, 相当于GO111MODULE=off.
回归我们的主题: 为什么GO111MODULE到处都是?
因为: GO111MODULE=on可以允许你去通过git tags获取一个指定的版本, 而如果GO111MODULE=off, 你就只能获取master分支的最新版本.
陷阱: go.mod会被悄悄的更新
通常, 我们是希望go get命令, 去获取我们当前包的依赖, 并把相应的依赖放到go.mod文件中.
而go install命令, 我们只希望安装包, 并不想对我们的go.mod文件产生影响.
但是事与愿违. go命令都会对go.mod进行更新. 这一点要注意.
所以有时我们会看到这样的go get语句:
cd && GO111MODULE=on go get golang.org/x/tools/gopls@latest
cd就是为了进入到一个不能含有go.mod文件的目录, 这样在使用go get安装包的时候, 就不会对当前的go.mod文件产生影响.
陷阱: -u 和 @version不应该联用
下面的语句是很危险的
GO111MODULE=on go get -u golang.org/x/tools/gopls@latest
也许你只是想把gopls更新(-u)到最新(@latest), 但是这条命令实际上会把所有的依赖都更新到最新的最小修正版本0.1.0 ==> 0.2.0!! 版本间很有可能不兼容.
常用的go mod命令
go mod init <module_name> // initialize new module in current directory(在当前目录初始化mod, 会增加一个go.mod文件)
go mod tidy // add missing and remove unused modules (拉取缺少的模块,移除不用的模块)
NOTE: go mod init 命令创建了一个Module(go.mod文件)后, 所有的go toolchain, 如go mod/get/install/build等, 都会去修改/维护这个文件.
go mod tidy 今天遇到了一个坑, 解决方案: 解决 go mod tidy 报错 unknow version
创建一个Module
我们在$GOPATH/src外, 创建一个空的目录mkdir -p /home/gopher/hello, 进入hello目录, 创建一个go源文件hello.go:
package hello // 包名是hello
func Hello() string {
return "Hello, world."
}
在同文件夹下创建一个测试文件hello_test.go :
package hello // 同文件夹, 包名要相同
import "testing"
func TestHello(t *testing.T) {
want := "Hello, world."
if got := Hello(); got != want {
t.Errorf("Hello() = %q, want %q", got, want)
}
}
到这一步, 当前的文件夹包含了一个package, 但是还不是一个module, 因为没有go.mod文件. 我们在当前文件夹(/home/gopher/hello/), 运行go test命令. 可以看到:
$ go test
PASS
ok _/home/gopher/hello 0.020s
最后一行统计录整个包hello的测试结果. 因为我们在$GOPATH之外, 又不处于任何module中, 所以go命令知道, 当前的目录(/home/gopher/hello/)没有import path. 所以就基于目录名构造了一个import path: _/home/gopher/hello.
现在, 让我们通过执行go mod init命令, 使得当前的目录(/home/gopher/hello/)成为一个module的根目录, 并且再次执行go test命令:
$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello # example.com/hello ==> "module path"
$ go test
PASS
ok example.com/hello 0.020s
恭喜, 到这里, 你就创建了一个module, 并且对其进行了测试.
go mod init 命令创建了一个 go.mod文件, 内容如下:
$ cat go.mod
module example.com/hello
go 1.12
go.mod文件只存在于module的根目录. 子目录中的packages的import path是由<module path>/<path to the subdirectory of package>构成.
如: 我们创建了一个子目录world, world目录下的package会被自动识别为module hello的一部分, 并且import path是example.com/hello/world.
向当前的module中添加依赖 : import
go module的目的是为了使用其他开发者的"轮子"(即向自己的module中添加依赖).
我们使用import <import path>, 去导入其他的package来用.
我们通过import rsc.io/quote 导入quote包, 并改造我们的hello.go:
package hello
import "rsc.io/quote"
func Hello() string {
return quote.Hello()
}
执行go test
$ go test
go: finding rsc.io/quote v1.5.2
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
PASS
ok example.com/hello 0.023s
go命令通过使用go.mod中列出的具体module的版本来解析导入哪个版本的package。当要导入的package, go.mod文件没有任何module可以提供时, go命令会自动查找包含该包的模块并将其添加到go.mod中(使用该module的@lastest版本)。
“Latest” is defined as the latest tagged stable (non-prerelease) version, or else the latest tagged prerelease version, or else the latest untagged version.
go test查到, module rsc.io/quote v1.5.2 可以提供我们要的import path为rsc.io/quote的包(quote). 所以下载了该module.
同时, go test命令也下载了两个module(rsc.io/sampler v1.3.0, golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c). 这两个module提供了package quote的依赖packages rsc.io/sampler 和 golang.org/x/text.
虽然go test下载了3个module, 但是只有直接依赖的module才会被写入go.mod文件.
$ cat go.mod
module example.com/hello
go 1.12
require rsc.io/quote v1.5.2 # 直接依赖被写入
此时, 如果再次执行go test, 就不会重复这部分下载工作了, 因为go.mod文件已经是最新的了, 并且需要的module已经被缓存到了$GOPATH/pkg/mod/下.
$ go test
PASS
ok example.com/hello 0.020s
就像我们上面见到的, 添加一个直接依赖, 通常会带来很多间接依赖. 通过命令go list -m all查看当前module和所有它需要的依赖.
$ go list -m all
example.com/hello # 当前的module
# 所有依赖项
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
golang.org/x/text 的版本 v0.0.0-20170915032832-14c0d48ead0c 是一个伪版本, 是go命令的版本语法, 用来指示一个没有打上tag的版本.
除了go.mod文件, go命令还维护着一个go.sum文件, 该文件中存放的是指定版本module的加密哈希值.
$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZO...
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:Nq...
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3...
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPX...
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/Q...
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9...
go命令使用go.sum文件来保证将来无论下载多少次这些modules, 都和第一次下载的相同.
更新依赖项
使用GO Modules后, moduel的版本通过semantic version tags去引用. 一个版本包括三个部分: major 主版本, minor次要版本, patch修订版本.
举例:v0.1.2, 主版本是0, 次版本是1, 修订版本是2. 我们先看一些次要版本的升级.
go list -m all, 可以看到我们正在使用没有版本标签的module golang.org/x/text. 现在我们通过go get golang.org/x/text把它升级到最新的@latest版本. 然后执行go test看下是否还可以正常工作.
$ go get golang.org/x/text
go: finding golang.org/x/text v0.3.0
go: downloading golang.org/x/text v0.3.0
go: extracting golang.org/x/text v0.3.0
$ go test
PASS
ok example.com/hello 0.013s
一切正常, 我们使用 go list -m all 看下当前的依赖, 和 go.mod 的文件变化:
$ go list -m all
example.com/hello
golang.org/x/text v0.3.0 # 更新了版本
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$ cat go.mod
module example.com/hello
go 1.12
require (
golang.org/x/text v0.3.0 // indirect
rsc.io/quote v1.5.2
)
golang.org/x/text package 已经被升级到了@latest版本(v0.3.0). 同时, go.mod文件中也记录了该module的指定版本号v0.3.0.
indirect注释字段表明这个module不是当前mudule的直接依赖. go help modules可以看到更多的信息.
现在我们尝试升级rsc.io/sampler的次要版本. 同样, 我们使用go get进行升级, 并执行go test进行测试.
$ go get rsc.io/sampler
go: finding rsc.io/sampler v1.99.99
go: downloading rsc.io/sampler v1.99.99
go: extracting rsc.io/sampler v1.99.99
$ go test
--- FAIL: TestHello (0.00s)
hello_test.go:8: Hello() = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world."
FAIL
exit status 1
FAIL example.com/hello 0.014s
显然, v1.99.99和当前的版本不兼容. 我们查看下rsc.io/sampler都有哪些版本:
$ go list -m -versions rsc.io/sampler
rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99
我们之前使用的是v1.3.0, 明显v1.99.9不兼容, 我们尝试升级到v1.3.1:
$ go get rsc.io/sampler@v1.3.1 #指定升级的版本号
go: finding rsc.io/sampler v1.3.1
go: downloading rsc.io/sampler v1.3.1
go: extracting rsc.io/sampler v1.3.1
$ go test
PASS
ok example.com/hello 0.022s
注意go get的参数@v1.3.1表明了指定的版本, 默认的是@latest(最新的tagged版本).
添加一个指定主版本的依赖项
现在在我们的hello package中, 添加一个新的函数: Proverb, 该函数通过调用quote package的Concurrency方法返回一个字符串(谚语).
该quote package的import path是rsc.io/quote/v3.
首先, 我们改造hello.go文件:
package hello
import (
"rsc.io/quote"
quoteV3 "rsc.io/quote/v3" // 引用v3版本的quote, 并给该包起一个别名, 防止和上面的包名冲突, 因为这两个package包名都是quote
)
func Hello() string {
return quote.Hello() // rsc.io/quote 包引用
}
func Proverb() string {
return quoteV3.Concurrency() // rsc.io/quote/v3包引用
}
我们在hello_test.go文件中, 增加一个测试函数:
func TestProverb(t *testing.T) {
want := "Concurrency is not parallelism."
if got := Proverb(); got != want {
t.Errorf("Proverb() = %q, want %q", got, want)
}
}
测试一下:
$ go test
go: finding rsc.io/quote/v3 v3.1.0
go: downloading rsc.io/quote/v3 v3.1.0
go: extracting rsc.io/quote/v3 v3.1.0
PASS
ok example.com/hello 0.024s
可以看到, 我们当前的module依赖了两个modulersc.io/quote和rsc.io/quote/v3:
$ go list -m rsc.io/q...
# 这里是两个不同的module(虽然在一个仓库里, 虽然package的名字都是quote)
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
Go Module的每个不同的主版本(v1, v2, ...) 都要使用一个不同的module path(通过go.mod文件说明). 并且module path必须以主版本号作为结束(rsc.io/quote/v3). 这个约定被称作"语义导入版本控制", 这样就可以把不兼容的包(主版本不同的包)区分开.
在例子中, import path为rsc.io/quote/v3的包, 已经不再属于module path为rsc.io/quote的模块. 而是属于module path为rsc.io/quote/v3的模块.
同时要注意: 不同于主版本, 次要版本之间需要向后兼容(v1.6.0要兼容v1.5.2), 所以这些版本的包要同属于一个module.
在通过import引用依赖包的时候, go命令针对每个不同module path的module在本地只会有一个版本被编译. 也就说针对一个仓库, 不同的主版本(不同的module path的module), 只能有一个被引用. 如: rsc.io/quote, rsc.io/quote/V2, rsc.io/quote/V3.
不同的次要版本属于同一个module, 所以说你是无法同时使用rsc.io/quote v1.5.2和 rsc.io/quote v1.6.0这两个版本的.
不同的主版本属于不同的module, 所以你可以同时使用. 这样方便你去逐步的升级(比如一开始使用依赖的V2, 现在要使用依赖的V3, 你不用一次性修改所有的V2代码, 可以先保留, 后续再改).
删除不需要的依赖项
go build/ go test等命令, 很容易就能发现那些依赖项(module)需要被添加, 但是无法知道某个依赖不在被需要了(该module没有任何package被使用).
此时go mod tidy命令就可以上场了, 它会清理掉这些没有被使用的依赖项.
总结
这一部分的总结如下:
go mod init创建了一个新的module, 并在根目录创建一个go.mod文件去描述该module(如module path,依赖项(指定版本module)).go build,go test, 以及其他的编译命令会下载需要的依赖, 并把依赖项添加到go.mod文件中.go list -m all打印当前module, 和所有的依赖项(直接/间接依赖)go get获取/更新一个依赖项.go mod tidy清理无用的依赖项.
Module 与 package
新手总会被这两个概念的关系弄晕, 比如import一个package的时候, 到底这个package在哪里?
Module如何引入package?
- 引入外部依赖包(标准库包/第三方包) : 会从
$GOROOT/src,$GOPATH/src下去找. 这里和不使用go module是一致的. - 引入内部的package: 会从
moduleName + path/to/pacakge去找 见下面的踩坑. 这里和不使用go module有很大不同, 请注意
踩坑 : Module中如何引用(import)内部的package
执行 go mod init hello, 创建一个Module
项目文件树如下:
.
├── api
│ └── api.go
├── go.mod
└── main.go
// api.go代码
package api
import (
"fmt"
)
func HelloWorld() {
fmt.Println("hello world")
}
// main.go代码
package main
import (
api "./api" // 坑: 这里使用的是相对路径作为api包的import path, 会出错
)
func main() {
api.HelloWorld()
}
执行: go run main.go 会报错.
错误如下: cannot find module for path _/C_/Users/Thinkpad/Documents/go_test/api
为啥找不到package api呢? 因为我们使用的是moduel, 就不能使用之前的$GOPATH/src作为导入路径前缀了, 我们来看下官网的解释:
官网的解释: The go.mod file only appears in the root of the module. Packages in subdirectories have import paths consisting of the module path plus the path to the subdirectory.
在本例中, module path是hello, 所以 main.go应该这样引入内部的package
// main.go代码
package main
import (
api "hello/api" // "moudle path + /api"
)
func main() {
api.HelloWorld()
}
参考链接