延迟执行 (defer
) 关键字的使用
在 Go 语言中,defer
关键字用于推迟某个函数的执行,直到其所在的外层函数即将返回时才执行。这在文件输入输出操作中非常有用,因为它允许你在打开文件后直接将关闭文件的操作放在附近,从而避免忘记关闭文件。defer
可以让你的代码更加简洁、可读。虽然在后续章节中我们将讨论 defer
在文件操作中的应用,本文先介绍 defer
在其他场景中的两种用法。
defer
的执行顺序
一个非常重要的点是,defer
语句会按照后进先出的顺序(LIFO)执行。这意味着,如果你在同一个函数中依次 defer
了 f1()
、f2()
和 f3()
,那么在函数返回时,f3()
将会先执行,接着是 f2()
,最后是 f1()
。
为了更好地理解 defer
的工作机制,下面是一个简单的 Go 代码示例:
go
package main
import (
"fmt"
)
func d1() {
for i := 3; i > 0; i-- {
defer fmt.Print(i, " ")
}
}
除了 import
块外,上面的代码实现了一个名为 d1()
的函数,其中包含一个 for
循环和一个 defer
语句。defer
将会在循环体内执行三次。
接下来是程序的第二部分:
go
func d2() {
for i := 3; i > 0; i-- {
defer func() {
fmt.Print(i, " ")
}()
}
fmt.Println()
}
在这个部分的代码中,你可以看到另一个名为 d2()
的函数实现。它同样包含一个 for
循环和一个 defer
语句,但这次 defer
应用于一个匿名函数,而不是直接调用 fmt.Print()
。匿名函数没有参数,因此每次循环都会捕获 i
的当前值。
最后一部分代码如下:
go
func d3() {
for i := 3; i > 0; i-- {
defer func(n int) {
fmt.Print(n, " ")
}(i)
}
}
func main() {
d1()
d2()
fmt.Println()
d3()
fmt.Println()
}
在这个部分,main()
函数调用了 d1()
、d2()
和 d3()
函数。在 d3()
中,匿名函数带有一个参数 n
,并且在每次 defer
时,将 i
的当前值传递给了该匿名函数。执行整个程序时,输出如下:
1 2 3
0 0 0
1 2 3
你可能觉得这个输出很难理解,因为 defer
的操作和结果可能有些让人迷惑。我们来解释一下这些输出,以帮助你更好地理解。
结果分析
首先,输出的第一行 1 2 3
是由 d1()
函数生成的。在 d1()
中,i
的值按顺序是 3、2、1,但由于 defer
的执行顺序是 LIFO,因此在 d1()
返回时,值按相反顺序输出。
接下来是由 d2()
生成的第二行输出 0 0 0
。为什么不是 1 2 3
?原因在于,for
循环结束时,i
的值为 0,而匿名函数是在 for
循环结束后才执行的,因此 i
的值为 0 时,匿名函数被执行了三次,结果是三个 0。
最后,第三行 1 2 3
是由 d3()
生成的。因为匿名函数带有参数 n
,每次 defer
时 i
的值会被传递给匿名函数,因此 defer
的匿名函数捕获了不同的 i
值,输出了正确的顺序 1 2 3
。
因此,最好的 defer
使用方法是像 d3()
那样,通过显式传递所需的参数来避免混淆。
日志中的 defer
使用
defer
还可以应用于日志记录,帮助你在程序中更好地组织日志信息。通过在函数开头和返回前分别记录开始和结束日志,你可以确保所有日志输出都是成对的。这样可以让日志信息更加清晰,易于查找。
例如,以下代码展示了如何使用 defer
记录函数的开始和结束日志:
go
package main
import (
"fmt"
"log"
"os"
)
var LOGFILE = "/tmp/mGo.log"
func one(aLog *log.Logger) {
aLog.Println("-- 函数 one 开始 --")
defer aLog.Println("-- 函数 one 结束 --")
for i := 0; i < 10; i++ {
aLog.Println(i)
}
}
这个 one()
函数使用了 defer
,确保第二个 aLog.Println()
在函数返回前被执行,因此日志输出会被封装在两个日志调用之间,使得日志信息更具可读性。
接下来是另一个类似的函数 two()
:
go
func two(aLog *log.Logger) {
aLog.Println("---- 函数 two 开始 ----")
defer aLog.Println("-- 函数 two 结束 --")
for i := 10; i > 0; i-- {
aLog.Println(i)
}
}
two()
函数也使用了 defer
来组织日志信息,这次的日志内容略有不同,但原理相同。
最后,我们看看 main()
函数的实现:
go
func main() {
f, err := os.OpenFile(LOGFILE, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Println(err)
return
}
defer f.Close()
iLog := log.New(f, "logDefer ", log.LstdFlags)
iLog.Println("程序开始!")
one(iLog)
two(iLog)
iLog.Println("程序结束!")
}
这里,我们打开了一个日志文件,并使用 defer
确保文件在程序结束时被关闭。运行这个程序并查看日志文件的内容,你会发现以下输出:
logDefer 2019/01/19 21:15:11 -- 函数 one 开始 --
logDefer 2019/01/19 21:15:11 0
logDefer 2019/01/19 21:15:11 1
...
logDefer 2019/01/19 21:15:11 -- 函数 one 结束 --
logDefer 2019/01/19 21:15:11 ---- 函数 two 开始 ----
logDefer 2019/01/19 21:15:11 10
logDefer 2019/01/19 21:15:11 9
...
logDefer 2019/01/19 21:15:11 -- 函数 two 结束 --
这样,通过 defer
,日志信息可以成对显示,使日志更加清晰,便于调试。
panic
和 recover
接下来,我们讨论一个稍微复杂点的机制:panic()
和 recover()
。panic()
是 Go 语言中的内建函数,它会中断当前程序的正常执行,并进入恐慌状态。而 recover()
则允许你在发生恐慌后重新获得控制权。
以下是一个展示这两者使用的示例:
go
package main
import "fmt"
func a() {
fmt.Println("进入 a()")
defer func() {
if c := recover(); c != nil {
fmt.Println("在 a() 中恢复!")
}
}()
fmt.Println("即将调用 b()")
b()
fmt.Println("b() 已退出!")
}
func b() {
fmt.Println("进入 b()")
panic("b() 中的恐慌!")
}
func main() {
a()
fmt.Println("main() 已结束!")
}
运行这段代码会得到以下输出:
进入 a()
即将调用 b()
进入 b()
在 a() 中恢复!
main() 已结束!
在这个例子中,b()
中调用了 panic()
,但由于 a()
中有一个 recover()
,程序得以从恐慌中恢复,并且继续执行剩下的代码。
使用 panic()
处理错误
在某些情况下,你可能只想使用 panic()
来强制终止程序。以下代码
展示了这种情况:
go
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) == 1 {
panic("参数不足!")
}
fmt.Println("感谢提供参数!")
}
当没有提供命令行参数时,程序将输出以下内容并中止:
panic: 参数不足!
panic()
是一种直接处理错误的方式,但请记住,如果不使用 recover()
,panic()
会使程序立即崩溃。
UNIX 调试工具
当程序出现问题时,有时我们不希望通过修改代码来添加大量的调试信息。这时可以借助 UNIX 下的工具,如 strace
和 dtrace
,来跟踪程序的系统调用并找出问题所在。
strace
工具
strace
是一个用于跟踪 Linux 系统中系统调用和信号的工具。你可以使用它来查看某个程序在运行时所执行的系统调用。例如,运行 strace ls
会输出如下内容:
execve("/bin/ls", ["ls"], [/* 15 vars */]) = 0
dtrace
工具
dtrace
是 macOS 和 FreeBSD 系统中的另一个强大工具,允许你监视系统中正在运行的程序而无需修改代码。例如,使用 dtruss godoc
命令可以跟踪 godoc
程序的系统调用。
检查 Go 语言环境
Go 语言提供了 runtime
包,用于查看当前 Go 环境的信息。以下代码展示了如何使用 runtime
获取系统信息:
go
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Println("使用的编译器:", runtime.Compiler)
fmt.Println("系统架构:", runtime.GOARCH)
fmt.Println("Go 语言版本:", runtime.Version())
fmt.Println("CPU 数量:", runtime.NumCPU())
fmt.Println("当前 Goroutines 数量:", runtime.NumGoroutine())
}
运行这段代码,你可以得到当前使用的编译器、系统架构、Go 版本等信息。
WebAssembly 的生成与使用
Go 支持将代码编译为 WebAssembly(Wasm),这是一种面向虚拟机的高效执行格式,适用于多种平台。以下是一个简单的 Go 代码示例,它将会被编译为 WebAssembly:
go
package main
import (
"fmt"
)
func main() {
fmt.Println("生成 WebAssembly 代码!")
}
使用以下命令将其编译为 WebAssembly:
bash
$ GOOS=js GOARCH=wasm go build -o main.wasm toWasm.go
生成的 main.wasm
文件可以在支持 WebAssembly 的浏览器中运行。你还需要加载 wasm_exec.js
文件,来帮助浏览器运行 WebAssembly。
以下是一个简单的 index.html
文件,包含用于加载和运行 WebAssembly 的代码:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Go 和 WebAssembly</title>
</head>
<body>
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
go.run(result.instance);
});
</script>
</body>
</html>
编写高质量的 Go 代码的建议
本文最后总结了一些实用的建议,帮助你编写高质量的 Go 代码:
- 当函数中出现错误时,要么记录错误,要么返回错误,不要同时做这两件事,除非有特殊理由。
- Go 接口定义的是行为,而不是数据。
- 使用
io.Reader
和io.Writer
接口,使代码更具扩展性。 - 只有在必要时才传递变量的指针,其他时候直接传递值。
- 错误类型不是字符串,它是
error
类型。 - 不要在生产环境中测试代码,除非有特殊理由。
- 如果不熟悉某个 Go 特性,先做测试再用,尤其是大规模应用时。
- 不要害怕犯错,尽量多做实验,实践是最好的学习方式。