当前位置: 首页 > news >正文

深入理解 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. 不要害怕犯错,尽量多做实验,实践是最好的学习方式。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • C2 Magic 附工具下载,供学习使用
  • pyspark.sql.types
  • 想了解ECM衍生材料?看这里,从提取到应用!
  • Flask session cookie 失效在Safari中的解决方法
  • web前端-HTML常用标签(二)
  • 代理伺服器地址怎麼正確填寫-okeyproxy
  • 零基础国产GD32单片机编程入门(十九)红外避障传感器模块实战含源码
  • Android视频编辑:利用FFmpeg实现高级功能
  • LVM逻辑卷创建的完整过程
  • python-月份有几天
  • Win使用SSH
  • k8s Prometheus
  • flask下https教程
  • OpenGL Texture C++ 预览Camera视频
  • 一分钟了解网络安全风险评估!
  • 【刷算法】求1+2+3+...+n
  • Angular 响应式表单 基础例子
  • docker python 配置
  • JDK 6和JDK 7中的substring()方法
  • JS进阶 - JS 、JS-Web-API与DOM、BOM
  • mysql常用命令汇总
  • vue-cli3搭建项目
  • Vultr 教程目录
  • 从零到一:用Phaser.js写意地开发小游戏(Chapter 3 - 加载游戏资源)
  • 从零开始的无人驾驶 1
  • 极限编程 (Extreme Programming) - 发布计划 (Release Planning)
  • 聚簇索引和非聚簇索引
  • 日剧·日综资源集合(建议收藏)
  • 深度学习入门:10门免费线上课程推荐
  • 微信小程序实战练习(仿五洲到家微信版)
  • 译有关态射的一切
  • 没有任何编程基础可以直接学习python语言吗?学会后能够做什么? ...
  • ​ArcGIS Pro 如何批量删除字段
  • (003)SlickEdit Unity的补全
  • (14)Hive调优——合并小文件
  • (52)只出现一次的数字III
  • (C++哈希表01)
  • (CVPRW,2024)可学习的提示:遥感领域小样本语义分割
  • (function(){})()的分步解析
  • (ZT)一个美国文科博士的YardLife
  • (每日持续更新)jdk api之FileFilter基础、应用、实战
  • (七)Knockout 创建自定义绑定
  • (一)kafka实战——kafka源码编译启动
  • (转)四层和七层负载均衡的区别
  • .Net Core webapi RestFul 统一接口数据返回格式
  • .NET Core 项目指定SDK版本
  • .NET 设计模式—适配器模式(Adapter Pattern)
  • .NET 使用 XPath 来读写 XML 文件
  • .NET 通过系统影子账户实现权限维持
  • .NET/MSBuild 中的发布路径在哪里呢?如何在扩展编译的时候修改发布路径中的文件呢?
  • .net6Api后台+uniapp导出Excel
  • .Net接口调试与案例
  • .Net中wcf服务生成及调用
  • .pyc文件是什么?
  • .sh 的运行