—Tony Bai · Go 语言第一课
在 Go 语言中,只有首字母为大写的标识符才是导出的(Exported),能力对包外的代码可见;
如果首字母是小写的,那么就阐明这个标识符仅限于在申明它的包内可见。
- Go 包是 Go 语言的根本组成单元。一个 Go 程序就是一组包的汇合,所有 Go 代码都位
- 于包中;
- Go 源码能够导入其余 Go 包,并应用其中的导出语法元素,包含类型、变量、函数、方
- 法等,而且,main 函数是整个 Go 利用的入口函数;
- Go 源码须要先编译,再散发和运行。如果是单 Go 源文件的状况,咱们能够间接应用
- go build 命令 +Go 源文件名的形式编译。不过,对于简单的 Go 我的项目,咱们须要在
- Go Module 的帮忙下实现我的项目的构建。
咱们应用 tree 命令来查看一下 Go 语言我的项目本身的最后源码构造布局,以 Go 1.3 版本为例,后果是这样的:
你会看到 src 下的二级目录 pkg 上面寄存着运行时实现、规范库包实现,这些包既能够被下面 cmd 下各程序所导入,也能够被 Go 语言我的项目之外的 Go 程序依赖并导入。
Go 1.4 版本删除 pkg 这一中间层目录并引入 internal 目录。
依据 internal 机制的定义,一个 Go 我的项目里的 internal 目录下的 Go 包,只能够被本我的项目外部的包导入。我的项目内部是无奈导入这个 internal 目录上面的包的。能够说,internal 目录的引入,让一个 Go 我的项目中 Go 包的分类与用处变得更加清晰。
Go1.6 版本减少 vendor 目录
减少了 vendor 构建机制,也就是 Go 源码的编译能够不在 GOPATH 环境变量上面搜寻依赖包的门路,而在 vendor 目录下查找对应的依赖包。
Go 1.13 版本引入 go.mod 和 go.sum
引入了 Go Module 构建机制,也就是在我的项目引入 go.mod 以及在 go.mod 中明确我的项目所依赖的第三方包和版本,我的项目的构建就将解脱 GOPATH 的解放,实现精准的可重现构建.
可执行程序我的项目是以构建可执行程序为目标的我的项目,Go 社区针对这类 Go 我的项目所造成的典
型构造布局是这样的:
cmd 目录。 cmd 目录就是寄存我的项目要编译构建的可执行文件对应的 main 包的源文件。如果你的我的项目中有多个可执行文件须要构建,每个可执行文件的 main 包独自放在一个子目录中,比方图中的 app1、app2,cmd 目录下的各 app 的 main 包将整个我的项目的依赖连贯在一起。
main 包应该很简洁。咱们在 main 包中会做一些命令行参数解析、资源初始化、日志设施初始化、数据库连贯初始化等工作,之后就会将程序的执行权限交给更高级的执行管制对象。
pkgN 目录,这是一个寄存我的项目本身要应用、同样也是可执行文件对应 main 包所要依赖的库文件,同时这些目录下的包还能够被内部我的项目援用。
如果 Go 可执行程序我的项目有一个且只有一个可执行程序要构建,那就比拟好办了,咱们可
以将下面我的项目布局进行简化:
Go 库我的项目仅对外裸露 Go 包,这类我的项目的典型布局模式是这样的:
库类型我的项目相比于 Go 可执行程序我的项目的布局要简略一些。因为这类我的项目不须要构建可执行程序,所以去除了 cmd 目录。
以生产可执行程序为目标的 Go 我的项目,它的典型我的项目构造分为五局部:
- 放在我的项目顶层的 Go Module 相干文件,包含 go.mod 和 go.sum;
- cmd 目录:寄存我的项目要编译构建的可执行文件所对应的 main 包的源码文件;
- 我的项目包目录:每个我的项目下的非 main 包都“平铺”在我的项目的根目录下, 每个目录对应一个 Go 包;
- internal 目录:寄存仅我的项目外部援用的 Go 包,这些包无奈被我的项目之外援用;
- vendor 目录:这是一个可选目录,为了兼容 Go 1.5 引入的 vendor 构建模式而存在
- 的。这个目录下的内容均由 Go 命令主动保护,不须要开发者手工干涉。
第二,对于以生产可复用库为目标的 Go 我的项目,它的典型构造则要简略许多,咱们能够直
接了解为在 Go 可执行程序我的项目的根底上去掉 cmd 目录和 vendor 目录。
深刻 Go Module 构建模式
Go Module 的语义导入版本机制:
语义版本号分成 3 局部:主版本号 (major)、次版本号 (minor)和补丁版本号 (patch)。例如下面的 logrus module 的版本号是 v1.8.1,这就示意它的主版本号为 1,次版本号为 8,补丁版本号为 1
Go 命令和 go.mod 文件都应用下面这种合乎语义版本标准的版本号,作为形容 Go Module 版本的规范模式。借助于语义版本标准,Go 命令能够确定同一 module 的两个版本公布的先后秩序,而且能够确定它们是否兼容.
依照语义版本标准,主版本号不同的两个版本是互相不兼容的。而且,在主版本号雷同的状况下,次版本号大都是向后兼容次版本号小的版本。补丁版本号也不影响兼容性.
如果一个我的项目依赖 logrus v2.0.0 版本,那么它的包导入门路就不能再与下面的导入形式雷同了。那咱们应该应用什么形式导入 logrus v2.0.0 版本呢?Go Module 创新性地给出了一个办法:将包主版本号引入到包导入门路中,咱们能够像上面这样导入 logrus v2.0.0 版本依赖包:
import (
"github.com/sirupsen/logrus"
logv2 "github.com/sirupsen/logrus/v2"
)
Go Module 的最小版本抉择准则
在这张图中,myproject 有两个间接依赖 A 和 B,A 和 B 有一个独特的依赖包 C,但 A 依
赖 C 的 v1.1.0 版本,而 B 依赖的是 C 的 v1.3.0 版本,并且此时 C 包的最新公布版为 C
v1.7.0。这个时候,Go 命令是如何为 myproject 选出间接依赖包 C 的版本 V1.3.0
降级 / 降级依赖的版本
基于初始状态执行的 go mod tidy 命令,帮咱们抉择了 logrus 的最新发
布版本 v1.8.1。如果你感觉这个版本存在某些问题,想将 logrus 版本降至某个之前公布的
兼容版本,比方 v1.7.0,那么咱们能够在我的项目的 module 根目录下,执行带有版本号的
go get 命令:
$ go get github.com/sirupsen/logrus@v1.7.0
go: downloading github.com/sirupsen/logrus v1.7.0
go get: downgraded github.com/sirupsen/logrus v1.8.1 => v1.7.0
如果咱们要为 Go 我的项目增加主版本号大于 1 的依赖,咱们就须要应用“语义导入版本”机制,在申明它的导入门路的根底上,加上版本号信息。咱们以“向 modulemode 我的项目增加 github.com/go-redis/redis 依赖包的 v7 版本”为例,看看增加步骤。
首先,咱们在源码中,以空导入的形式导入 v7 版本的 github.com/go-redis/redis 包:
接下来的步骤就与增加兼容依赖一样,咱们通过 go get 获取 redis 的 v7 版本:
$ go get github.com/go-redis/redis/v7
go: downloading github.com/go-redis/redis/v7 v7.4.1
go: downloading github.com/go-redis/redis v6.15.9+incompatible
go get: added github.com/go-redis/redis/v7 v7.4.1
移除一个依赖
go mod tidy 命令,将这个依赖项彻底从 GoModule 构建上下文中革除掉。go mod tidy 会主动剖析源码依赖,而且将不再应用的依赖从 go.mod 和 go.sum 中移除。
如果咱们要基于 vendor 构建,而不是基于本地缓存的 Go Module 构建,咱们须要在 go
build 前面加上 -mod=vendor 参数。
在 Go 1.14 及当前版本中,如果 Go 我的项目的顶层目录下存在 vendor 目录,那么 go build 默认也会优先基于 vendor 构建,除非你给 go build 传入 -mod=mod 的参数。
Go 包的初始化秩序
在初始化 Go 包时,Go 会依照肯定的秩序,逐个、程序地调用这个包的 init 函数。一般来说,先传递给 Go 编译器的源文件中的 init 函数,会先被执行;而同一个源文件中的多个 init 函数,会按申明程序顺次执行。
一个 Go 程序就是由一组包组成的,程序的初始化就是这些包的初始化。每个 Go 包还会有本人的依赖包、常量、变量、init 函数(其中 main 包有 main 函数)等。
- Go 包的初始化秩序并不难,你只须要记住这三点就能够了:
- 依赖包按 ” 深度优先 ” 的秩序进行初始化;
- 每个包内按以 常量 -> 变量 -> init 函数 的程序进行初始化;
- 包内的多个 init 函数按呈现秩序进行主动调用。
init 函数具备的几种行为特色:
- 执行顺位排在包内其余语法元素的前面;
- 每个 init 函数在整个 Go 程序生命周期内仅会被执行一次;
- init 函数是程序执行的,只有当一个 init 函数执行结束后, 才会去执行下一个 init 函数