Inspired by Tom Dalling’s classic article, this post explores how to wildly over-engineer the simple FizzBuzz problem using Go.
We’ll start with the most basic implementation and progressively refactor it to explore Go’s language features, from basic conditionals to higher-order functions and concurrency.
The output must be the classic FizzBuzz sequence from 1 to 15:
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
We start with the most straightforward approach. It gets the job done, but it requires duplicating the modulo logic for both 3 and 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)
}
}
}
To avoid duplicating the numbers 3 and 5, and the strings "Fizz" and "Buzz", we can build the resulting string sequentially. If the string remains empty after our checks, we simply output the number.
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)
}
}
Next, let’s extract the rules completely.
Note: The original article used a map[int]string for this. However, Go deliberately randomizes map iteration order. Using a map means you might occasionally print “BuzzFizz” instead of “FizzBuzz”! To fix this, we use a slice of structs to guarantee the order of execution.
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)
}
}
Instead of passing data, what if we pass behavior? We can define a Trigger type as a function that modifies a string pointer based on the current iteration.
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)
}
}
Writing out those anonymous functions is repetitive. We can DRY this up by writing a factory function that returns a function. This takes advantage of closures in Go.
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 acts as a factory, returning a custom Trigger function
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)
}
}
Finally, let’s make it idiomatic Go by utilizing concurrency. Instead of accumulating the results in a slice and returning them at the end, we can process the numbers in a goroutine and stream the results back to the main thread via a channel.
Notice the chan<- string signature in run. This correctly restricts the function to only send data to the channel, a good practice in 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 // Stream the result
}
close(out) // Always close the channel when finished
}
func create(number int, word string) Trigger {
return func(res *string, i int) {
if i%number == 0 {
*res += word
}
}
}
func main() {
// Create an unbuffered channel
results := make(chan string)
// Start the generation in a background goroutine
go run(15, results, []Trigger{
create(3, "Fizz"),
create(5, "Buzz"),
func(res *string, i int) {
if *res == "" {
*res = fmt.Sprint(i)
}
},
})
// Range over the channel to read values as they arrive
for word := range results {
fmt.Println(word)
}
}
By over-engineering a simple problem like FizzBuzz, we’ve walked through structural loops, data-driven rules (and why map iteration order matters!), closures, higher-order functions, and finally, goroutines and channels. It’s a great reminder of how many ways there are to solve a single problem in Go!
Get the curated offline edition of Kaue's Way of Lifeβa practical field guide to optimizing π Health, π° Wealth, β€οΈ Love, and π§ Meaning.
Get notified when I publish new essays, book summaries, and practical guides on living well. No spamβunsubscribe anytime.
Have a question, perspective, or suggestion? Share your thoughts below.
Updated on 2026 Oct 11.
DISCLAIMER: The content on this site is for informational and educational purposes only and does not constitute medical, psychological, financial, investment, tax, or legal advice. The ideas, curation, and opinions presented here are my own and do not represent those of my employer. Book, lecture, and article summaries are independent educational commentary and remain the intellectual property of their respective authors. AI tools may be used to assist with editing, formatting, and synthesis; while every effort is made to ensure accuracy, errors may occur. PRIVACY: This site uses third-party services for cookieless analytics (Google Analytics), optional newsletter subscriptions (Mailchimp), comments (Disqus), live chat (Tawk.to), and embedded media (YouTube), which may process visitor IP addresses and cookies in accordance with their respective privacy policies.