💻 細かすぎるFizzBuzz:Go言語の視点から徹底解剖 🔗

Tom Dalling氏の名記事に着想を得て、この記事ではシンプルなFizzBuzz問題をGoを使って「どこまで過剰設計(オーバーエンジニアリング)できるか」を探求します。

最も基本的な実装からスタートし、基本的な条件分岐から高階関数、そして並行処理(コンカレンシー)に至るまで、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️⃣ 構造体によるパラメータ化(データ駆動) 🔗

次に、ルールを完全に外部へ抽出してみましょう。

注:元の記事ではこの部分に map[int]string に相当するマップを使っていました。しかし、Goはマップの反復順序を意図的にランダム化します。そのため、マップを使うと "FizzBuzz" ではなく "BuzzFizz" と出力されてしまうことがあります!これを防ぐため、ここでは構造体のスライスを使って実行順序を保証しています。

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️⃣ 高階関数(Triggerのファクトリ関数) 🔗

先ほどのように無名関数を毎回書き出すのは冗長です。「関数を返す」ファクトリ関数を書くことで、ここもDRYにできます。これは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 はファクトリとして機能し、カスタムの 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️⃣ 並行処理の導入(チャネル) 🔗

最後に、並行処理(コンカレンシー)を取り入れて、よりGoらしい(イディオマティックな)コードにしてみましょう。結果をスライスに溜め込んで最後にまとめて返すのではなく、ゴルーチン(goroutine)の中で数値を処理し、チャネルを介してメインスレッドへ結果をストリーミングします。

run 関数のシグネチャにある chan<- string に注目してください。これにより、この関数はチャネルへのデータ送信のみに制限されます。これは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)
	
	// バックグラウンドのゴルーチンで生成処理を開始する
	go run(15, results, []Trigger{
		create(3, "Fizz"),
		create(5, "Buzz"),
		func(res *string, i int) {
			if *res == "" {
				*res = fmt.Sprint(i)
			}
		},
	})
	
	// チャネルを range で回し、値が届き次第読み取る
	for word := range results {
		fmt.Println(word)
	}
}

🏁 まとめ 🔗

FizzBuzzのようなシンプルな問題をあえて過剰設計してみることで、構造化されたループ、データ駆動のルール(そしてマップの反復順序がなぜ重要か!)、クロージャ、高階関数、そして最後にゴルーチンとチャネルまでを一通り巡ることができました。Goで1つの問題を解決する方法がいかに多様であるかを改めて実感させてくれます!


📚 無料電子書籍をダウンロード 🔗

カウエの生き方のオフライン版を入手しましょう——🍏 健康、💰 富、❤️ 愛、🧘 意味を最適化するための実践ガイドです。

カウエの生き方

カウエの生き方

📬 ニュースレターを購読する 🔗

新しいエッセイ、本の要約、より良く生きるための実践ガイドを公開した際にお知らせします。スパムはありません——いつでも配信停止できます。


💬 コメントとディスカッション 🔗

質問、ご意見、提案はありますか?ぜひ下であなたの考えを共有してください。コメント欄は GitHub Discussions(広告なし・スパムなし)を利用しています——下の GitHub でサインイン をクリックしてコメントやリアクションを残してください(無料の GitHub アカウントは1分未満で作成できます)。



最終更新日: 2026/10/11.

免責事項:当サイトのコンテンツは情報提供および教育のみを目的としており、医療、心理、金融、投資、税務、または法律上の助言を構成するものではありません。ここに掲載されている考え、キュレーション、意見はすべて私個人のものであり、雇用主の見解を代表するものではありません。書籍、講演、記事の要約は独立した教育的な解説であり、それぞれの著者の知的財産です。編集、フォーマット、要約の補助としてAIツールを使用する場合がありますが、正確性を期すよう努めているものの誤りが含まれる可能性があります。プライバシー:当サイトでは、任意のニュースレター購読(follow.it)、コメント機能(giscus / GitHub Discussions)、および埋め込みメディア(YouTube)に外部サービスを利用しており、これらのサービスはそれぞれのプライバシーポリシーに従って訪問者のIPアドレスやCookieを処理する場合があります。