defer
) 关键字的使用在 Go 语言中,defer
关键字用于推迟某个函数的执行,直到其所在的外层函数即将返回时才执行。这在文件输入输出操作中非常有用,因为它允许你在打开文件后直接将关闭文件的操作放在附近,从而避免忘记关闭文件。defer
可以让你的代码更加简洁、可读。虽然在后续章节中我们将讨论 defer
在文件操作中的应用,本文先介绍 defer
在其他场景中的两种用法。
defer
的执行顺序一个非常重要的点是,defer
语句会按照后进先出的顺序(LIFO)执行。这意味着,如果你在同一个函数中依次 defer
了 f1()
、f2()
和 f3()
,那么在函数返回时,f3()
将会先执行,接着是 f2()
,最后是 f1()
。
为了更好地理解 defer
的工作机制,下面是一个简单的 Go 代码示例:
package main import ( "fmt" ) func d1() { for i := 3; i > 0; i-- { defer fmt.Print(i, " ") } }
除了 import
块外,上面的代码实现了一个名为 d1()
的函数,其中包含一个 for
循环和一个 defer
语句。defer
将会在循环体内执行三次。
接下来是程序的第二部分:
func d2() { for i := 3; i > 0; i-- { defer func() { fmt.Print(i, " ") }() } fmt.Println() }
在这个部分的代码中,你可以看到另一个名为 d2()
的函数实现。它同样包含一个 for
循环和一个 defer
语句,但这次 defer
应用于一个匿名函数,而不是直接调用 fmt.Print()
。匿名函数没有参数,因此每次循环都会捕获 i
的当前值。
最后一部分代码如下:
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
记录函数的开始和结束日志:
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()
:
func two(aLog *log.Logger) { aLog.Println("---- 函数 two 开始 ----") defer aLog.Println("-- 函数 two 结束 --") for i := 10; i > 0; i-- { aLog.Println(i) } }
two()
函数也使用了 defer
来组织日志信息,这次的日志内容略有不同,但原理相同。
最后,我们看看 main()
函数的实现:
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()
则允许你在发生恐慌后重新获得控制权。
以下是一个展示这两者使用的示例:
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()
来强制终止程序。以下代码
展示了这种情况:
package main import ( "fmt" "os" ) func main() { if len(os.Args) == 1 { panic("参数不足!") } fmt.Println("感谢提供参数!") }
当没有提供命令行参数时,程序将输出以下内容并中止:
panic: 参数不足!
panic()
是一种直接处理错误的方式,但请记住,如果不使用 recover()
,panic()
会使程序立即崩溃。
当程序出现问题时,有时我们不希望通过修改代码来添加大量的调试信息。这时可以借助 UNIX 下的工具,如 strace
和 dtrace
,来跟踪程序的系统调用并找出问题所在。
strace
工具strace
是一个用于跟踪 Linux 系统中系统调用和信号的工具。你可以使用它来查看某个程序在运行时所执行的系统调用。例如,运行 strace ls
会输出如下内容:
execve("/bin/ls", ["ls"], [/* 15 vars */]) = 0
dtrace
工具dtrace
是 macOS 和 FreeBSD 系统中的另一个强大工具,允许你监视系统中正在运行的程序而无需修改代码。例如,使用 dtruss godoc
命令可以跟踪 godoc
程序的系统调用。
Go 语言提供了 runtime
包,用于查看当前 Go 环境的信息。以下代码展示了如何使用 runtime
获取系统信息:
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 版本等信息。
Go 支持将代码编译为 WebAssembly(Wasm),这是一种面向虚拟机的高效执行格式,适用于多种平台。以下是一个简单的 Go 代码示例,它将会被编译为 WebAssembly:
package main import ( "fmt" ) func main() { fmt.Println("生成 WebAssembly 代码!") }
使用以下命令将其编译为 WebAssembly:
$ GOOS=js GOARCH=wasm go build -o main.wasm toWasm.go
生成的 main.wasm
文件可以在支持 WebAssembly 的浏览器中运行。你还需要加载 wasm_exec.js
文件,来帮助浏览器运行 WebAssembly。
以下是一个简单的 index.html
文件,包含用于加载和运行 WebAssembly 的代码:
<!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 代码:
io.Reader
和 io.Writer
接口,使代码更具扩展性。error
类型。- EOF -