写点什么

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

作者:俞凡
  • 2023-11-26
    上海
  • 本文字数:2480 字

    阅读完需:约 8 分钟

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


Michal Matlon @Unsplash


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


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



1. 使用 init()

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


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


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


// package Afunc init() {}
// package B func init() {}
// which run first?
复制代码


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


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


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


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

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

2. 使用全局变量

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


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


  1. 竞争条件: 当有多个程序试图同时访问同一个全局变量时,事情可能会变得混乱。

  2. 更少的可测试性: 应用程序依赖于全局变量,意味着有状态,从而在单元或集成测试期间,这些全局变量需要与main()函数中的内容或在生产环境中部署的内容保持一致。

  3. 模块化程度较低,可重用性较差: 可以从任何地方访问全局变量,很难跟踪其使用方式和位置。


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


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

3. 忽略错误信息

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


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


是的,完全正确。


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


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


// sample 1func main() {  var x interface{} = "hello"  s := x.(int) // panic: interface conversion: interface {} is string, not int  fmt.Println(s)}
// sample 2func 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 风格的解决方案:


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


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


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 的一些示例代码:


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",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

发布于: 刚刚阅读数: 3
用户头像

俞凡

关注

公众号:DeepNoMind 2017-10-18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
从错误中学习: 了解Go编程的6个坏习惯_golang_俞凡_InfoQ写作社区