post:基本完成命令部分的内容。

This commit is contained in:
徐涛 2023-06-18 19:25:17 +08:00
parent 4ba6fa3126
commit 1cab115d2c
8 changed files with 462 additions and 3 deletions

View File

@ -6,9 +6,13 @@
- [Go 语言简介](./preguide/intro.md)
- [环境安装与配置](./preguide/installation.md)
- [不一样的编程思路](./preguide/codex.md)
- [基本命令]()
- [go 命令]()
- [go mod 命令]()
- [基本命令](./basic-cmd/index.md)
- [go mod](./basic-cmd/mod.md)
- [go get](./basic-cmd/get.md)
- [go run](./basic-cmd/run.md)
- [go build](./basic-cmd/build.md)
- [go install](./basic-cmd/install.md)
- [go generate](./basic-cmd/generate.md)
- [项目基本结构]()
- [创建一个项目]()
- [用包来组织代码]()

227
src/basic-cmd/build.md Normal file
View File

@ -0,0 +1,227 @@
# `go build`命令
`go build`命令顾名思义就是用来对项目进行编译的,它的命令格式为`go build [-o 输出目录] [编译参数] [待编译的包]`。
## 更改输出目录
默认情况下,`go build`会将编译输出的结果文件放置在与待编译包或者文件相同的目录中,而且名称也相同。例如在 Windows 系统中执行`go build main.go`来对`main.go`文件进行编译,那么编译后的结果文件就会与`main.go`文件在一起,且名称为`main.exe`。
如果需要更改`go build`的默认输出目录,就需要使用`-o`参数,`-o`参数中一般只需要指定输出目录即可,编译输出的文件名会与输出的文件名相同。
这里在输出目录和输出文件名的指定上有以下需要注意的情况:
1. 编译命令为`go build -o ./build main.go`,但`./build`目录不存在,那么此时`main.go`文件将会被编译为一个名为`build`的可执行文件。
1. 编译命令为`go build -o ./build main.go`,但`./build`目录已经存在,那么此时`main.go`文件将会被编译为一个保存在`./build`目录下的名为`main`的可执行文件。
1. 编译命令为`go build -o ./build/ main.go`,但`./build`目录不存在,那么此时`go build`命令将会创建`build`目录,然后将编译好的名为`main`的可执行文件放置在其中。
1. 编译命令为`go build -o ./build/ main.go`,但`./build`目录已经存在,那么此时`go build`命令将编译好的名为`main`的可执行文件放置在指定的`./build`目录中。
以上四种情况就是在指定编译目标的时候,带与不带结尾的`/`的区别,在实际编译项目的时候需要注意。
## 其他常用编译参数
一般情况下直接使用`go build`对项目进行编译就已经能够满足要求了,但是在一些特殊情况下,对于项目的编译还需要进行一些自定义,这是就需要使用到额外的编译参数了。
- `-a`,强制重新编译所有的包。
- `-n`,输出要执行的命令但并不实际执行。
- `-race`,使用数据竞争检测。该选项有平台限制,使用的时候需要查看所使用 Go 版本的说明。
- `-msan`,启用与内存清理器的交互。
- `-asan`,启动与地址清理器的交互。
- `-work`,输出临时工作目录并予以保留。
- `-buildmode mode`,设定要使用的编译模式。
- `-compiler name`,舍弟索要使用的编译器的名称,可选`gccgo`或者`gc`。
- `gccgoflags '[pattern=]arg list'`,设定使用`gccgo`编译器的时候需要传递给编译器的参数。
- `gcflags '[pattern=]arg list'`,设定使用`gc`编译器的时候需要传递给编译器的参数。
- `-ldflags [pattern=]arg list'`,设定每次工具链接调用时传递的参数。
- `-linkshared`,设定在构建前链接之前使用`-buildmode=shared`构建的动态链接代码。
- `-mod mode`,设定模块的下载使用模式。
- `-tags tag,list`,使用一个逗号分割的列表来设定用于约束编译的条件。
## 编译输出模式
在前面一节中,常用的参数提到了可以通过`-buildmode`来设定编译器的编译输出模式,常用的编译输出模式主要有 C 静态链接库、静态链接库、C 动态链接库、动态链接库、可执行文件等。
### C 静态链接库
输出 C 静态链接库需要使用`-buildmode=c-archive`参数。
C 静态链接库可以使用`//export 导出函数名`标记来标记`main`包中的函数,并将其作为静态链接库的导出函数供其他 C 程序使用。
例如编写一个简单的程序。
```go
package main
import (
"fmt"
"C"
)
func main() {}
//export Add
func Add(a, b int) int {
return a+b
}
```
这个示例文件`add.go`在编译为 C 静态链接库以后,将会生成`add.h`和`add.a`两个文件。熟悉 C 语言的朋友可以直接使用 C 语言中调用静态链接库的方法来使用生成的静态链接库。
### C 动态链接库
输出 C 动态链接库需要使用`-buildmode=c-shared`参数。
用来生成动态链接库的源代码文件与之前用来生成静态链接库的源代码文件是一致的,而且其中的标记也是一致的。但是编译后产生的文件则分别是`add.so`Windows 平台上是`add.dll`)和`add.h`。
### Go 动态链接库
输出 Go 动态链接库需要使用`-buildmode=shared`参数。这样构建出来的讲师仅能够由 Go 使用的动态链接库。
例如可以执行以下命令将 Go 标准库编译安装为动态链接库,`go install -buidlmode=shared std`,之后就可以使用`-linkshred`编译我们的程序,例如以下这个。
```go
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
```
此时,使用命令`go build -linkshared main.go`进行编译,可以发现生成的可执行文件体积比不使用`-linkshared`要少非常多,而使用`ldd`命令对其调用链介于分析,也可以看到它依赖了许多动态链接库文件。
但是这种方式生成的可执行文件体积虽然小,但是缺点也非常的多,如果系统中缺少了程序所依赖的动态链接库,或者动态链接库文件版本不匹配,那么程序都不会正常运行。
### 插件模式
插件模式是 Go 1.8 推出的一个新的构建方式,这个模式使用`-buildmode=plugin`进行编译。插件模式可以将一个`package main`包编译为一个动态链接库文件,然后可以供其他程序使用标准库中的`plugin`包功能在运行时动态加载。
例如有以下一个文件,其中提供了一个可以供其他程序调用的函数。
```go
package main
import "fmt"
type greeting string
func (g greeting) Say(s string) {
fmt.Printf("Hello %s.\n", s)
}
var Greeter greeting
```
现在使用命令将其编译为一个插件,执行`go build -buildmode=plugin -o greeter.so greeter.go`。
接下来就可以在其他的程序中使用标准库中的`plugin`包中的功能进行动态加载了。
```go
package main
import (
"fmt"
"os"
"plugin"
)
type Greet interface {
Say(string)
}
func main() {
plug, err := plugin.Open("./greeter.so")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
symObj, err := plug.Lookup("Greeter")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
var greeter Greet
greeter, ok := symObj.(Greet)
if !ok {
fmt.Println(err)
os.Exit(1)
}
greeter.Say("world")
}
```
```admonish caution
插件模式目前仅支持Linux、FreeBSD和macOS系统Windows系统中是不支持的。
```
## 条件编译
Go 中对于条件编译的支持,是通过一条特殊的注释来实现的,这个注释以`//go:build`开头,位于`package`语句之前,它可以控制整个文件是否会被包含在编译过程中。
例如以下注释:
```go
//go:build (linux && 386) || (darwin && !cgo)
```
这个注释就约束了这个文件只会在`go build`命令的`-tags`参数中,包含`"linux,386"`或者`"darwin"`且不包含`"cgo"`标签的时候才会进行编译,例如执行编译命令`go build -tags="linux,386"`的时候,这个文件就会参与到编译中,而执行命令`go build -tags="win,x64"`时,这个文件就会被略过。
条件编译的标签组合操作符与普通的逻辑表达式的操作符是一样的,`&&`表示并且,`||`表示或者,`!`表示非。`//go:build`注释在一个文件中可以出现多行,多个约束条件之间默认采用`&&`的关系。如果编译条件被设定为`//go:build ignore`,那么这个文件将不会被包含在任何编译中。
如果在`//go:build`中使用的是操作系统和平台架构相关的限制内容,那么编译约束除了可以在`-tags`参数中提供,还将受到`GOOS`和`GOARCH`两个环境变量的影响。
如果一个源代码文件中的内容只会被用在某一个操作系统或者某一个平台架构中,那么还有一种更加简单的办法来对其进行约束。在 Go 中,源代码文件的文件名在去除`.go`的后缀以后,其结尾使用下划线分隔开的部分内容对于 Go 语言是有意义的,所以在 Go 源代码文件的命名中,不要轻易的使用下划线。这里对源代码文件的命名规则做一个简单的介绍。
- 文件以`_test`结尾,则表明这是一个用于单元测试的文件,会在执行`go test`命令时被执行。
- 文件以`_GOOS`系列内容结尾,例如`_windows`、`_darwin`、`_linux`则表示该文件需要在`GOOS`环境变量为相应值的时候参与编译。
- 文件以`_GOARCH`系列内容结尾的时候,例如`_386`、`_amd64`,则表示该文件需要在`GOARCH`环境变量为对应值的时候参与编译。
- 文件以`_GOOS_GOARCH`格式内容结尾时,其中将同时匹配`GOOS`和`GOARCH`两个环境变量中的内容,例如`_windows_amd64`。
```admonish tip
`GOARCH`环境变量还可以定义其下的架构特征作为更加详细的过滤条件,例如`GOARCH=386, GO386=sse2`,这就会将`386.sse2`也列入到编译条件中。具体可以使用的结构特征可以参考Go官方文档中的说明。
```
## 交叉编译
交叉编译又称为跨平台编译是一种可以在一个平台上生成另一个平台使用的可执行程序的功能。Go 的工具链原生支持交叉编译功能。
使用 Go 的交叉编译十分简单,只需要在编译的时候临时改变以下环境变量的值即可。与交叉编译相关的环境变量主要有以下三个。
- `CGO_ENABLED`,用于指示 Go 编译器是否启用 CGO 功能对于使用交叉编译功能来说CGO 功能是不可用的,所以要恒定设置为`CGO_ENABLED=0`。
- `GOOS`,用于指示 Go 编译器,所编译的目标操作系统。
- `GOARCH`,用于指示 Go 编译器,所编译的目标平台架构。
所以在 macOS 上编译 Windows 和 Linux 系统的可执行程序就可以使用以下两个命令。
```bash
# 编译Windows平台程序
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
# 编译Linux平台程序
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
```
在 Linux 上编译 Windows 和 macOS 上的可执行程序可以使用以下两个命令。
```bash
# 编译Windows平台程序
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
# 编译macOS平台程序
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build main.go
```
在 Windows 上编译 Linux 和 macOS 上的可执行程序就有些不一样,但是核心原理还是一样的。
```bash
# 编译Linux平台程序
SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build main.go
# 编译macOS平台程序
SET CGO_ENABLED=0
SET GOOS=darwin
SET GOARCH=arm64
go build main.go
```
因为交叉编译每次都需要设置环境变量,所以建议在需要使用交叉编译的时候,可以将编译指令编写成一个脚本文件来简化编译过程。

99
src/basic-cmd/generate.md Normal file
View File

@ -0,0 +1,99 @@
# `go generate`命令
`go generate`命令的主要功能是根据代码中的注释,使用注释中声明使用的工具生成所需要的代码文件。首先能够被`go generate`命令识别的注释是有固定格式的,其格式如下。
```go
//go:generate 要执行的命令 命令参数
```
从这个注释的格式可以看出来,执行`go generate`命令实际上就是执行在代码注释中提前编写的命令。这样一来,就比较容易理解`go generate`命令了。我们可以把需要`go generate`执行的命令全部安装起来,然后根据需要编写在所需要的位置,只需要在最终编译项目的时候,提前使用`go generate`命令生成所需要的文件即可。
## 一个生成代码文件的示例
在网上有很多艳尸如何使用`go generate`命令的示例,基本上都大同小异,大多都是选择使用`golang.org/x/tools`工具包中的`cmd/stringer`工具来生成枚举类型的定义。其实还有许多其他的工具也是可以被`go generate`命令使用的,例如生成 Protobuf 数据包结构等。
这里也不免俗的继续用`stringer`工具来演示生成枚举类型定义的功能。在使用`go generate`命令之前,首先需要完成`stringer`工具的安装,可以执行命令`go install golang.org/x/tools/cmd/stringer@latest`来完成。
安装完`stringer`工具以后,我们需要首先来定义一套枚举类型。通常情况下我们定义的枚举类型是需要能够输出其代表内容的。例如如果不使用`stringer`工具生成的时候,手工编写的代码是这样的。
```go
type ErrCode int16
const (
Err_Authorize_Failed ErrCodes = iota // 用户认证失败
Err_User_Not_Exists // 用户不存在
Err_Operate_Denied // 操作被拒绝
Err_Service_Downgraded // 服务被降级处理
)
func (e ErrCode) String() string {
switch e {
case Err_Authorize_Failed:
return "用户认证失败"
case Err_User_Not_Exists:
return "用户不存在"
case Err_Operate_Denied:
return "操作被拒绝"
case Err_Service_Downgraded:
return "服务被降级处理"
}
return "出现未知错误"
}
```
这个文件的内容在每次错误内容发生变化的时候,都需要重新编辑一遍。所以最理想的情况就是我们只需要定义`ErrCode`类型和`const`定义部分,剩下的事情让辅助工具来做。在这里辅助工具就是`stringer`。
在继续之前,首先介绍`stringer`工具中常用的几个选项,这是会被用在`//go:generate`中的。
- `-type`,设定需要生成的内容依据的类型名称,如果需要生成多个类型的内容,则这些类型的名称需要用逗号分隔开。
- `-linecomment`,使用每一行的注释内容作为枚举值输出的内容,不指定这个选项的时候将会直接使用枚举常量本身的内容。
- `-trimprefix 前缀`,移除枚举常量中的前缀内容。
在我们这个示例中,我们只需要用到`-type`和`-linecomment`两个参数。以下是供`stringer`读取的源文件。
```go
package exceptions
//gp:generate stringer -type=ErrCode -linecomment
type ErrCode int16
const (
Err_Authorize_Failed ErrCodes = iota // 用户认证失败
Err_User_Not_Exists // 用户不存在
Err_Operate_Denied // 操作被拒绝
Err_Service_Downgraded // 服务被降级处理
)
```
这样这个文件就准备好了,现在我们只需要在这个文件所在的目录中执行`go generate`命令,`stringer`工具就会自动生成一个新的文件。这里将这个文件的内容贴出来。
```go
// Code generated by "stringer -type=ErrCode -linecomment"; DO NOT EDIT.
package exceptions
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[Err_Authorize_Failed-0]
_ = x[Err_User_Not_Exists-1]
_ = x[Err_Operate_Denied-2]
_ = x[Err_Service_Downgraded-3]
}
const _ErrCode_name = "用户认证失败用户不存在操作被拒绝服务被降级处理"
var _ErrCode_index = [...]uint8{0, 18, 33, 48, 69}
func (i ErrCode) String() string {
if i < 0 || i >= ErrCode(len(_ErrCode_index)-1) {
return "ErrCode(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ErrCode_name[_ErrCode_index[i]:_ErrCode_index[i+1]]
}
```
可以看到`go generate`调用`stringer`工具根据设定的配置生成了一个经过了优化的代码。

37
src/basic-cmd/get.md Normal file
View File

@ -0,0 +1,37 @@
# `go get`命令
`go get`民引领基本上是在 Go 程序构建的过程中使用的最多的一个命令了。它的主要功能就是下载依赖包到本地缓存并将其添加到项目的`go.mod`依赖描述中。
`go get`命令的使用格式为`go get [-t] [-u] [-v] [编译标记] [依赖包]`。
## 控制项目的依赖
下载一个依赖包的最新版本是非常简单的,可以直接使用以下命令。
```bash
go get archgrid.xyz/pkg
```
如果需要下载一个指定的版本,只需要使用`@`符号即可。
```bash
go get archgrid.xyz/pkg@0.1.0
```
移除一个依赖包就比较有技巧了,可以使用`@none`版本标记。
```bash
go get archgrid.xyz/pkg@none
```
## 升级项目中的依赖
升级项目中的依赖包版本可以使用`go get -u`,如果不指定具体要升级的依赖包名称和版本,那么将会升级项目中所使用到的全部依赖包的版本。
## 旧版本 Go 中的`go get`
`go get`命令在旧版本 Go 或者在没有使用 Go Module 的项目中,其实际下载依赖包的动作会很不一样。
首先,`go get`命令不会将下载的依赖包添加到`go.mod`文件中,因为在不使用 Go Module 时,项目中也不会有`go.mod`文件。
其次,`go get`命令会对下载的依赖包进行编译和安装,这一点就有点儿像`go install`命令了。

24
src/basic-cmd/index.md Normal file
View File

@ -0,0 +1,24 @@
# Go 语言的基本命令
Go 语言的大部分功能都是通过一系列的子命令提供的,其中比较常用的子命令主要有以下这些。
- `go`Go 语言核心命令,主要通过其提供的子命令来完成相应的功能。
- `go build`,编译 Go 语言代码以及依赖包。
- `go mod`Go Module 相关管理命令,主要提供了 Go Module 功能相关的管理功能。
- `go clean`,提供对于项目中编译中间文件和缓存文件的清理功能。
- `go doc`,提供展示指定依赖包或标识符的文档的功能。
- `go env`,用于显示当前 Go 安装环境中所有的环境变量。
- `go fix`,用于升级依赖包。
- `go fmt`,格式化指定 Go 代码文件。
- `go generate`,用于根据源码定义自动生成 Go 文件。
- `go get`,用于下载依赖包并将其安装到当前香茗中。
- `go install`,用于下载指定的包并将其编译为二进制可执行文件,
- `go list`,列出或查询包或者模块。
- `go work`,管理工作空间。
- `go run`,编译并运行指定的程序。
- `go test`,运行指定的单元测试。
- `go version`,列出当前安装的 Go 版本。
基本上所有的子命令都可以使用`go help <command>`的子命令形式来获取其内置的帮助说明。
这里会选取几个最为常用的命令进行介绍。

7
src/basic-cmd/install.md Normal file
View File

@ -0,0 +1,7 @@
# `go install`命令
与`go get`命令不同,`go install`命令会将指定的包进行编译,并将编译好的可执行文件放置在`$GOPATH/bin`或者`$HOME/go/bin`目录中,从而允许直接在终端或者命令行中直接运行所安装的包。
`go install`命令的使用格式非常简单,就是`go install [编译参数] [待编译的包]`,其中编译参数基本上与`go build`命令共用相同的参数。
使用`go install`安装的包通常都是作为一个工具程序存在的,`go install`命令在执行的时候是无视当前目录中的`go.mod`文件的,也不会对当前目录中的`go.mod`文件做出任何修改。

48
src/basic-cmd/mod.md Normal file
View File

@ -0,0 +1,48 @@
# `go mod`命令
自从 Go 1.13 正式发布 Go Module 以后,基本上绝大多数的 Go 程序都采用了模块化的组织方式。Go Module 的核心概念就是忙完一个 Go 程序,不管是可执行的二进制程序还是一个可以被其他程序所使用的依赖包,都是以一个模块的形式存在的。
因为 Go 1.0 在发布的时候已经基本上固定了依赖包的获取和缓存等机制,所以 Go Module 也只能在这个机制上进行改进。接下来会有专门的一章来具体的对 Go Module 中一些常见的概念和应用进行说明。这里先简要的介绍一下`go mod`命令提供的功能。
`go mod`命令所提供的功能是通过它引入的另一级的子命令完成的,所以`go mod`命令的使用格式就变成了`go mod <command>`
## `go mod download`
`download`命令的主要目的是把指定的模块下载到本地缓存中,这通常都是为了提高 Go 编译速度而做的提前准备。将依赖包模块下载到本地缓存也有助于 Go 程序的离线编译。
## `go mod init`
`init`命令是一个一般情况下只会用一次的命令,它的功能就是创建一个新的 Go 模块。例如我们可以在一个空白的新目录中执行命令`go mod init .`,这样就可以在这个目录中初始化建立一个新的 Go 模块,其中会包括所需的`go.mod`、`go.sum`和`main.go`文件。
目前基本上每一个 Go 程序都是从`go mod init`命令开始的,它的命令格式为`go mod init <目标模块路径>`。如果指定的目录不存在,那么`go mod init`会自动创建一个新的目录。
## `go mod edit`
基于 Go Module 建立的程序,其模块定义和依赖包都是通过`go.mod`文件定义的,这个文件一般情况下是不建议手工修改的。但是为了有效合理的修改这个文件,`go mod`提供了一个`edit`命令。
`edit`命令的常用格式为`go mod edit [编辑内容] [go.mod位置]`。如果不提供`go.mod`文件的位置,那么`go mod edit`命令将默认修改当前目录中的`go.mod`文件。
以下是`go mod edit`命令所常用的编辑内容参数。
- `-module`,用于更改模块的路径,通常是`go.mod`文件中`module`一行的内容。
- `-require=路径@版本`,用于增加一个指定路径的模块依赖。
- `-droprequire=路径`,用于删除一个指定路径的模块依赖。
- `-exclude=路径@版本`,用于排除一个指定路径模块的依赖,被排除的内容将不会被引入到程序依赖中。注意,如果指定了排除的版本,那么依赖包的其他版本还是可以被加入到程序中的。
- `-dropexclude=路径@版本`,用于解除一个指定路径模块的依赖排除。
- `-replace=旧路径[@版本]=新路径[@版本]`,用于定义一个依赖包的替换,这种替换可以为指定的依赖包提供一个同功能的但路径不同的镜像。
- `-dropreplace=旧路径[@版本]`,用于删除一个依赖包的替换定义。
- `-retract=版本`,用于增加一个版本的撤回提醒。
- `-dropretract=版本`,用于撤回一个版本的撤回提醒。
- `-go=版本`,定义模块所期望使用的 Go 版本。
## `go mod tidy`
`tidy`命令的功能其实非常简单,它所提供的功能就是根据当前模块中代码时间引用的依赖包情况,对`go.mod`文件中列举的额外的依赖包进行清理,`tidy`命令不仅会删除当前程序中没有使用到的依赖包,还会自动添加已经在程序中使用了但是还没有被添加到`go.mod`中的依赖包。
```admonish caution
`go mod tidy`在使用的时候需要额外注意一下,如果我们提前在`go.mod`文件中声明所要依赖的依赖包,但是在程序中还没有使用到的话,此时调用`go mod tidy`命令将会使这个依赖包被清理掉。
```
## `go mod vendor`
`vendor`命令将重置模块的`vendor`目录,并将所有编译需要的依赖包放置在其中。

13
src/basic-cmd/run.md Normal file
View File

@ -0,0 +1,13 @@
# `go run`命令
`go run`命令的功能非常直接,就是编译并运行指定的包。`go run`的命令格式为`go run [编译参数] 要执行的包 [程序运行参数]`。
其实大部分情况下都是使用最简单的`go run main.go`。如果程序的运行需要参数,可以直接在指定要运行的包或者文件名后面列出即可。
```admonish tip
通过`go run`启动执行的程序,从其中通过`os.Args`获取到的程序运行参数并不会有什么特殊的变化,`os.Args[0]`依旧是程序的名字,`os.Args[1]`是第一项参数。
```
能够被运行的文件其实是有一定要求的,首先这个文件必须要隶属于`main`包,也就是在文件的开头使用`package main`声明。其次其中必须定义一个`func main()`的函数。
如果一个 Module 中所有隶属于`main`包的文件中,只存在一个`func main()`函数,那么就可以通过`go run module名称`的方式来运行。如果有多个`func main()`函数,那么就需要进行具体的指定了。