从错误中学习: 了解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多平台发布

相关推荐
研究司马懿8 小时前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大1 天前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰1 天前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘1 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤1 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt112 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题6 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo