# go-enhance **Repository Path**: zhangshenao/go-enhance ## Basic Information - **Project Name**: go-enhance - **Description**: Go 语言进阶 - **Primary Language**: Go - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2023-11-16 - **Last Updated**: 2024-09-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # go-enhance——Go 语言进阶 # 1. Go 语言简介 ## 1.1 入坑 Go 语言的三大理由 1. 对初学者足够友善,能够快速上手。Go 是一种非常简单的语言,它作为一门静态编程语言,已经将入门门槛降低到了动态语言的水平。 2. Go 语言是生产力与性能的最佳结合。 3. Go 语言编程体验好,且发展前景广阔。 ## 1.2 Go 语言发展历史与现状 ### 1.2.1 Go 语言创始人 - 肯·汤普森(Ken Thompson):图灵奖获得者、C 语法联合发明人、Unix 之父。 - 罗伯·派克(Rob Pike):Plan 9 操作系统领导者、UTF-8 编码的最初设计者。 - 罗伯特·格瑞史莫(Robert Griesemer):Java 的 HotSpot 虚拟机和 Chrome 浏览器的 JavaScript V8 引擎的设计者之一。 **Go 语言的设计初衷:在 C 语言的基础上,修正一些明显的缺陷,删除一些被诟病较多的特性,并增加一些缺失的功能。比如,使用 import 替代 include、去掉宏、增加垃圾回收、支持接口等。** ### 1.2.2 Go 语言发展历程 - 2009.11.10:Go 语言项目开源,Go 语言正式诞生。 - 2011.3.28:Go 1.0 版本正式发布,同时 Go 官方发布了“Go 1 兼容性”承诺。 - 2014:Go 1.4 版本发布,这是最后一个由 C 语言实现编译器和运行时的版本。 - 2015:Go 1.5 版本发布,Go 语言实现了自举,并大幅降低了 GC 延迟。 - 2018:Go 1.11 版本发布,引入最新的包管理机制:go module。 - 2021:Go 1.16 版本发布,go module 成为了默认的包管理机制。 - 2022:Go 1.18 版本发布,支持泛型(类型参数)。 ## 1.3 Go 语言的设计哲学 - 简单 - **less is more** - Go 语言的设计者们在语言设计之初,就拒绝了走语言特性融合的道路,而是选择了“做减法”,并致力于打造一门简单的语言,同时又不乏生产力。 - 简单的设计哲学是 Go 生产力的源泉,也是 Go 对开发者最大的吸引力。 - 显式 - Go 希望任何代码行为都需开发者明确知晓,不存在因“暗箱操作”而导致可维护性降低和不安全的结果。 - 组合 - **Go 语言完全摒弃了面向对象,而是选择了组合优于继承。** - 通过 Type Embedding 类型嵌入,实现了垂直组合。 - 通过接口实现了水平组合。 - 组合原则的应用,实际上塑造了 Go 程序的骨架结构,它可以大幅降低程序元素间的耦合,提高程序的可扩展性和灵活性。 - 并发 - **Go 语言面向工程,原生支持并发。可以让开发人员在多核时代更容易写出充分利用系统资源、支持性能随 CPU 核数增加而自然提升的应用程序** - **Go 采用了用户层轻量级线程,即 goroutine。** - 面向工程 - **Go 语言设计的初衷,就是面向解决真实世界中 Google 内部大规模软件开发存在的各种问题,为这些问题提供答案。** - Go 将语言要解决的问题域扩展到那些原本并不是由编程语言去解决的领域,从而覆盖了更多开发者在开发过程遇到的“痛点”,为开发者提供了更好的使用体验。 # 2. Go 的程序结构与构建方式 ## 2.1 Go 程序结构 - **包(package)是 Go 程序的基本组成单元,通常采用单个的小写单词命名,一个 Go 程序本质上就是一组包的集合。整个 Go 程序中仅允许存在一个名称为 main 的包,main 包中的 main 函数是程序的执行入口。** - 可以通过 import 声明进行包的导入,import 后面指定包的完整路径。被导入后的包,可以通过包名调用包内的语法元素。main 包不可以被导入,否则会编译报错。 - Go 是编译型语言,Go 源码需要先经过编译,再分发和运行。通过 go build 命令编译后,会生成特定平台下的可执行文件,并可以直接运行在没有 Go 环境的相同平台上。 - 对于复杂项目的构建,需要采用 Go Module 的构建模式。 ## 2.2 Go 项目布局标准 ### 2.2.1 可执行程序项目的典型结构布局 ```Shell exe-layout ├── cmd/ │ ├── app1/ │ │ └── main.go │ └── app2/ │ └── main.go ├── go.mod ├── go.sum ├── internal/ │ ├── pkga/ │ │ └── pkg_a.go │ └── pkgb/ │ └── pkg_b.go ├── pkg1/ │ └── pkg1.go ├── pkg2/ │ └── pkg2.go └── vendor/ ``` **以生产可执行程序为目的的 Go 项目,它的典型项目结构分为五部分:** - 放在项目顶层的 Go Module 相关文件,包括 go.mod 和 go.sum; - cmd 目录:存放项目要编译构建的可执行文件所对应的 main 包的源码文件; - 项目包目录:每个项目下的非 main 包都“平铺”在项目的根目录下,每个目录对应一个 Go 包; - internal 目录:存放仅项目内部引用的 Go 包,这些包无法被项目之外引用; - vendor 目录:这是一个可选目录,为了兼容 Go 1.5 引入的 vendor 构建模式而存在的。这个目录下的内容均由 Go 命令自动维护,不需要开发者手工干预。 ### 2.2.2 库项目的典型结构布局 ```Shell lib-layout ├── go.mod ├── internal/ │ ├── pkga/ │ │ └── pkg_a.go │ └── pkgb/ │ └── pkg_b.go ├── pkg1/ │ └── pkg1.go └── pkg2/ └── pkg2.go ``` ## 2.3 Go Module 构建模式 ### 2.3.1 基础概念 - **Go 程序的构建过程,就是确定包版本、编译包和将编译后生成的目标文件链接在一起的过程。** - **一个 Go Module 就是一组包的集合。** - **Go 包是程序逻辑封装的基本单元,每个包都可以理解为是一个“自治”的、封装良好的、对外部暴露有限接口的基本单元。** - Module 是有**版本**的,Module 下的包也有版本属性。 - 这个 Module 与它所有依赖的包组成了一个独立的版本单元,它们一起打版本、构建和分发。 - 通常一个代码仓库对应一个 Go Module,一个 Go Modele 的顶层目录下存在一个 go.mod 文件,**Go Module 与 go.mod 文件是一一对应的。** - go.mod 文件所在的顶层目录也被称为 module 的根目录,module 根目录以及它子目录下的所有 Go 包均归属于这个 Go Module,这个 module 也被称为 main module。 - go.mod 路径下,还存在一个 go.sum 文件,它是根据所有依赖的 module 和特定版本生成的 Hash 值,主要用于在构建时判断依赖是否发生了变更。 - 最佳实践是将 go.mod 和 go.sum 文件一并提交到代码仓库进行管理。 ### 2.3.2 语义导入版本 (Semantic Import Versioning) - **Go 语言规定,在 Go Module 中依赖的包,统一按照 `vmajor.minor.patch` 的格式指定版本号。** - 按照语义版本规范,主版本号不同的的两个包被认为是不兼容的。 - 如果同一个包的新旧版本是兼容的,那么它们的包导入路径应该是相同的。 - **可以在 import 语句中指定导入包的版本**,即 `import xxx/yyy/zzz/v2` 。在同一个 Go Module 中可以同时依赖一个包的多个版本。 ### 2.3.3 最小版本选择 (Minimal Version Selection) - **在执行 go mod tidy 后,Go Module 机制会默认选择我们指定依赖的最新版本。** - 在一个 Go Module 中,如果同时依赖了一个包的不同版本,那么Go 会在该项目依赖项的所有版本中,选出符合项目整体要求的“最小版本”。 ### 2.3.4 Go Module 常用操作 #### 2.3.4.1 使用 Go Module 模式构建步骤 1. 执行 go mod init xxx,进行 Go Module 初始化操作,生成 go.mod 文件。 2. 编写代码。 3. 执行 go mod tidy,自动更新依赖信息、go.mod 和 go.sum 文件。**go mod tidy 可以自动分析 Go 源码的依赖变更,包括依赖的新增、版本变更以及删除,并更新 go.mod 和 go.sun 中的依赖信息**。 4. 执行 go build 进行项目构建。 #### 2.3.4.2 为当前 module 引入新的依赖 1. 执行 go get xxx 命令,下载依赖包。**Go 会默认为我们下载指定 major 版本下的最新版依赖包。** 2. 使用新引入的依赖开发。 3. 执行 go mod tidy,自动更新依赖信息。 #### 2.3.4.3 升级 / 降级依赖的版本 1. 通过 go list -m -versions 命令,可以列出一个依赖包的所有版本: ```Shell go list -m -versions github.com/sirupsen/logrus ``` 2. 在项目的 module 根目录下,执行带有版本号的 go get 命令 ```Shell go get github.com/sirupsen/logrus@v1.7.0 ``` 3. 执行 go mod tidy,此时项目中依赖的就是我们指定的版本了。 4. 也可以通过 go mod edit -require=xxx 命令,明确指定我们需要依赖的版本,之后再执行 go mod tidy 就可以了。 ```Shell go mod edit -require=github.com/sirupsen/logrus@v1.7.1 module-mode go mod tidy go: downloading github.com/sirupsen/logrus v1.7.1 go: downloading github.com/magefile/mage v1.10.0 ``` #### 2.3.4.3 添加一个主版本号大于1的依赖 1. 在源码中,以空导入的方式导入高版本的依赖包。**针对版本号 >1 的包的导入,需要通过 vx 来指定版本**: ```go import _ "github.com/go-redis/redis/v7" //"_"为空导入 ``` 2. 通过 go get 命令,下载高版本的依赖包: ```Shell go get github.com/go-redis/redis/v7 go: downloading github.com/go-redis/redis v6.15.9+incompatible go: downloading github.com/go-redis/redis/v7 v7.4.1 go: added github.com/go-redis/redis/v7 v7.4.1 ``` #### 2.3.4.4 将依赖升级到一个不兼容的版本 1. 以空导入的方式导入新版本的依赖包: ```Go import _ "github.com/go-redis/redis/v8" //"_"为空导入 ``` 2. 通过 go get 命令下载新版本的依赖包: ```shell go get github.com/go-redis/redis/v8 ``` 3. 执行 go mod tidy,更新依赖信息。 #### 2.3.4.5 移除一个依赖 1. 在源码中删除对依赖包的 import 语句。 2. 执行 go mod tidy,更新依赖信息。**go mod tidy 会自动分析源码依赖,而且将不再使用的依赖从 go.mod 和 go.sum 中移除**。 ## 2.4 Go 程序执行顺序 ### 2.4.1 main.main 函数:Go 程序的入口函数 - main 包中的 main 函数,即 main.main,它是所有 Go 可执行程序的用户层执行逻辑的入口函数。 - **可执行程序的 main 包必须定义 main 函数,否则 Go 编译器会报错**。 - main.main 函数将在 Go 应用的主 Goroutine 中执行。 - main.main 函数的返回就意味着整个 Go 程序的终止,不论是否还有其它子 Goroutine 在执行。 ### 2.4.2 init 函数:Go 包的初始化函数 - 在每个 Go 包中,都可以定义 init 初始化函数,这些 init 函数会在包初始化时被自动执行。 - init 函数不可以被手动显式调用,否则会收到编译报错。 - 一个 Go 包可以定义多个 init 函数,在初始化 Go 包时,Go 会按照一定的次序,逐一、顺序地调用这个包的 init 函数。一般来说,先传递给 Go 编译器的源文件中的 init 函数,会先被执行;而同一个源文件中的多个 init 函数,会按声明顺序依次执行。 ### 2.4.3 Go 包的初始化顺序 - **Go 在进行包初始化时,会采用“深度优先”原则,递归初始化各个包的依赖包。** - **在每个包内部,会按照常量 -> 变量 -> init 函数的顺序进行包初始化。** - 如果包内定义了多个 init 函数,那么这些 init 函数会按照声明顺序进行初始化。 - 如果一个包被多个包依赖,那么仅会初始化一次。 ### 2.4.4 init 函数的用途 - 重置包级变量值。 - 实现对包级变量的复杂初始化。 - 在 init 函数中实现“注册模式”。这种方式在空导入时用得比较多,即导入一个包,但是并不使用它的任何语法元素,而仅仅是为了执行包的 init 函数进行注册。这种通过在 init 函数中注册自己的实现的模式,就有效降低了 Go 包对外的直接暴露,尤其是包级变量的暴露,从而避免了外部通过包级变量对包状态的改动。 # 3. Go 基础语法 ## 3.1 变量声明 ### 3.1.1 变量的声明方式 - 通用变量声明:var 关键字 + 变量名 + 变量类型 = 初始值 ```go var i int = 10 ``` - **一个变量如果仅被声明而没有被初始化,那么编译器就会给它自动赋予类型对应的零值。**数组、结构体这样复合类型变量的零值就是它们组成元素都为零值时的结果。 - 在声明变量时,可以省略类型信息,编译器会根据变量的初值,自动推到出变量的默认类型。 - **短变量声明:varName := initExpression。短变量声明是局部变量特有的一种声明方式,也是采用最多的。** ### 3.1.2 包级变量的声明方式 - 包级别变量只能采用带有 var 关键字的变量声明形式,不能使用短变量声明形式,但在形式细节上可以有一定的灵活度。 - 对于采用默认类型,且显式初始化的变量,可以采用省略类型的声明方式。 - 对于需要延迟初始化的包级变量,只能采用完整变量声明方式。 ### 3.1.3 局部变量的声明方式 - **对于延迟初始化的局部变量声明,采用通用的变量声明形式。** - **对于声明且显式初始化的局部变量声明,建议采用短变量声明的形式。** - **在分支控制语句中,尽量采用短变量声明形式。这种方式也体现了就近原则,即将变量的作用域最小化。** - **如果在声明局部变量时遇到了适合声明聚类的场景,也应该采用 var 声明块来声明多个局部变量。** ### 3.1.4 变量声明原则 - **声明聚类:将同一类的变量声明放在同一个 var 变量声明块中**。例如将延迟初始化的变量声明放在第一个 var 块中,将声明且显式初始化的变量放在第二个 var 块中。 - **就近原则:尽量在靠近第一次使用变量的位置声明这个变量,这样可以有效减少变量的作用域。**如果一个包级变量在包内部被多处使用,那么这个变量还是放在源文件头部声明比较适合的。 # 4. 接口 # 5. 并发 # 6. 项目实战 # 7. 相关概念汇总 - Go 语言在1.5版本实现了自举。**所谓自举,即使用要编译的目标语言(Go 语言)来编写其编译器(Go 语言编译器)**。在实现了自举之后,Go 核心团队基本就告别了 C 语言,开始专心写 Go 代码。使用 Go 语言实现编译器和 runtime 还利于 Go 编译器以及运行时的优化,Go 1.5及后续版本GC延迟大幅降低以及性能的大幅提升都说明了这一点。 - 向后兼容(backwards compatible):使用新版编译器也可以正确编译用老版本语法编写的代码。 - 自带电池:如果说一门编程语言是“自带电池”,则说明这门语言标准库功能丰富,多数功能不需要依赖外部的第三方包或库。Go 语言恰恰就是这类编程语言。 - 可重现构建(Reproducible Build):针对同一份 go module 的源码进行构建,不同人、在不同机器(同一架构,比如都是x86-64)上、相同的 OS 上、在不同的时间点,都能得到相同的二进制文件。**本质上明确定义了项目所依赖的包及其版本。**