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

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

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

defer 的执行顺序

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

panicrecover

接下来,我们讨论一个稍微复杂点的机制: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 调试工具

当程序出现问题时,有时我们不希望通过修改代码来添加大量的调试信息。这时可以借助 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 获取系统信息:

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:

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 代码的建议

本文最后总结了一些实用的建议,帮助你编写高质量的 Go 代码:

  1. 当函数中出现错误时,要么记录错误,要么返回错误,不要同时做这两件事,除非有特殊理由。
  2. Go 接口定义的是行为,而不是数据。
  3. 使用 io.Readerio.Writer 接口,使代码更具扩展性。
  4. 只有在必要时才传递变量的指针,其他时候直接传递值。
  5. 错误类型不是字符串,它是 error 类型。
  6. 不要在生产环境中测试代码,除非有特殊理由。
  7. 如果不熟悉某个 Go 特性,先做测试再用,尤其是大规模应用时。
  8. 不要害怕犯错,尽量多做实验,实践是最好的学习方式。
  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值