第一部分
ch03 适用 Go 工具
本章介绍 Go 开发工具, 以及 Go 项目的基本结构, 以及如何编译与执行 Go 代码, 然后介绍如何安装与使用 Go 调试工具, 以及 lint 和格式化工具.
作者提示可以下载示例代码: https://github.com/apress/pro-go
使用 Go 命令
go 命令提供了所有本书需要的有关编译与运行的所有功能. 使用 go 命令参数的形式来提供. go 命令包含很多参数, 常用的如下:
参数 | 描述 |
---|---|
build | go build 命令会在当前目录构建源代码, 并生成可执行文件. 细节会在"编译并运行源代码"一节进行介绍. |
clean | go clean 清理由 go build 生成的内容, 包括可执行程序, 编译过程中临时生成的文件. 细节会在 "编译与运行源代码" 一节介绍. |
doc | go doc 会从源代码中生成文档. 参考 "Linting Go Code" 一节中的示例. |
fmt | go fmt 用于确保源代码中的缩进与对齐一致. 在"格式化 Go 代码"一节中会介绍. |
get | go get 用于下载和安装扩展包, 会在 ch12 中介绍. |
install | go install 命令用于下载包, 它一般用于安装工具包. 在 "调试 Go 代码" 一节会介绍. |
help | go help 用于显示其他特性的帮助信息. 例如 go help build 会列出所有编译的参数. |
mod | go mod 命令用于创建并管理 Go 模块. 第12章 定义模块一节中会有介绍. |
run | go run 命令会在不创建可执行文件的情况执行代码. 细节会在 "使用 Go Run 命令" 一节中介绍. |
test | go test 用于执行单元测试. 会在第 31 章介绍. |
version | go version 用于打印版本号. |
vet | go vet 用于查询 Go 代码中的公共问题. "在 Go 代码中固定公共问题" 一节中会有介绍. |
创建 Go 项目
go 项目创建并不麻烦, 并且很简单.
- 打开命令行, 创建文件夹
tools
- 在文件夹中创建文件
main.go
- 然后编写代码
package main
import "fmt"
func main() {
fmt.Println("Hello. Go")
}
作者说代码细节后面会介绍. 下图展示了 main.go
文件的核心组成
- 包声明
- 导入语句
- 函数
理解包声明
第一句话是包声明, 包是对一类功能的分组, 每一个代码都需要声明包, 表示它属于哪一类. 其语法是使用 package
关键字, 然后跟上包名. 这里包名为 main
.
理解导入语句
第二句话是导入语句, 用于导入需要依赖的其他包. 然后作者描述了一下语法: import
加上双引号括起来的包名. fmt
用于读写格式化的字符串. 细节会在 17 章介绍.
所有的包可以在 https://golang.org/pkg 查看.
理解函数
main.go
文件剩下的代码定义了一个名为 main
的函数. 函数的细节会在 第8章 讨论. 但是 main
函数比较特殊. 当你在名为 main
包中定义了 main
函数, 那么你就定义了命令行程序的入口.
然后作者介绍了一下函数的语法, 并说明这里的函数很简单, 但足够解释现在的作用. 并解释函数中代码的作用, 以及什么时候会执行.
理解代码语句 (Code Statement)
函数中只有一句话, 作者解释了如何执行包中的代码, 解释了包名的含义, 以及基本语法.
在 Go 代码中使用分号
Go 中分号的作用与 JS 一样, 表示语句结束, 但可以不写. 逻辑上与 JS 相同, 一行结束表示语句结束.
Go 代码的风格最好是遵守, 常见的一个错误是将开始的花括号另起一行来编写. 因为逻辑上会在函数后的圆括号处自动加上分号.
编译与运行源代码
使用 go build
命令来编译代码, 并生成可执行程序. 在 tools
目录中执行下面命令来编译代码:
go build main.go
windows 上会生成 main.exe
文件, linux 上则是 main
. 使用 ./main
可以执行.
配置 Go 编译器
可以通过参数来配置 Go 编译器. 虽然默认已经足够使用. 常用的参数有 -a
, 表示无论文件是否发生改变都重新编译, 另一个是 -o
表示编译后的输出名. 可以使用 go help build
查看详情. 编译后的输出可以有不同结果, 细节可以参考: https://pkg.go.dev/cmd/go#hdr-Build_modes
清空 (Clean Up)
移除编译过程中生成的内容运行下面的命令:
go clean main.go
使用 Go Run 命令
大多数开发会使用 go run
命令.
go run main.go
该命令会在临时目录中生成需要的文件. 从命令运行上该命令只需要一步即可完成编译到运行的过程.
注意涉及防火墙的时候, 由于每次都是在一个新的临时文件中生成编译的内容, 所以可能会频繁提示防火墙的提示.
定义模块
前面的代码仅表示创建了 go 代码, 然后编译运行. 但是通常不会这么做. 一般都是先创建一个 Go 模块. 创建 Go 模块可以简单的使用第三方模块, 也可以简化构建过程.
在 tools
目录下执行:
go mod init tools
该命令会在 tools
文件夹中创建 go.mod
文件. 使用模块, 可以简化构建过程. 使用点表示当前目录, 来进行构建, 而不用指定文件来构建.
go run .
go.mod
还有其他用处, 后面会介绍到. 作者全书的代码都会使用 go.mod
.
调试 Go 代码
go 应用的标准调试工具名为 Delve. 它是一个第三方工具, 但是对 Go 支持的很好, 也被 Go 开发团队所推荐. 它支持主流 OS. 安装使用命令:
go install github.com/go-delve/delve/cmd/dlv@latest
细节可以参考: https://github.com/go-delve/delve/tree/master/Documentation/installation.
安装需要 梯子
go install
命令用于下载并安装一个包. 它会安装诸如 debugger
的包. 还有另一个类似的命令 go get
, 也执行类似的包任务. 细节会在 第12章介绍.
要明确安装成功与否, 执行命令
dlv version
默认情况下, 程序会被安装到 ~/go/bin
目录下. 这个目录由环境变量 $GOPATH
来控制.
未找到该命令可以使用
~/go/bin/dlv version
使用 PRINTLN
函数来调试
作者喜欢 Delve, 但是只有在 Println
无法解决问题时才会使用它, 因为简单输出够快, 够方便. 另外作者表示不要拘泥于某个工具, 灵活简单的使用比较重要.
为调试做准备
package main
import "fmt"
func main() {
fmt.Println("Hello. Go")
for i := 0; i < 5; i++ {
fmt.Println(i)
}
}
语句会在第5章说明, 这里仅为演示调试时怎么进行的. 编译运行代码, 使用 go run .
使用 Debugger
运行代码
dlv debg main.go
第一感觉有点像
gdb
该工具是基于文本的调试客户端, 开始会比较麻烦, 需要熟练. 进入调试后首先是设置断点:
break bp1 main.main:3
该命令设置了一个断点, 并对其命名, 设置断点的位置使用 包名.函数名:行数
的形式.
一直比较觉得
gdb
很麻烦, 这下好了, 还是得弄.
设置断点后, 命令行也会返回设置好的提示信息. 形如下面的文本:
(dlv) break bp1 main.main:3
Breakpoint bp1 set at 0x90da35 for main.main() C:/Users/jk/Desktop/tools/main.go:6
下面创建条件断点. 编写下面代码, 然后回车
condition bp1 i == 2
作者简要解释了一下条件的作用与什么是条件断点.
要开始运行代码, 输入下面命令, 然后回车
continue
首先是代码的输出, 然后会显示代码所停的位置.
debugger 提供了一整套检查与修改应用程序状态的命令. 最常用的如下表:
完整信息可以参考: https://github.com/go-delve/delve
命令 | 描述 |
---|---|
print <expr> | 计算表达式值, 并打印输出. |
set <variable> = <value> | 用于修改变量值. |
locals | 用于输出所有局部变量的值. |
whatis <expr> | 用于判断表达式最终结果的类型. 类型在 第4章介绍. |
然后作者为了简要演示, 执行了命令 print i
, 输出了变量 i
的值.
debugger 还提供了一套控制流程的命令, 最常用的如下表:
命令 | 描述 |
---|---|
continue | 恢复程序的运行. |
next | 执行下一条命令. |
step | 进入当前语句. |
stepout | 从当前语句中移除. |
restart | 重置执行过程, 使用 continue 来开始运行. |
exit | 退出调试模式. |
作者简要演示了一下其使用.
逻辑上
next
就是下一步 (逐过程)
step
就是逐语句, 会进入到函数内部, 要跳出则使用stepout
.
使用 Delve 编辑器插件
Delve 也提供了编辑器插件, 以供基于 UI 的调试. 但是 VSCode 的插件更好用. 在安装 Go 语言工具时会自动安装.
自己演示的时候, 似乎使用的依旧是
delv
, 只需要F5
即可
Linting Go 代码
linter 是一个检查代码的工具. 利用一组规则来进行检查. 这些规则描述了可能导致混淆的问题, 导致异常的结果, 以及降低代码可读性的问题. Go 中广泛使用的 linter 是 golint
. 其规则来源于两个地方.
- 一个是 Google 提供的高效文档 (https://go.dev/doc/effective_go), 有助于编写简洁干净的代码.
- 另一个是注释集 (https://github.com/golang/go/wiki/CodeReviewComments)
golint
的问题是无配置, 它是要么不起作用, 要么全部生效. 如此, 常常会混淆一些应该关注的问题. 对于初学者, 容易望而生畏, 从而放弃该语言. 所以作者比较乐意实用 revive
包. 该包可直接替代 golint
. 其特点是可以控制启用什么规则. 安装包的命令:
go install github.com/mgechev/revive@latest
Linting 的优缺点
在开发人员参差不齐的时候非常有用.
但是不同开发人员有不同的开发习惯.
不用完全依赖于 lint, 可以适当灵活一些.
使用 Linter
main.go
代码太简单, 扩充一下代码
package main
import "fmt"
func main() {
PrintHello()
for i := 0; i < 5; i++ {
PrintNumber(i)
}
}
func PrintHello() {
fmt.Println("Hello. Go")
}
func PrintNumber(number int) {
fmt.Println(number)
}
然后运行 revive
细节会在 第12 章介绍
Go 中约定首字母大写的函数是导出函数, 而导出函数是需要提供注释的. 给 PrintNumber
添加注释:
package main
import "fmt"
func main() {
PrintHello()
for i := 0; i < 5; i++ {
PrintNumber(i)
}
}
func PrintHello() {
fmt.Println("Hello. Go")
}
// 该函数使用 fmt.Println 来打印一个数字
func PrintNumber(number int) {
fmt.Println(number)
}
再次运行 revive
, 会得到不一样的错误:
有些 Linter 约定了一些必须的结构. 如:https://golang.org/doc/effective_go.html#commentary 中描述的, 约定需要以函数名开头, 并描述函数的作用.
package main
import "fmt"
func main() {
PrintHello()
for i := 0; i < 5; i++ {
PrintNumber(i)
}
}
func PrintHello() {
fmt.Println("Hello. Go")
}
// PrintNumber 数使用 fmt.Println 来打印一个数字
func PrintNumber(number int) {
fmt.Println(number)
}
::tip 理解 Go 文档
之所以对注释要求这么严格, 因为 go doc
可以从注释中生成文档. https://blog.golang.org/godoc 中介绍了该命令的用法, 也可以适用 go doc -all
来快速展示其用法.
:::
禁用 Linter 规则
可以在代码中使用注释直接配置 revive
, 来对某些代码禁用规则.
package main
import "fmt"
func main() {
PrintHello()
for i := 0; i < 5; i++ {
PrintNumber(i)
}
}
// revive:disable:exported
func PrintHello() {
fmt.Println("Hello. Go")
}
// revive:disable:exported
// PrintNumber 数使用 fmt.Println 来打印一个数字
func PrintNumber(number int) {
fmt.Println(number)
}
该语法的形式是固定的, 使用冒号分隔. revive
加上 enable
或 disable
, 然后是可选的规则. 可用规则可以参考: https://github.com/mgechev/revive#available-rules.
禁用包注释可以使用
package-comments
规则.
使用 Linter 配置文件
为了整个项目, 使用配置文件更简洁. 配置文件使用 TOML 格式.
添加文件 revive.toml
TOML 格式可以参考: https://toml.io/en
细节可以参考: https://github.com/mgechev/revive#configuration
ignoreGeneratedHeader = false
severity = "warning"
confidence = 0.8
errorCode = 0
warningCode = 0
[rule.blank-imports]
[rule.context-as-argument]
[rule.context-keys-type]
[rule.dot-imports]
[rule.error-return]
[rule.error-strings]
[rule.error-naming]
#[rule.exported]
[rule.if-return]
[rule.increment-decrement]
[rule.var-naming]
[rule.var-declaration]
[rule.package-comments]
[rule.range]
[rule.receiver-naming]
[rule.time-naming]
[rule.unexported-return]
[rule.indent-error-flow]
[rule.errorf]
这是推荐使用的规则 https://github.com/mgechev/revive#recommended-configuration
简单来说, 需要的规则写进去, 不需要的注释掉即可.
然后移除代码中的注释, 在运行 revive
revive -config revive.toml
至此不会显示任何警告.
代码编辑器中的 LINTING