一、Go 语言编译环境
1.1 Go 语言跨平台编译
1. 跨平台编译相关环境变量
- GOOS: Go的编译目标操作系统
- GOARCH: GO的编译目标CPU架构
src/go/build/syslist.go 中可查看支持的 OS 和 Arch 列表。
- GOOS 常见列表:
- linux
- darwin
- windows
- freebsd
- GOARCH 常见列表:
- amd64
- 386
- arm # 只支持linux OS
2. 常见跨平台编译设置
- Windows平台下为MacOS、Linux平台编译
|
|
- Linux平台为Windows、MacOS平台编译
|
|
- MacOS平台为Windows、Linux平台编译
|
|
1.2 Go 编译器指令简介
Golang 的编译器指令是以注释的形式存在的。
1、//line filename:line
指令指定源代码中的文件名及行号
//line filename:line
是一个特殊的注释,用于指定源代码中的文件名及行号,它通常用于生成代码或者调试时帮助开发者更好地定位问题。
例如,在调试一个使用了代码生成器的程序时遇到了问题,可以使用 //line
指令(注释)来查看生成的代码,示例:
|
|
编译输出:
|
|
//line
只是一个注释,不会影响代码的执行,它只是用于指定文件名和行号,以便在编译错误或调试时更好地定位问题。
2、//go:noinline
指令禁止当前函数内联
//go:noinline
的作用是禁止当前函数内联。inline是编译器优化代码的一种手段,将函数调用的地方替换为函数调用实际代码。
函数内联的优势如下:
- 减少函数调用的开销,提升性能,如果要进入函数需要存储函数入栈和出栈;
- 消除分支,充分利用CPU的cache的空间局部性和指令的顺序性;
同时也带来一些问题,如下:
- 导致同一个函数存在多个调用替换,增加code size;
- 如果代码过多,导致一个函数膨胀或者由于分支过多,可能降低缓存的命中率;
//go:noinline
的使用示例:
|
|
汇编输出:
|
|
3、//go:noescape
禁止变量逃逸
//go:noescape
的作用是禁止变量逃逸。
逃逸 是指编译器自动地将超出自身生命周期的变量,从函数栈转移到堆中的行为。 Golang 自动将函数外部引用的变量转移到堆中,从而保障其它地方使用不会为空或者异常。
禁止逃逸优势:如果变量不逃逸,GC压力就会很小,提升性能; 禁止逃逸也带来一些问题:如果函数外部使用已经销毁的变量,就会异常或者core;
示例:
|
|
noescape
函数 加上 go:noescape
,escape
函数什么都不加,执行输出:
|
|
可以看出没有内存逃逸的noescape的性能要好很多。
4、//go:norace
跳过竞态检测
//go:norace
的作用是跳过竞态检测。
优势:由于Goroutine很方便写并行逻辑,但是往往我们再代码中需要通过go run -race xxx.go
测试是否有竞态,会导致编译慢,如果加上 norace
就可以跳过;
同时也带来一些问题:需要开发者自己控制是否变量竞争的问题; 示例:
|
|
没有加go:norace
,执行会提示输出:
|
|
加了go:norace
,执行则直接不提示。
5、//go:nosplit
跳过栈溢出检测
//go:nosplit
的作用是跳过栈溢出检测。
栈溢出:Goroutine的起始栈大小是有限制的,且比较小的,可以做到支持并发很多Goroutine,并高效调度,如果当前栈不够用,则会自动扩展栈空间。
栈溢出也会引入性能问题,因为需要检查栈是否需要动态扩展,如果加上//go:nosplit
,则跳过这个检查。
跳过栈溢出检测的优势:不执行栈溢出检查,提升性能; 同时也带来一些问题:可能导致stack overflow;
|
|
加上go:nosplit,执行go run main.go
执行输出:
|
|
6、 //go:linkname localname importpath.name
//go:linkname
的作用是编译器使用importpath.name
作为源代码中声明为localname
的变量或函数的目标文件符号名称。但是由于这个伪指令,可以破坏类型系统和包模块化,只有引用了 unsafe 包才可以使用。
简单来讲,就是 importpath.name
是 localname
的符号别名,编译器实际上会调用 localname
。使用的前提是使用了 unsafe
包才能使用。
示例:
|
|
在这个案例中可以看到 time.now函数 并没有具体的实现。通过全局搜索一下源码,就会发现其实现在 runtime 中 有具体实现:
|
|
其中在 runtime/timestub.go 中的实现如下
|
|
配合//go:linkname
的用法解释,可得知在 runtime 包中,声明了 time_now
方法是 time.now
的符号别名。并且在文件头引入了 unsafe 达成前提条件。
示例:
|
|
|
|
编译时报错:
|
|
这是因为 go build
添加了 -complete
参数来检查完整性。提供.s文件后, 以便编译器绕过 -complete
的检查,允许不完整的函数声明。
在 hello 文件夹下添加任意的 .s 的汇编文件(例如 hello.s 或 xx.s)便可以通过编译执行:
|
|
注意:需要使用import _ "unsafe"
引入unsafe包。
7、//go:notinheap
类型声明
//go:notinheap
的作用是类型声明,不允许当前类型在GC堆上申请内存,主要为了提升性能。
示例:
|
|
这个是mcache代码 //go:notinheap
。
8、//go:build
//go:build是一个编译器指令,用于根据条件编译代码。语法:
|
|
如果是支持linux并且是amd64,可以写 //go:build linux && amd64
|
|
如果执行GOARCH=amd32 go run .
后输出:
|
|
表示不支持当前类型,需要执行GOARCH=amd64 go run .
才能编译通过。
9、其它编译指令
//go:systemstack
一个函数必须在系统栈上运行,这个会通过一个特殊的函数前引(prologue)动态地验证。//go:nowritebarrier
告知编译器如果以下函数包含了写屏障,触发一个错误。//go:yeswritebarrierrec
告知编译器如果以下函数以及它调用的函数(递归下去),直到一个go:yeswritebarrierrec为止,包含了一个写屏障的话,触发一个错误。
1.3 Go 条件编译
Go没有预处理,没有宏定义系统,不可以像c语言那样使用#define
来控制是否包含平台相关的特定代码。作为替代,Go使用go/build包中定义的标签系统(system of tags)和命名约定(naming convention)以及go tool
中的相应支持来允许Go包编译特定代码,即 条件编译。
在某些场景下需要对进行编译的 Go 文件做限制,例如:
- 在 Debug 时,希望编译的是 a.go 文件,而生产环境下希望编译的是 b.go 文件;
- 在 linux 系统中,希望编译的是 a.go 文件,而 darwin 系统中个,系统编译的是 b.go 文件;
Golang支持两种 条件编译 的实现方式:编译标签(build tags) 和 文件后缀(file postfix)。
方式一:编译标签(build tags)
在 Go 文件中可以用类似注释的方式设置编译限制(条件编译指令),使用 // +build
编译器指令在源文件中的代码之前 设置 编译标签(build tags)。
例如文件 a.go 的源代码之前有如下内容:
|
|
则运行该文件必须带上 debug 标签,eg:
|
|
需要注意的是该类标签只能写在文件中的源代码之前,并且其后需要跟一个空行才能生效。 以下是一个包含了许可证,构建标签,包声明的例子:
|
|
编译标签(build tags)遵循以下三个原则:
-
每个选项由字母和数字组成,如果前面加上
!
是非(NOT)的关系; -
逗号隔开的选项之间是与(AND)的关系;
-
空格隔开的选项之间是或(OR)的关系;
-
!
非(NOT):非标签标示的是编译或者运行时不可携带 debug 标签;
|
|
,
与(AND):多个标签中的逗号代表 “与” 的含义,下述标签代表需要 taga 和 tagb 同时存在;
|
|
编译或运行时命令:
|
|
space
或(OR):多个标签中使用空格代表 “或” 的语义,下述标签表示需要taga
或者tagb
二者其中之一即可;
|
|
标签可以写成多行,表示的仍然所示 “与” 的含义,如下:
|
|
标签可以进行"与或非"语义的组合,如下:
|
|
该语义表示 (taga && tagb) || (tagc && !tagd)
方式二:文件名后缀(file postfix)
文件名后缀(file postfix) 的方式限定了运行和编译文件的系统和架构。这个方法通过改变文件名的后缀来提供条件编译,这种方案比编译标签要简单,go/build可以在不读取源文件的情况下就可以决定哪些文件不需要参与编译。
文件命名约定可以在go/build 包里找到详细的说明,简单来说如果源文件名包含后缀:_GOOS.go
,那么这个源文件只会在这个平台下编译,_GOARCH.go
也是如此。这两个后缀可以结合在一起使用,但是要注意顺序为:_GOOS_GOARCH.go
,不能反过来用:_GOARCH_GOOS.go。
Go 源文件中,这三类特殊的模式会在编译时进行识别,分别是:
- *_GOOS:例如 source_linux.go 表示该文件需要在 linux 系统才能够运行;
- *_GOARCH:例如 source_amd64.go 表示该文件需要在 amd64 架构下才能够运行;
- *_GOOS_GOARCH:例如 source_linux_amd64.go 表示该文件需要在 amd64 架构的 linux 系统中才能够运行。
条件编译小结:
编译标签(build tags) 和 文件名后缀(file postfix) 可作用于任何go tool可以编译的源码文件,包括.c和.s文件。Go标准库中,尤其是runtime,syscall,os,net包中包含了大量这种例子。
测试文件也支持 编译标签(build tags) 和 文件名后缀(file postfix),它们的表现和Go源码文件表现一致。从而允许我们为特定平台指定特定测试用例。
1.4 go list 命令
go list
允许访问包内部的数据结构从而驱动编译过程(build process)。
go list
的大部分参数和 go build
、go test
、go install
相同,但是它不会执行编译。使用 -f
格式化参数,可以填入一段 text/template的模板代码,它会在一个包含go/build.Package结构的上下文环境被执行。 如下例子,可以得到所有将被编译的源码文件名:
|
|
这个例子查询了在当前执行环境 linux/arm 下 os/exec包 和 fmt 中将会被编译的文件。结果文件有两类:
- exec.go、doc.go 等无 _GOOS.go、_GOARCH.go 和 _GOOS_GOARCH.go 后缀名的源代码文件包含了在所有平台共享的通用代码;
- lp_unix.go 是有 _GOOS.go、_GOARCH.go 或 _GOOS_GOARCH.go 后缀名的源代码文件包含了一份在unix-like系统独有的exec.LookPath实现;
如果在Windows系统执行上面那个命令,结果如下:
|
|
二、Go语言包
2.1 Go语言包简介
Go语言是使用包(package)来组织源代码的,包(package)是多个 Go 源码的集合,是一种高级的代码复用方案。Go语言中为我们提供了很多内置包,如 fmt、os、io 等。
Go语言的包(package)借助了目录树的组织形式,一般包的名称就是其源文件所在目录的名称,虽然Go语言没有强制要求包名必须和其所在的目录名同名,但还是建议包名和所在目录同名,这样结构更清晰。
包(package)可以定义在很深的目录中,包名的定义是不包括目录路径的,但是包在引用时一般使用全路径引用。
Go语言的每一个代码文件都是属于一个包(package)的,go是以包的形式来管理的,一个源代码文件目录(不包括子目录)就是一个 包(package),一般目录名就是 包名(package name)。
包的习惯用法:
- 包名一般是小写的,使用一个简短且有意义的名称。
- 包名一般要和所在的目录同名,也可以不同,包名中不能包含
-
等特殊符号。- 包名为
main
的包为应用程序的入口包,编译不包含 main 包的源码文件时不会得到可执行文件。
- 包名为
- 一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码文件不能放在多个文件夹下。
包(package)的作用:
1、区分同名字的函数、变量等标识符 2、当程序较多时,可以很好的管理项目 3、控制函数、变量的访问范围,即作用域
打包 和 导入包:
|
|
一般在每一个源代码文件的第一行使用 package 包的路径/包名
指令声明 包。
示例:
|
|
2.2 包的引用格式
包的引用有四种格式,下面以 fmt 包为例来分别演示一下这四种格式。
1、标准引用格式
|
|
此时可以用fmt.作为前缀来使用 fmt 包中的方法,这是常用的一种方式。
|
|
2、自定义别名引用格式
在导入包的时候,我们还可以为导入的包设置别名,如下所示:
|
|
其中 F
就是 fmt
包的别名,使用时可以使用 F.
来代替标准引用格式的 fmt.
来作为前缀使用 fmt 包中的方法
|
|
3、省略引用格式
|
|
这种格式相当于把 fmt
包直接合并到当前程序中,在使用 fmt
包内的方法是可以不用加前缀 fmt.
,直接引用。
|
|
4、匿名引用格式
在引用某个包时,如果只是希望执行包初始化的 init
函数,而不使用包内部的数据时,可以使用匿名引用格式,如下所示:
|
|
匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。
使用标准格式引用包,但是代码中却没有使用包,编译器会报错。如果包中有 init 初始化函数,则通过 import _ "包的路径"
这种方式引用包,仅执行包的初始化函数,即使包没有 init 初始化函数,也不会引发编译器报错。
|
|
注意:
- 一个包可以有多个 init 函数,包加载时会执行全部的 init 函数,但并不能保证执行顺序,所以不建议在一个包中放入多个 init 函数,将需要初始化的逻辑放到一个 init 函数里面。
- 包不能出现环形引用的情况,比如包 a 引用了包 b,包 b 引用了包 c,如果包 c 又引用了包 a,则编译不能通过。
- 包的重复引用是允许的,比如包 a 引用了包 b 和包 c,包 b 和包 c 都引用了包 d。这种场景相当于重复引用了 d,这种情况是允许的,并且 Go 编译器保证包 d 的 init 函数只会执行一次。
2.3 包使用细节
-
包名通常和文件所在的文件夹(目录)名一致,一般为
小写字母
比如 utils 文件夹,对应的包名就是utils
-
当一个文件要使用其它包函数或变量时,需要先引入对应的包,再使用 包的引入方式:
import 包名
或import (包名1,包名2)
;- 在
import
包时,路径从$GOPATH
的src
下开始(不用带src,编译器会自动从src下开始引入);或者从当面项目目录开始;
- 在
-
为了让其它的包文件,可以访问到本包的函数,则该函数名 的
首字母需要大写
; 同理,如果需要调用其它包的 常量/变量名 也必须要首字母大写 -
在调用其它包函数时,其语法是
包名.函数名
; 比如上面的的main.go文件中 调用 utils 包的 Add 函数:1
utils.Add(12, 23)
-
如果包名较长,Go支持给包起别名,注意细节,取别名后,“原来的包名就不能用了”。
-
在同一个"包"下,不能有完全相同的函数名、常量、变量,否则包内标识符重复定义。
-
如果要把一个项目(代码)编译成一个可执行文件,就需要这个项目下有一个包声明为
main
包 并且该包内包含main
函数实现 ,即package main
+func main() {...}
,这个就是一个语法规范,如果是写一个库,包名可以自定义。
三、Go 语言依赖包管理的核心(Go Modules)
3.1 go mod 简介
go mod 是 Go 语言的简化依赖的管理的工具,是Go v1.11版引入的,用于依赖管理的官方解决方案。并且从 Go1.13 版本开始,go module 成为了Go语言默认的依赖管理工具。
go mod主要解决了这些问题
- 依赖关系混乱,不清楚具体使用了哪些包和版本
- 不同机器无法复现相同的依赖构建,导致构建失败
- 上传代码时缺失依赖,导致他人无法构建
在Go1.11后新增了modules特性,模块是相关Go包的集合。如果在cmd中执行以下命令将GO111MODULE变量的值设为on:
|
|
GO111MODULE 有三个值:off, on和auto(默认值)。
- GO111MODULE=off,go命令行将不会支持module功能,寻找依赖包的方式将会沿用旧版本那种通过vendor目录或者GOPATH模式来查找。
- GO111MODULE=on,go命令行会使用modules,而一点也不会去GOPATH目录下查找。
- GO111MODULE=auto,默认值,go命令行将会根据当前目录来决定是否启用module功能。这种情况下可以分为两种情形:
- 当前目录在GOPATH之外且该目录包含go.mod文件 开启
- 当处于 GOPATH 内且没有 go.mod 文件存在时其行为会等同于 GO111MODULE=off
- 如果不使用 Go Modules, go get 将会从模块代码的 master 分支拉取
- 而若使用 Go Modules 则可以利用 Git Tag 手动选择一个特定版本的模块代码
go mod 有以下命令:
命令 | 说明 |
---|---|
download | download modules to local cache(下载依赖包) |
edit | edit go.mod from tools or scripts(编辑go.mod) |
graph | print module requirement graph (打印模块依赖图) |
init | initialize new module in current directory(在当前目录初始化mod) |
tidy | add missing and remove unused modules(拉取缺少的模块,移除不用的模块) |
vendor | make vendored copy of dependencies(将依赖复制到vendor下) |
verify | verify dependencies have expected content (验证依赖是否正确) |
why | explain why packages or modules are needed(解释为什么需要依赖) |
go mod 的主要功能有
- 使用 go.mod 文件记录依赖信息,根据 import 自动添加依赖
- 自动下载依赖包并缓存在 GOPATH/pkg/mod 目录
- 可以查询、添加、、更新、删除依赖包
- 可以指定依赖版本,锁定依赖版本
- 可以设置代理,实现国内下载加速
go mod 基本概念
- module: 一个模块,通常对应一个代码仓库,一个 module 拥有独立的依赖信息
- package: 一个 module 中的代码包,通常是一个目录(不包含子目录)一个package
- dependency: 依赖包,一个 module 需要引用的其它 module
一个 module 拥有一个唯一的 module path 以及一个版本号。
go mod 工作原理
- go mod 通过 go.mod 文件记录当前 module 的元信息和所有依赖项信息。
- 然后会自动下载并缓存依赖的包,缓存放在 GOPATH/pkg/mod 目录。
- 通过 go mod 管理依赖,就可以很方便地重现相同的依赖环境。
go mod 命令参考
|
|
3.2 创建 go.mod 文件
go.mod 文件存储在模块根目录,记录了当前模块的元数据,包括模块路径、依赖列表等。 使用 go mod init 来初始化 go.mod 文件
|
|
这会在模块根目录生成一个 go.mod 文件
|
|
也可以通过 go build、go run 等命令自动创建 go.mod 文件。
3.3 查询依赖 及 生成依赖关系图
|
|
3.4 添加、更新依赖
使用 go get 可以下载依赖
|
|
使用 go get -u 可以升级依赖包到最新的次要版本。 也可以手动修改 go.mod Require 语句指定版本后,运行 go get 导致更新。
|
|
3.5 删除依赖
go mod tidy 可以移除不用的依赖项。 也可以直接编辑 go.mod 文件,删除不需要的 module 语句, 然后执行 go mod tidy 重新整理。
3.6 go mod 原理解析
go.mod 使用语义版本规范,可以记录最小和最大版本号。 环境变量控制代理,实现国内代理加速。 go mod 通过元数据和缓存实现可重现的依赖构建。
3.7 代理设置
GOPROXY 环境变量控制下载代理
|
|
GOPRIVATE 控制不走代理的私有模块。