“Bash 是一种很棒的胶水语言,但 Go 是更好的胶水。”
在日常开发中,我们常常会编写一些 Shell 脚本 来处理诸如本地环境配置、启动 Docker 容器、同步密钥等琐碎任务。起初,它们可能只是寥寥数行简单的命令;但随着时间推移和需求增加,这些脚本会逐渐膨胀成包含数百行 jq、sed、awk 命令的庞然大物,充斥着针对 macOS 和 Linux 的条件分支,以及大量“千万别动这行代码”的神秘注释。
近日,一位开发者在社区分享了他用 Go 语言 重写复杂 Bash 脚本的经历,引发了关于工程可维护性与“胶水代码”治理的深度探讨。本文将深入剖析这次从脚本到工程的“降熵”之旅,并探讨在 AI 辅助编程时代,这一选择背后的新逻辑。
Bash 脚本的“熵增”之路
许多团队的本地开发环境配置脚本,往往始于一个简单的需求:例如,从 AWS SSM 或 Vault 拉取密钥,生成 .env 文件,然后启动服务。
最初的 Bash 脚本可能只有 10 行。但随着时间的推移,它会逐渐演变成这样:
- 工具链依赖地狱:脚本严重依赖特定版本的
sed、grep 或 jq。一旦有同事更新了系统工具链,脚本就可能无法运行。
- 跨平台噩梦:
sed 等命令在 macOS 和 Linux 上的行为存在差异,导致脚本中充斥大量 if [[ “$OS” == “darwin” ]] 的条件分支。
- 调试困难:当脚本出错时,很难快速定位是哪一行的管道(pipe)出了问题,缺乏类型检查使得潜在错误难以被发现。
正如一位开发者所言:“Bash 脚本就像是一堆没有明确所有权的‘杂物’。每个人都在上面打补丁,直到它变成一个没人敢碰的定时炸弹。”
Go 作为“强力胶水”的优势
原作者将这套复杂的 Bash 逻辑重构为一个名为 envmap 的小型 Go CLI 工具。虽然代码行数可能有所增加(Go 语法确实比 Bash 更繁琐),但他获得了工程质量的质变:
结构化配置与类型安全
告别脆弱的字符串解析。配置被定义为强类型的 struct,编译器会在编译阶段检查拼写错误和类型不匹配。
// Bash: 祈祷这个字符串解析是对的...
// Go: 编译器保证它是对的
type Config struct {
Env string `json:"env"`
Region string `json:"region"`
UseVault bool `json:"use_vault"`
}
接口抽象与可测试性
原作者定义了一个 Provider 接口来抽象不同的密钥后端(如 AWS SSM, Vault, 本地文件)。这不仅让代码结构更清晰,更重要的是,它变得可测试了。可以轻松编写单元测试来验证核心逻辑,而无需真正连接到 AWS 或 Vault 服务。
type Provider interface {
Get(ctx context.Context, key string) (string, error)
// ...
}
跨平台的一致性
Go 编译出的静态二进制文件,彻底解决了“它在我的机器上能跑”的问题。无论团队成员使用的是 macOS、Linux 还是 Windows,运行的都是完全相同的、行为一致的可执行程序,不再受系统自带 Shell 工具版本差异的影响。
社区的思辨——这是“杀鸡用牛刀”吗?
这次重构也引发了社区的激烈讨论。有开发者质疑:用 Go 来写这类脚本是否过于“重量级”?Python 或 TypeScript 岂不是更轻量的选择?甚至,为什么不继续使用 Makefile?
反方观点:复杂度的转移
- “代码量增加”:Go 的语法确实相对繁琐。一个简单的
cp a b 操作,在 Go 中需要写更多代码。
- “引入编译步骤”:虽然
go run 命令很快,但毕竟多了一个编译环节,不如直接运行脚本直观。
正方观点:维护性的胜利
- “长期收益显著”:一位开发者分享了他将 4 万行 Bash/Perl 脚本重构为 1 万行 Go 代码的经历。虽然初期投入较大,但换来了完整的测试覆盖、清晰的文档以及零依赖的便捷部署。
- “显式契约优于隐式约定”:Bash 脚本之间通常通过不稳定的文本流(stdout/stdin)进行通信,这种方式非常脆弱。而 Go 代码之间通过明确的接口和模块进行调用,通信方式更加稳健可靠。
正如一位评论者总结的:“如果你只是写一个 10 行的、一次性的脚本,Bash 是完美的选择。但如果你的脚本开始需要处理复杂的逻辑、状态和错误处理,那么它就不再是一个简单的‘脚本’,而是一个程序。既然是程序,就应该使用适合编写程序的语言(如 Go)来构建。”
AI 时代的变量——“繁琐”不再是借口
在过去,阻碍开发者用 Go 替代 Bash 的最大阻力往往是编写效率。花时间写几十行 Go 代码来替换一行 sed 命令,听起来既繁琐又低效。
然而,在 AI 辅助编程工具(如 Copilot, Cursor, Claude Code 等)日益普及的今天,这个天平正在发生倾斜。
AI 为 Go 支付了“样板税”
Go 语言的一些特性——例如显式的错误处理、结构体定义、库引入——曾经是手写代码时的负担。但在 AI 时代,这些标准化的、模式固定的样板代码恰恰是LLM(大语言模型)最擅长生成的部分。
你只需要向 AI 描述需求:“编写一个 CLI 工具,读取环境变量,请求 AWS SSM 获取密钥,如果出错则打印红色日志。” AI 几乎能瞬间生成 80% 的 Go 代码骨架。开发者后续只需专注于核心业务逻辑的微调和验证。
编译器是 AI 最好的“质检员”
使用 AI 生成 Bash 脚本有时像一场赌博。LLM 可能会编造出不存在的 awk 参数,或者写出在某些 Shell 环境下不兼容的语法,这些错误往往要到运行时才能发现,甚至可能引发严重后果。
相比之下,使用 AI 生成 Go 代码则拥有一道天然的安全屏障:
- 静态类型检查:如果 AI“幻觉”出了不存在的方法或属性,编译器会立刻报错,而不是等到程序运行时崩溃。
- 确定性高:Go 语言的语法规范极其严格统一,这大大减少了 AI 生成“虽然能运行但逻辑怪异”代码的概率。
正如原作者在回复中所说:“我使用了 Cursor 和 Codex 等工具,代码的复杂性主要来自业务逻辑本身,而非语言特性。”在 AI 的辅助下,获得一个类型安全、跨平台、易于维护的 Go 二进制文件,其整体开发效率已经与编写、调试一个脆弱的 Bash 脚本不相上下,甚至更具优势。
小结:从脚本到工程,从手写到与AI协同
这个案例揭示了一个重要观点:“胶水代码”同样需要工程化的治理。
当你的 Bash 脚本开始变得让你感到恐惧、难以测试和维护时,或许就是考虑用 Go 重写它的时机。虽然你可能需要多写一些 if err != nil 这样的错误处理代码,但你换来的是行为的确定性、长期的可维护性以及内心的宁静。
特别是在 AI 与云原生 时代,Go 语言所谓的“繁琐”已被智能编程助手极大消解,而它所带来的“稳健”与“工程友好”特性却愈发珍贵。Go 或许不是语法最简洁的“胶水”,但在 AI 的赋能下,它正成为性价比极高、连接最为牢固的工程化胶水。
资料参考:https://www.reddit.com/r/golang/comments/1pb7t1q/show_tell_bash_is_great_glue_go_is_better_glue/