Day 002 — 定数とiota

2026-07-30 🟢 初心者 / Phase 1 実装 定数とiota

📚 背景知識(読んでから問題へ)

Goには他言語にあるようなenumというキーワードが存在しません。その代わりにconstブロックとiotaという特殊な仕組みを組み合わせて、列挙型(enum)に相当する概念を表現します。これはGoの「言語仕様は小さく保ち、既存の仕組み(型システム)を組み合わせて表現力を出す」という設計思想の典型例です。

const の基本

const Pi = 3.14159
const MaxRetries = 3

varと違い、constで宣言した値はコンパイル時に確定し、実行中に変更できません。定数はゼロ値を持たず、必ず値を与える必要があります。

const ブロックと iota

複数の関連する定数をまとめて宣言する際は、constブロックを使います。

const (
    Sunday = iota // 0
    Monday        // 1
    Tuesday       // 2
)

iotaconstブロック内でのみ有効な「行カウンタ」です。ブロックの先頭で0から始まり、新しい行(定数宣言)が現れるたびに1ずつ増加します。2行目以降で式を省略すると、直前の行の式がそのまま繰り返されるため、iotaを使った式は自動的に増分されていきます。

なぜGoは enum キーワードを用意しなかったのか

enumを専用構文にすると、「取りうる値の集合を型として保証する」ための新しいコンパイラルールが必要になります。Goは代わりに「名前付きの型(named type)」+「const+iota」という既存の仕組みの組み合わせで同じ効果を実現しています。専用構文を増やさない代わりに、以下のような制約は言語レベルでは保証されません。

type Weekday int

const (
    Sunday Weekday = iota
    Monday
    Tuesday
)

var w Weekday = 100 // コンパイルは通ってしまう(Weekdayとして未定義の値)

これは「シンプルさとのトレードオフ」であり、実務ではString()メソッドの実装やレビューでカバーする文化になっています。

iota を使ったビットフラグパターン

iotaはビットシフトと組み合わせることで、フラグの集合(複数のON/OFFを1つの整数で表現するパターン)にもよく使われます。

const (
    ReadPermission  = 1 << iota // 1 << 0 = 1
    WritePermission             // 1 << 1 = 2
    ExecPermission              // 1 << 2 = 4
)

Stringer インターフェースによる可読な出力

const+iotaで作った値は素の整数なので、fmt.Printlnでそのまま出力すると01としか表示されません。String() stringメソッドを実装した型はfmt.Stringerインターフェースを満たし、fmt.Println%vで自動的に人間が読める文字列に変換されます。

func (w Weekday) String() string {
    return [...]string{"Sunday", "Monday", "Tuesday"}[w]
}

📝 問題

タスク1(概念理解):
以下の問いに簡潔に答えてください。

  1. iotaはどのようなルールで値が増えていきますか。constブロックの何行目で何の値になるか説明してください。
  2. Goにenumという専用キーワードがないのはなぜですか。代わりに何を組み合わせて列挙型を表現しますか。
  3. 1 << iotaという書き方は何のためのパターンですか。

タスク2(実装):
以下の要件を満たすmain.goを実装してください。

  1. Seasonという名前付き型(type Season int)を定義し、constブロックとiotaを使ってSpring, Summer, Autumn, Winter(0〜3)を定義する
  2. Season型にString() stringメソッドを実装し、fmt.Stringerインターフェースを満たすようにする(fmt.Println(season)"Spring"のように表示されること)
  3. type Permission intという名前付き型を定義し、1 << iotaパターンでRead, Write, Exec(1, 2, 4)の3つの権限フラグを定義する
  4. ビットOR演算(|)で複数の権限を組み合わせた値を作り、ビットAND演算(&)で特定の権限を持っているかどうかを判定する関数HasPermission(p Permission, target Permission) boolを実装する
  5. main関数内で、Seasonの全パターンをforループで出力し、権限の組み合わせ(例: Read+Write)を作ってHasPermissionで判定した結果を出力する

🔍 ヒント(段階的開示)

ヒント1 — 方向性
  • package mainfunc main()から始まる、go runでそのまま実行できる完全なプログラムにしてください
  • SeasonString()メソッドは、レシーバの整数値を配列やスライスのインデックスとして使い、対応する文字列を返す形にすると簡潔に書けます
  • 権限フラグは、単なるintの定数ではなく1, 2, 4, 8...という「2の累乗」になっていることが重要です。これが後のビットAND判定を可能にします
ヒント2 — アプローチ
  • String()メソッドは値レシーバ(func (s Season) String() string)で定義します。ポインタレシーバにするとfmt.Println(season)(値渡し)で呼ばれない場合があるため注意してください
  • HasPermissionの判定は p&target == target という式で「targetのビットが全てpに含まれているか」を確認できます
  • Seasonの全パターンを列挙するには、SpringからWinterまでの範囲をfor i := Spring; i <= Winter; i++のように回します(Seasonintベースなのでfor文で扱えます)
ヒント3 — コード骨格
package main

import "fmt"

type Season int

const (
	Spring Season = iota
	Summer
	Autumn
	Winter
)

func (s Season) String() string {
	names := [...]string{"Spring", "Summer", "Autumn", "Winter"}
	return names[s]
}

type Permission int

const (
	Read Permission = 1 << iota
	Write
	Exec
)

func HasPermission(p Permission, target Permission) bool {
	// pにtargetのビットが全て含まれているかを判定
	return false // ここを実装
}

func main() {
	for s := Spring; s <= Winter; s++ {
		fmt.Println(s)
	}

	perms := Read | Write
	fmt.Println(HasPermission(perms, Read))
	fmt.Println(HasPermission(perms, Exec))
}

模範解答

package main

import "fmt"

// --- Season: iotaによる列挙 ---
type Season int

const (
	Spring Season = iota // 0
	Summer                // 1
	Autumn                // 2
	Winter                // 3
)

func (s Season) String() string {
	names := [...]string{"Spring", "Summer", "Autumn", "Winter"}
	if int(s) < 0 || int(s) >= len(names) {
		return "Unknown"
	}
	return names[s]
}

// --- Permission: 1 << iota によるビットフラグ ---
type Permission int

const (
	Read Permission = 1 << iota // 1 << 0 = 1
	Write                        // 1 << 1 = 2
	Exec                          // 1 << 2 = 4
)

func HasPermission(p Permission, target Permission) bool {
	return p&target == target
}

func main() {
	fmt.Println("--- Season(iotaによる列挙) ---")
	for s := Spring; s <= Winter; s++ {
		fmt.Printf("%d: %s\n", s, s)
	}

	fmt.Println("--- Permission(1 << iota によるビットフラグ) ---")
	perms := Read | Write
	fmt.Printf("perms = %d (Read+Write)\n", perms)
	fmt.Println("HasPermission(perms, Read):", HasPermission(perms, Read))
	fmt.Println("HasPermission(perms, Write):", HasPermission(perms, Write))
	fmt.Println("HasPermission(perms, Exec):", HasPermission(perms, Exec))

	all := Read | Write | Exec
	fmt.Printf("all = %d (Read+Write+Exec)\n", all)
	fmt.Println("HasPermission(all, Exec):", HasPermission(all, Exec))
}
▶ 実行結果を見る(go run main.go)
--- Season(iotaによる列挙) ---
0: Spring
1: Summer
2: Autumn
3: Winter
--- Permission(1 << iota によるビットフラグ) ---
perms = 3 (Read+Write)
HasPermission(perms, Read): true
HasPermission(perms, Write): true
HasPermission(perms, Exec): false
all = 7 (Read+Write+Exec)
HasPermission(all, Exec): true

🪜 Step-by-Step 解説

1
名前付き型で列挙の「型」を作る
type Season int
単にconstだけを使うのではなく、Seasonという名前付き型を経由することで「この関数は季節しか受け取らない」という意図を型シグネチャで表現できます。func Plant(s Season)のように書けば、うっかり無関係なintを渡すミスをコンパイラが(部分的に)防いでくれます。
2
const ブロックと iota で連番を作る
const (
	Spring Season = iota // 0
	Summer                // 1
	Autumn                // 2
	Winter                // 3
)
最初の行だけ型と= iotaを明示すれば、以降の行は式を省略しても直前の行がそのまま繰り返されるため、iotaが自動的に1ずつ増えていきます。これにより「連番の振り直し漏れ」というヒューマンエラーが起きません。
3
Stringer インターフェースを実装する
func (s Season) String() string {
	names := [...]string{"Spring", "Summer", "Autumn", "Winter"}
	return names[s]
}
String() stringというシグネチャを持つメソッドを定義するだけで、fmtパッケージ内部が自動的にこの型をfmt.Stringerとして扱い、Println%v%sで人間可読な文字列に変換してくれます。継承ベースのtoString()オーバーライドと違い、Goでは「そのシグネチャを満たしているかどうか」だけで判定される(構造的部分型)点が特徴です。
4
1 << iota でビットフラグを作る
const (
	Read Permission = 1 << iota // 1 << 0 = 1
	Write                        // 1 << 1 = 2
	Exec                          // 1 << 2 = 4
)
iotaが0, 1, 2と増えるのに合わせて1 << iotaは1, 2, 4という2の累乗になります。それぞれが2進数で見ると001, 010, 100のように1ビットしか立っていないため、OR演算で自由に組み合わせても値が衝突しません。
5
ビット演算で判定する
func HasPermission(p Permission, target Permission) bool {
	return p&target == target
}
p & targetは「両方のビットが立っている部分」だけを残す演算です。その結果がtarget自身と一致していれば、targetが要求するビットが全てpに含まれていることになります。

💡 設計思想・なぜこう書くのか

📌
専用構文を増やさない: enumという新しいキーワードを追加する代わりに、既存のconstiota・named type・インターフェースの組み合わせで同じ表現力を実現しています。言語仕様を小さく保つことで、コンパイラの複雑さと学習コストの両方を抑えるのがGoの一貫した方針です。
📌
明示のコストは開発者が引き受ける: 型としての厳密な値の制約(Season(100)を防げない)は保証されませんが、それは「めったに起きない問題のために言語を複雑にしない」というトレードオフです。実務ではString()実装やコードレビュー、静的解析でカバーします。
📌
ビット演算が第一級の道具である: 高レベル言語ではビット演算を意識する機会が減りがちですが、Goは低レイヤーも意識した言語設計のため、フラグ管理・プロトコル実装などでiotaとビットシフトの組み合わせが自然に使われます。

🌐 他言語との比較

観点GoJavaPythonTypeScript
列挙の書き方const+iota+named typeenum専用構文enum.Enumクラス(標準ライブラリ)enum専用構文(コンパイル時のみ)
型としての厳密さ保証は弱い(intの代入を防げない)強い(enum型は列挙値以外を持てない)強い(Enumメンバ以外は生成不可)中程度(数値enumはstructural typingの影響で緩い)
文字列表現Stringerインターフェースを自前実装toString()をデフォルトでオーバーライド可能__str__.name属性enumのキー名をそのまま使える
ビットフラグ表現iota+ビットシフトで明示的に実装EnumSetなどのユーティリティに依存enum.Flagでサポートビット演算を手動で書くのが一般的

Java/Python/TypeScriptのenumは「専用の型システムサポート」を持つ一方、Goのconst+iotaは「既存の仕組みの組み合わせ」です。安全性は一段階弱くなりますが、言語仕様がシンプルなままで済むという設計判断です。他言語経験者ほど「なぜGoにはenumがないのか」に違和感を覚えやすいポイントなので、この設計思想の違いを意識すると理解が深まります。

🏆 実務での使いどころ

  • HTTPステータスコードの分類やAPIのステータス(Pending, Active, Completedなど)を表す型はconst+iota+Stringerで実装されることが非常に多い(構造体のフィールドとして持たせ、JSON出力時は文字列に変換するなど)
  • ログレベル(Debug, Info, Warn, Error)は典型的なiotaベースの列挙パターンで、標準ライブラリのlog/slogもこの考え方に近い設計になっている
  • Unix系のファイルパーミッション(読み取り・書き込み・実行)を扱うコードや、フィーチャーフラグ・権限管理システムでは1 << iotaによるビットフラグパターンが実際に使われる
  • osパッケージのos.O_RDONLY, os.O_WRONLY, os.O_CREATEなどのファイルオープンフラグも、まさにこのiota+ビットシフトパターンで定義されている(標準ライブラリ自身がこのイディオムの実例)

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
iotaconstブロックの外でも使えると思う単なる特殊な変数だと誤解するためiotaconstブロック内でのみ意味を持つ。ブロック外では未定義
途中の行に別の値を書くと以降のiotaがずれることに気づかないJava/TypeScriptのenumのように連番が自動保証されると思い込むある行で明示的に別の値を書くと、その行のiota値自体は消費されるが、以降の行はまた「式の繰り返し」に戻る。途中に手動の値を混ぜると可読性が落ちるため避けるのが無難
ビットフラグの定数を1, 2, 3のように連番で作ってしまうiotaの連番の感覚をそのまま流用してしまうビットフラグは1, 2, 4, 8という2の累乗でなければOR/AND演算が破綻する。必ず1 << iotaを使う
Season(100)のような不正な値の代入がコンパイルエラーになると思うJava/TypeScriptのenumが厳密な型チェックを持つ感覚を引きずるGoのnamed typeは基底型(int)の値なら何でも代入できてしまう。バリデーションが必要な場面は自前でチェック関数を用意する

🚀 次のステップ

  • 発展: SeasonIsValid() boolメソッドを追加し、範囲外の値を検出できるようにしてみましょう。またiotaの応用として、_(ブランク識別子)で特定の値をスキップするパターンも調べてみましょう
  • 次回予告: Day 003 — 制御構文(if/for/switch)(Goにはwhileがない理由・forの多用途性)

🎯 自己評価

自分の回答

気づき・メモ