Go 语言基础--入门篇

---Tony Bai · Go语言第一课
在 Go 语言中,只有首字母为大写的标识符才是导出的(Exported),才能对包外的代码可见;
如果首字母是小写的,那么就说明这个标识符仅限于在声明它的包内可见。

  1. Go 包是 Go 语言的基本组成单元。一个 Go 程序就是一组包的集合,所有 Go 代码都位
  2. 于包中;
  3. Go 源码可以导入其他 Go 包,并使用其中的导出语法元素,包括类型、变量、函数、方
  4. 法等,而且,main 函数是整个 Go 应用的入口函数;
  5. Go 源码需要先编译,再分发和运行。如果是单 Go 源文件的情况,我们可以直接使用
  6. go build 命令 +Go 源文件名的方式编译。不过,对于复杂的 Go 项目,我们需要在
  7. Go Module 的帮助下完成项目的构建。
我们使用 tree 命令来查看一下 Go 语言项目自身的最初源码结构布局,以 Go 1.3 版本为例,结果是这样的:
Go 语言基础--入门篇
文章图片

你会看到 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 项目所形成的典
型结构布局是这样的:
Go 语言基础--入门篇
文章图片

cmd 目录。 cmd 目录就是存放项目要编译构建的可执行文件对应的 main 包的源文件。如果你的项目中有多个可执行文件需要构建,每个可执行文件的 main 包单独放在一个子目录中,比如图中的 app1、app2,cmd 目录下的各app的 main 包将整个项目的依赖连接在一起。
main 包应该很简洁。我们在 main 包中会做一些命令行参数解析、资源初始化、日志设施初始化、数据库连接初始化等工作,之后就会将程序的执行权限交给更高级的执行控制对象。
pkgN 目录,这是一个存放项目自身要使用、同样也是可执行文件对应main 包所要依赖的库文件,同时这些目录下的包还可以被外部项目引用。
【Go 语言基础--入门篇】如果 Go 可执行程序项目有一个且只有一个可执行程序要构建,那就比较好办了,我们可
以将上面项目布局进行简化:
Go 语言基础--入门篇
文章图片

Go 库项目仅对外暴露 Go 包,这类项目的典型布局形式是这样的:
Go 语言基础--入门篇
文章图片

库类型项目相比于 Go 可执行程序项目的布局要简单一些。因为这类项目不需要构建可执行程序,所以去除了 cmd 目录。
以生产可执行程序为目的的 Go 项目,它的典型项目结构分为五部分:
  1. 放在项目顶层的 Go Module 相关文件,包括 go.mod 和 go.sum;
  2. cmd 目录:存放项目要编译构建的可执行文件所对应的 main 包的源码文件;
  3. 项目包目录:每个项目下的非main包都“平铺”在项目的根目录下,每个目录对应一个 Go 包;
  4. internal 目录:存放仅项目内部引用的 Go 包,这些包无法被项目之外引用;
  5. vendor 目录:这是一个可选目录,为了兼容 Go 1.5 引入的 vendor 构建模式而存在
  6. 的。这个目录下的内容均由 Go 命令自动维护,不需要开发者手工干预。
第二,对于以生产可复用库为目的的 Go 项目,它的典型结构则要简单许多,我们可以直
接理解为在 Go 可执行程序项目的基础上去掉 cmd 目录和 vendor 目录。
深入 Go Module 构建模式
Go Module 的语义导入版本机制:
Go 语言基础--入门篇
文章图片

语义版本号分成 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 的最小版本选择原则
Go 语言基础--入门篇
文章图片

在这张图中,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 语言基础--入门篇
文章图片

接下来的步骤就与添加兼容依赖一样,我们通过 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 函数)等。
  1. Go 包的初始化次序并不难,你只需要记住这三点就可以了:
  2. 依赖包按"深度优先"的次序进行初始化;
  3. 每个包内按以 常量 -> 变量 -> init 函数 的顺序进行初始化;
  4. 包内的多个 init 函数按出现次序进行自动调用。
init 函数具备的几种行为特征:
  1. 执行顺位排在包内其他语法元素的后面;
  2. 每个 init 函数在整个 Go 程序生命周期内仅会被执行一次;
  3. init 函数是顺序执行的,只有当一个init函数执行完毕后,才会去执行下一个init函数

    推荐阅读