从错误中学习: 了解Go编程的6个坏习惯

Go编程的某些实践容易被误用或忽视,了解这些特性的特点和陷阱,可以帮助我们编写更好的代码。原文: 5+ BAD Practices In Go: Learn From Mistakes

使用Go和使用其他编程语言中一样,需要了解常见错误和不良实践,才能编写既干净又高效的代码。

本文讨论的一些实践并不一定都是不好的,在特定情况下很有用。 然而,我们需要知道可能会有什么问题,为什么应该回避某些习惯,以及如何避开常见的陷阱。


1. 使用init()

Go中的init()函数是一个特殊函数,在main函数之前执行。

"如果初始化对于任何包都很重要,为什么init()在Go中被认为是一个不好的做法?"

是的,虽然init()函数确实有助于在运行核心逻辑之前进行初始化,但其执行顺序可能很难理解,可能导致对初始化顺序的混淆。

golang 复制代码
// package A
func init() {}

// package B 
func init() {}

// which run first?

想象一下,有两个模块在安装时相互依赖,但位于不同的包中。结果我们最终需要编写更复杂的代码来管理时序,更糟的是,甚至可能陷入死锁情况。

使用init()的另一个缺点是测试会变得复杂。因为这些函数是自动运行的,无法选择何时执行。

缺乏控制使得设置测试用例成为一项挑战。

我曾经遇到过一个问题,我的服务在部署后花了很长时间才准备好。我在main()函数的开始处设置了一个断点,但从未触发。

经过冗长的调试后,我们发现一个成员使用了某个包中的init()函数从一个大文件加载一个大数据集,这让我们花费大量时间去解决这么一个小问题。

2. 使用全局变量

Go中的全局变量可能会带来类似单例的问题,特别是当这些全局变量很复杂时(比如映射、切片或指针)。

"那么,全局变量有什么大不了的?"

  1. 竞争条件: 当有多个程序试图同时访问同一个全局变量时,事情可能会变得混乱。
  2. 更少的可测试性 : 应用程序依赖于全局变量,意味着有状态,从而在单元或集成测试期间,这些全局变量需要与main()函数中的内容或在生产环境中部署的内容保持一致。
  3. 模块化程度较低,可重用性较差: 可以从任何地方访问全局变量,很难跟踪其使用方式和位置。

因此,这里的建议是保持对包的封装。

从而使得代码更容易移动,并且不太可能破坏其他东西。通过避免使用全局变量,可以使代码不那么受约束,并且更容易更新或复用。

3. 忽略错误信息

用Go编程时,错误是不可避免的,知道如何处理错误可以让我们避免各种各样的问题。

"忽略错误真的那么糟糕吗?"

是的,完全正确。

一些Go新手可能会用"_"符号将错误撇在一边,但忽略函数返回的错误值,可能会带来麻烦。

如果不对错误进行管理,也许程序会出现panic和crash。

golang 复制代码
// 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",就失去了对panic的坚实保护。

为什么?

因为当出现panic时,"defer"仍然会起作用,而"recover"会抓住panic,让我们有机会处理不可预见的问题

看看这个例子,其中'file.Close()'只是放在末尾,这不是一个Go风格的解决方案:

golang 复制代码
func readFile(filename string) {
    file, err := os.Open(filename)
    if err != nil {
        panic(err)
    }

    // Do something with the file
    file.Close() // <--- DONT
}

相反,像这样使用"defer":

golang 复制代码
func readFile(filename string) {
    file, err := os.Open(filename)
    if err != nil {
        panic(err)
    }
    defer file.Close()
    // Do something with the file
    ...
}

在打开文件后立即调用defer file.Close()可以确保即使readFile()遇到panic,文件也会被关闭。此外,还可以方便的提醒我们在打开资源后立即进行清理。

6. 过多使用context.Background()

Go的context功能非常有用,当代码与数据库或网站对话时,有助于管理时间限制等事情。

如果没有设定截止时间,应用可能会陷入阻塞,被数以百万计的请求淹没。

通过一个特殊功能,可以很容易的设置时间限制。

该函数有三种时间选择: Fast(0.5秒)、Medium(3秒)和Slow(10秒)。这样就不用一直使用context.Background(),而且可以为每个任务选择合适的时间限制。

以下是Fast的一些示例代码:

golang 复制代码
const FastTimeout = 500 * time.Millisecond

func WrapCustomContext(ctx context.Context, dur time.Duration) (context.Context, context.CancelFunc) {
  return context.WithTimeout(ctx, 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)
}

有了这些函数,就可以选择正确的时间限制,应用也因此运行得更好。


好还是不好,只是一些概念,我们可以决定其真正含义。

所以,明智的使用"不好"的特性,它就能变成"最好"的方案。


你好,我是俞凡,在Motorola做过研发,现在在Mavenir做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

本文由mdnice多平台发布

相关推荐
DemonAvenger6 小时前
深入剖析 sync.Once:实现原理、应用场景与实战经验
分布式·架构·go
一个热爱生活的普通人1 天前
Go语言中 Mutex 的实现原理
后端·go
孔令飞1 天前
关于 LLMOPS 的一些粗浅思考
人工智能·云原生·go
小戴同学1 天前
实时系统降低延时的利器
后端·性能优化·go
Golang菜鸟2 天前
golang中的组合多态
后端·go
Serverless社区2 天前
函数计算支持热门 MCP Server 一键部署
go
Wo3Shi4七2 天前
二叉树数组表示
数据结构·后端·go
网络研究院2 天前
您需要了解的有关 Go、Rust 和 Zig 的信息
开发语言·rust·go·功能·发展·zig
27669582922 天前
拼多多 anti-token unidbg 分析
java·python·go·拼多多·pdd·pxx·anti-token
程序员爱钓鱼3 天前
Go 语言邮件发送完全指南:轻松实现邮件通知功能
后端·go·排序算法