💻 过度设计的 FizzBuzz:Go 语言视角 🔗

受 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

1️⃣ 朴素实现(简单直接) 🔗

我们从最直接的写法开始。它能顺利完成任务,但需要对 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)
		}
	}
}

2️⃣ 遵循 DRY 原则(减少重复) 🔗

为了避免重复书写数字 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)
	}
}

3️⃣ 使用结构体参数化(数据驱动) 🔗

接下来,让我们把规则彻底抽离出来。

注意:原版文章在这里使用了哈希表(在 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)
	}
}

4️⃣ 使用函数参数化(闭包) 🔗

与其传递数据,不如传递“行为”会怎样?我们可以将 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)
	}
}

5️⃣ 高阶函数(触发器工厂) 🔗

手写上面那些匿名函数依然显得有些重复。我们可以编写一个返回函数的工厂函数来进一步消除重复,这充分利用了 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)
	}
}

6️⃣ 拥抱并发(Channels) 🔗

最后,让我们通过引入并发特性,写出最具地道 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。