深入理解 Go 中的 defer、panic 、日志管理与WebAssembly

延迟执行 (defer) 关键字的使用

在 Go 语言中,defer 关键字用于推迟某个函数的执行,直到其所在的外层函数即将返回时才执行。这在文件输入输出操作中非常有用,因为它允许你在打开文件后直接将关闭文件的操作放在附近,从而避免忘记关闭文件。defer 可以让你的代码更加简洁、可读。虽然在后续章节中我们将讨论 defer 在文件操作中的应用,本文先介绍 defer 在其他场景中的两种用法。

defer 的执行顺序

一个非常重要的点是,defer 语句会按照后进先出的顺序(LIFO)执行。这意味着,如果你在同一个函数中依次 deferf1()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,每次 deferi 的值会被传递给匿名函数,因此 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,日志信息可以成对显示,使日志更加清晰,便于调试。

panicrecover

接下来,我们讨论一个稍微复杂点的机制: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 下的工具,如 stracedtrace,来跟踪程序的系统调用并找出问题所在。

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 代码:

  1. 当函数中出现错误时,要么记录错误,要么返回错误,不要同时做这两件事,除非有特殊理由。
  2. Go 接口定义的是行为,而不是数据。
  3. 使用 io.Readerio.Writer 接口,使代码更具扩展性。
  4. 只有在必要时才传递变量的指针,其他时候直接传递值。
  5. 错误类型不是字符串,它是 error 类型。
  6. 不要在生产环境中测试代码,除非有特殊理由。
  7. 如果不熟悉某个 Go 特性,先做测试再用,尤其是大规模应用时。
  8. 不要害怕犯错,尽量多做实验,实践是最好的学习方式。
相关推荐
wjs2024几秒前
Git 工作区、暂存区和版本库
开发语言
Yake19652 分钟前
python 02 List
开发语言·python
一丝晨光4 分钟前
标准输入输出
java·c++·python·c#·go·c·io
Kimi-学长33 分钟前
Spring Boot 基础入门指南
java·spring boot·后端
__water36 分钟前
『网络游戏』GoLand服务器框架【01】
服务器·unity·go
微信bysj79836 分钟前
springboot网上商城源码分享
java·spring boot·后端·开源·毕业设计
骆晨学长37 分钟前
基于SPRINTBOOT+VUE文献资料检索系统
java·开发语言·spring boot·后端·spring
月落.1 小时前
C#中的报文(Message)
开发语言·c#
算法与编程之美1 小时前
通过两个类计算一个长方形的周长和面积
java·开发语言·javascript·jvm·servlet