受 Tom Dalling 经典文章的启发,本文将探讨如何使用 Go 语言对简单的 FizzBuzz 问题进行脑洞大开的“过度工程化(Over-engineering)”改造。
我们将从最基础的实现开始,逐步对其进行重构,以此探索 Go 语言的各种特性——从基础条件判断一路演进到高阶函数与并发编程。
程序必须输出从 1 到 15 的经典 FizzBuzz 序列:
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
我们从最直接的写法开始。它能顺利完成任务,但需要对 3 和 5 的取模判断逻辑进行重复编写。
package main
import "fmt"
func main() {
for i := 1; i <= 15; i++ {
if i%3 == 0 && i%5 == 0 {
fmt.Println("FizzBuzz")
} else if i%3 == 0 {
fmt.Println("Fizz")
} else if i%5 == 0 {
fmt.Println("Buzz")
} else {
fmt.Println(i)
}
}
}
为了避免重复书写数字 3 和 5 以及字符串 "Fizz" 和 "Buzz",我们可以按顺序拼接结果字符串。如果经过所有条件检查后字符串仍然为空,直接输出数字即可。
package main
import "fmt"
func main() {
for i := 1; i <= 15; i++ {
res := ""
if i%3 == 0 {
res += "Fizz"
}
if i%5 == 0 {
res += "Buzz"
}
if res == "" {
res = fmt.Sprint(i)
}
fmt.Println(res)
}
}
接下来,让我们把规则彻底抽离出来。
注意:原版文章在这里使用了哈希表(在 Go 中对应 map[int]string)。然而,Go 语言特意将 map 的遍历顺序设计为随机的。如果使用 map,你可能会时不时打印出 "BuzzFizz" 而不是 "FizzBuzz"!为了解决这个问题,我们使用结构体切片(Slice of structs)来严格保证规则的执行顺序。
package main
import "fmt"
type Rule struct {
Divisor int
Word string
}
func run(max int, rules []Rule) []string {
var all []string
for i := 1; i <= max; i++ {
res := ""
for _, rule := range rules {
if i%rule.Divisor == 0 {
res += rule.Word
}
}
if res == "" {
res = fmt.Sprint(i)
}
all = append(all, res)
}
return all
}
func main() {
rules := []Rule{
{3, "Fizz"},
{5, "Buzz"},
}
all := run(15, rules)
for _, word := range all {
fmt.Println(word)
}
}
与其传递数据,不如传递“行为”会怎样?我们可以将 Trigger 类型定义为一个函数,它会根据当前循环的迭代数字直接修改字符串指针。
package main
import "fmt"
type Trigger func(res *string, i int)
func run(max int, triggers []Trigger) []string {
var all []string
for i := 1; i <= max; i++ {
res := ""
for _, trigger := range triggers {
trigger(&res, i)
}
all = append(all, res)
}
return all
}
func main() {
all := run(15, []Trigger{
func(res *string, i int) {
if i%3 == 0 {
*res += "Fizz"
}
},
func(res *string, i int) {
if i%5 == 0 {
*res += "Buzz"
}
},
func(res *string, i int) {
if *res == "" {
*res = fmt.Sprint(i)
}
},
})
for _, word := range all {
fmt.Println(word)
}
}
手写上面那些匿名函数依然显得有些重复。我们可以编写一个返回函数的工厂函数来进一步消除重复,这充分利用了 Go 语言中的闭包(Closure)特性。
package main
import "fmt"
type Trigger func(res *string, i int)
func run(max int, triggers []Trigger) []string {
var all []string
for i := 1; i <= max; i++ {
res := ""
for _, trigger := range triggers {
trigger(&res, i)
}
all = append(all, res)
}
return all
}
// create 充当工厂函数,返回自定义的 Trigger 函数
func create(number int, word string) Trigger {
return func(res *string, i int) {
if i%number == 0 {
*res += word
}
}
}
func main() {
all := run(15, []Trigger{
create(3, "Fizz"),
create(5, "Buzz"),
func(res *string, i int) {
if *res == "" {
*res = fmt.Sprint(i)
}
},
})
for _, word := range all {
fmt.Println(word)
}
}
最后,让我们通过引入并发特性,写出最具地道 Go 语言风格(Idiomatic Go)的版本。我们不再将结果累积在切片中并在最后一次性返回,而是在独立的 Goroutine 中处理数字,并通过通道(Channel)将结果流式传输回主线程。
请注意 run 函数签名中的 chan<- string。它将通道参数正确地限制为“仅发送(send-only)”,这是 Go 语言中的一项良好工程实践。
package main
import "fmt"
type Trigger func(res *string, i int)
func run(max int, out chan<- string, triggers []Trigger) {
for i := 1; i <= max; i++ {
res := ""
for _, trigger := range triggers {
trigger(&res, i)
}
out <- res // 流式发送结果
}
close(out) // 结束时务必关闭通道
}
func create(number int, word string) Trigger {
return func(res *string, i int) {
if i%number == 0 {
*res += word
}
}
}
func main() {
// 创建一个无缓冲通道
results := make(chan string)
// 在后台 Goroutine 中启动生成过程
go run(15, results, []Trigger{
create(3, "Fizz"),
create(5, "Buzz"),
func(res *string, i int) {
if *res == "" {
*res = fmt.Sprint(i)
}
},
})
// 遍历通道,在值到达时实时读取并输出
for word := range results {
fmt.Println(word)
}
}
通过对 FizzBuzz 这样一个简单问题进行“过度设计”,我们一路体验了基础循环、数据驱动规则(以及为什么 map 遍历顺序至关重要!)、闭包、高阶函数,最后再到 Goroutine 与 Channel。这很好地提醒了我们:在 Go 语言中,解决同一个问题可以有如此丰富多彩的思路!
获取《卡乌埃的生活方式》精选离线版——一份全面优化 🍏 健康、💰 财富、❤️ 爱 与 🧘 意义 的实践指南。
当我发布关于美好生活的新随笔、读书笔记和实践指南时接收通知。绝无垃圾邮件——可随时退订。
有任何问题、见解或建议吗?欢迎在下方分享您的想法。评论功能由 GitHub Discussions 驱动(无广告、无垃圾信息)——点击下方的使用 GitHub 登录即可发表评论或互动(注册免费 GitHub 账号不到一分钟即可完成)。
最后更新于 2026年10月11日.
免责声明:本站所有内容仅供信息参考与教育用途,不构成任何医疗、心理、财务、投资、税务或法律建议。此处呈现的观点、整理与见解仅代表我个人,不代表我雇主的立场。书籍、演讲及文章摘要均为独立的教育性评论,其知识产权归原作者所有。本站可能使用 AI 工具辅助编辑、排版与归纳;尽管我们竭力确保准确无误,但仍可能存在疏漏。隐私政策:本站使用第三方服务提供可选的邮件通讯订阅(follow.it)、评论(giscus / GitHub Discussions)以及嵌入媒体(YouTube),这些服务可能会依据各自的隐私政策处理访客的 IP 地址和 Cookie。