📚 背景知識(読んでから問題へ)
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
)
iotaはconstブロック内でのみ有効な「行カウンタ」です。ブロックの先頭で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でそのまま出力すると0や1としか表示されません。String() stringメソッドを実装した型はfmt.Stringerインターフェースを満たし、fmt.Printlnや%vで自動的に人間が読める文字列に変換されます。
func (w Weekday) String() string {
return [...]string{"Sunday", "Monday", "Tuesday"}[w]
}
📝 問題
タスク1(概念理解):
以下の問いに簡潔に答えてください。
iotaはどのようなルールで値が増えていきますか。constブロックの何行目で何の値になるか説明してください。- Goに
enumという専用キーワードがないのはなぜですか。代わりに何を組み合わせて列挙型を表現しますか。 1 << iotaという書き方は何のためのパターンですか。
タスク2(実装):
以下の要件を満たすmain.goを実装してください。
Seasonという名前付き型(type Season int)を定義し、constブロックとiotaを使ってSpring,Summer,Autumn,Winter(0〜3)を定義するSeason型にString() stringメソッドを実装し、fmt.Stringerインターフェースを満たすようにする(fmt.Println(season)で"Spring"のように表示されること)type Permission intという名前付き型を定義し、1 << iotaパターンでRead,Write,Exec(1, 2, 4)の3つの権限フラグを定義する- ビットOR演算(
|)で複数の権限を組み合わせた値を作り、ビットAND演算(&)で特定の権限を持っているかどうかを判定する関数HasPermission(p Permission, target Permission) boolを実装する main関数内で、Seasonの全パターンをforループで出力し、権限の組み合わせ(例: Read+Write)を作ってHasPermissionで判定した結果を出力する
🔍 ヒント(段階的開示)
ヒント1 — 方向性
package mainとfunc main()から始まる、go runでそのまま実行できる完全なプログラムにしてくださいSeasonのString()メソッドは、レシーバの整数値を配列やスライスのインデックスとして使い、対応する文字列を返す形にすると簡潔に書けます- 権限フラグは、単なる
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++のように回します(Seasonはintベースなので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 解説
type Season int
単にconstだけを使うのではなく、Seasonという名前付き型を経由することで「この関数は季節しか受け取らない」という意図を型シグネチャで表現できます。func Plant(s Season)のように書けば、うっかり無関係なintを渡すミスをコンパイラが(部分的に)防いでくれます。
const (
Spring Season = iota // 0
Summer // 1
Autumn // 2
Winter // 3
)
最初の行だけ型と= iotaを明示すれば、以降の行は式を省略しても直前の行がそのまま繰り返されるため、iotaが自動的に1ずつ増えていきます。これにより「連番の振り直し漏れ」というヒューマンエラーが起きません。
func (s Season) String() string {
names := [...]string{"Spring", "Summer", "Autumn", "Winter"}
return names[s]
}
String() stringというシグネチャを持つメソッドを定義するだけで、fmtパッケージ内部が自動的にこの型をfmt.Stringerとして扱い、Printlnや%v・%sで人間可読な文字列に変換してくれます。継承ベースのtoString()オーバーライドと違い、Goでは「そのシグネチャを満たしているかどうか」だけで判定される(構造的部分型)点が特徴です。
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演算で自由に組み合わせても値が衝突しません。
func HasPermission(p Permission, target Permission) bool {
return p&target == target
}
p & targetは「両方のビットが立っている部分」だけを残す演算です。その結果がtarget自身と一致していれば、targetが要求するビットが全てpに含まれていることになります。
💡 設計思想・なぜこう書くのか
enumという新しいキーワードを追加する代わりに、既存のconst・iota・named type・インターフェースの組み合わせで同じ表現力を実現しています。言語仕様を小さく保つことで、コンパイラの複雑さと学習コストの両方を抑えるのがGoの一貫した方針です。Season(100)を防げない)は保証されませんが、それは「めったに起きない問題のために言語を複雑にしない」というトレードオフです。実務ではString()実装やコードレビュー、静的解析でカバーします。iotaとビットシフトの組み合わせが自然に使われます。🌐 他言語との比較
| 観点 | Go | Java | Python | TypeScript |
|---|---|---|---|---|
| 列挙の書き方 | const+iota+named type | enum専用構文 | 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+ビットシフトパターンで定義されている(標準ライブラリ自身がこのイディオムの実例)
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
iotaはconstブロックの外でも使えると思う | 単なる特殊な変数だと誤解するため | iotaはconstブロック内でのみ意味を持つ。ブロック外では未定義 |
途中の行に別の値を書くと以降の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)の値なら何でも代入できてしまう。バリデーションが必要な場面は自前でチェック関数を用意する |
🚀 次のステップ
- 発展:
SeasonにIsValid() boolメソッドを追加し、範囲外の値を検出できるようにしてみましょう。またiotaの応用として、_(ブランク識別子)で特定の値をスキップするパターンも調べてみましょう - 次回予告: Day 003 — 制御構文(if/for/switch)(Goにはwhileがない理由・forの多用途性)