从他人的错误中学习,通过本指南避免常见陷阱和坏习惯,提高你的 Go 编程技巧
在 Go 语言中,就像在任何编程语言中一样,了解常见陷阱和坏习惯是编写干净、高效代码的关键。
尽管下面列出的某些做法通常被认为是不好的,但在某些情况下它们可以有效地使用。 这篇文章旨在提醒大家这些做法的问题所在,并教导如何避免这些陷阱。
让我们深入探讨。
1. 使用 init()
在 Go 中,init()
函数是在主函数之前执行的特殊函数。
“如果在任何包中初始化是如此重要的过程,为什么在 Go 中
init()
被认为是一种不好的实践?” —— 读者
是的,虽然init()
函数有助于在运行核心逻辑之前进行初始化,但它们的执行顺序可能难以理解。 这可能会导致关于初始化顺序的混乱。
如果两个模块互相依赖于初始化并位于不同的包中,则可能会增加复杂性并需要额外的代码添加等待逻辑。然而,这也可能导致死锁的可能性。
另一个init()
函数的问题是它会使测试更加困难。因为它们自动运行,很难控制它们何时执行,这可能使设置测试用例和测试代码行为变得具有挑战性。
我遇到了一个问题,我的服务从部署状态到准备就绪需要 10 分钟的时间。我在主函数的第一行设置了断点,但它从未触发。
我们不得不调试所有的
init()
函数,发现一个队友在一个“我不记得的包”中使用了init()
函数,从一个大文件加载了大量数据到内存中,这导致花费了很多时间追踪一个微小的问题。
2. 使用全局变量
这与使用单例模式时可能出现的问题类似,特别是当全局变量是复杂的,包含映射、切片或指针时。
- 竞态条件(Race condition):当使用全局变量时,多个goroutine同时访问全局变量会导致意外行为。这在Go语言中是一个很大的问题。
- 难以测试:使用全局变量会使你的项目更具状态,这意味着当你开始单元测试/集成测试时,全局变量必须与运行main()或生产环境时相同。
- 不够模块化且难以重用:难以组织和封装数据,因为任何包或模块都可以访问它们。这可能会导致代码不够模块化,更难以理解,因为很难确定数据来自哪里以及如何使用它们。
通常建议封装您的包,使其可以在不影响其他包的情况下移动。使用全局变量可能会使您的代码更紧密耦合,更难以修改或重用。
3. 忽略错误消息
错误是Go编程的内在部分,处理它们以优雅的方式确保在发生错误时不会发生意外情况非常重要。
忽略错误消息的方法是使用“_”符号,这样做会丢弃函数返回的错误值,可能会导致意外行为。
检查错误并适当地处理它们以防止程序发生崩溃和崩溃非常重要。
// sample 1
func main() {
var x interface{} = "hello"
s := x.(int) // panic: interface conversion: interface {} is string, not int
fmt.Println(s)
}
// sample 2
func main() {
var x interface{} = "hello"
s, _ := x.(int) // safe but DON'T
fmt.Println(s)
}
忽略错误处理会导致生产代码中出现重大问题,因为这可能使得识别和修复错误变得困难。始终检查错误并适当地解决它们对于确保代码的顺畅运行非常重要。
4. GOTO语句 — 跳进陷阱
不仅在Go语言中,许多语言都认为使用“goto”语句是一种不良实践,因为它会使代码更难理解和维护。
原因是“goto”语句忽略了代码流程,使得在不引入错误的情况下难以理解代码的不同部分之间的依赖关系。
这可能会使得在运行时推理程序的状态变得困难,使得调试和测试变得更加困难。
使用“goto”可能会导致错误数量增加,使得更难以识别问题的根本原因。
5. 不使用Defer和Recover
利用“defer”和“recover”的主要原因是为了防止恐慌。 “defer”甚至可以在发生恐慌时执行,而“recover”可以捕捉到恐慌,允许更加控制地处理意外情况。
这种使用方式被认为是不好的代码实践:
func readFile(filename string) {
file, err := os.Open(filename)
if err != nil {
panic(err)
}
// Do something with the file
file.Close() // <--- DONT
}
我们使用defer
这种方式,这样即使readFile()
函数出现panic,文件也会被关闭。此外,这样做也容易记住在open()
函数之后立即放置关闭函数。
func readFile(filename string) {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// Do something with the file
...
}
6. 使用太多次的context.Background()
在 Go 中,上下文是最强大的功能之一。当正确使用时,它可以作为提供程序、树流和流控制器。
在进行外部调用(例如数据库、HTTP 等)时,使用context.Background()
或context.ToDo()
设置截止时间或超时非常重要。如果没有设置,当用户过多且外部服务没有及时响应时,可能会导致您的应用程序出现瓶颈。
我通常会编写一个上下文池的实用函数,其中包括 3 个函数来创建或包装具有超时的新上下文:快速(0.5 秒)、中等(3 秒)、慢速(10 秒)。这样,我就不必一直依赖context.Background()
,并且可以轻松地为每个调用设置适当的超时时间。
const FastTimeout = 500 * time.Millisecond
func WrapCustomContext(ctx context.Context, dur time.Duration) (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), dur)
}
func GenFastContext() (context.Context, context.CancelFunc) {
return WrapCustomContext(context.Background(), FastTimeout)
}
func WrapFastContext(ctx context.Context) (context.Context, context.CancelFunc) {
return WrapCustomContext(ctx, FastTimeout)
}
继续阅读
如果你想跟上 Golang 领域的最新动态,请关注我。我会确保让你掌握最新情况!
继续学习,享受编程的乐趣,愉快的编码!
网友评论