1-3. 制御構文 - if, for, switch, defer
所要時間: 30-50分(がっつりなら2セッション分) コミット内容:
~/learn/go/day03/flow/一式
このレッスンのゴール
-
if/for/switchの Go 流の書き方が手に馴染む -
if v, err := f(); err != nil { ... }の初期化付き if を書ける -
for1個で while / 無限ループ / range を全部書ける -
deferで「リソース取得 → エラー → defer 解放」鉄板パターン - 早期 return / ガード節でネストを浅く保てる
なぜ学ぶか
「TS の try/catch が無くて困らないの?」が一番気になるところ。答えは「if err != nil { return err } を脊髄反射で書けるようになると困らない」。さらに defer という JS には無い概念が、ファイル・DB行・HTTP body・ロックの解放を1行で安全にする。これを知らないと「Close 忘れでファイルディスクリプタが漏れて本番サーバーが落ちる」という古典的事故を起こす。
前章とのつながり
1-2_変数と型 で n, err := strconv.Atoi(s) のパターンを見た。本章ではこの err をどう if err != nil で受け止めるか が中心テーマ。ゼロ値・nil の知識をフル活用する。
これができると何が嬉しいか
- 「あ、これファイル使ってる、Close 忘れた」が無くなる →
deferが脊髄反射に - エラー処理が読みやすいコード が書ける → レビューで「Goっぽい」と言われる
switchの type switch でインターフェース実装の裏側が見える → Level 2 への布石
ストーリー導入: defer は「玄関で靴を脱ぐ約束」
家に入る瞬間に「出る時、靴を玄関に戻すからな」と約束しておくのが defer。本処理(家の中で過ごす)が成功しようがpanic(火事で逃げ出す)しようが、関数を抜ける直前で必ず靴を戻す。これがファイル Close、ロック解放、HTTP body Close の鉄板パターンの正体。
大前提: Go の制御構文は「機能を意図的に削った」設計
JS には for, for...of, for...in, while, do...while と無数のループがある。Go にあるのは for ひとつだけ。なぜか:
- 学ぶ構文が減る → 新人にやさしい
- 書き方が統一される → コードレビューが楽
- for だけで while / 無限ループ / イテレーションを全部表現できる → 機能不足にはならない
これは Go の哲学そのもの。「機能を増やす方が簡単。減らす方が難しい」というスタンスを最初から貫いている。
セッション①: if と for(25-30分)
0. 録画スタート&作業ディレクトリ
mkdir -p ~/log ~/learn/go/day03/flow
cd ~/learn/go/day03/flow
go mod init example.com/flow
script ~/log/go_day03.logmain.go を以下で開始:
package main
import "fmt"
func main() {
// ここに書いていく
}1. if / else - カッコ不要、波カッコ必須
score := 75
if score >= 80 {
fmt.Println("優")
} else if score >= 60 {
fmt.Println("良")
} else {
fmt.Println("不可")
}
ifの本質真偽値を見て分岐する基本構文。JS とほぼ同じだが 条件式にカッコ
()が無い、波カッコ{}が必須。// OK if x > 0 { fmt.Println("positive") } // NG: コンパイルエラー(カッコ不要) // if (x > 0) { ... } // NG: 1文でも { } 省略不可 // if x > 0 fmt.Println("positive")JS との違い
- JS:
if (x > 0) ...でカッコ必須、1文なら{}省略可- Go: カッコ無し、
{}必須文法エラーが減るので長期的にバグが減る設計。
Go 独特の: 初期化付き
if文if err := doSomething(); err != nil { fmt.Println("失敗:", err) return } // err はこのスコープ外では参照できない
if 初期化文; 条件 { ... }の形で、if の中でしか使わない一時変数 を作れる。これが Go の慣用句で、エラーハンドリングで多用する。使い所
- エラーチェック:
if v, err := f(); err != nil { ... }- 型アサーション:
if s, ok := x.(string); ok { ... }- 一時的な計算結果の判定
メリット: スコープが狭まる → 変数の寿命を短く保てる → バグが減る
ifの落とし穴
{を改行して書くとコンパイルエラー:if x > 0 \n {は NG。{は同じ行に- 真偽値以外は条件にできない:
if x { ... }のxが int だとエラー(JS のような暗黙の真偽変換は無い)。明示的にif x != 0 { ... }と書く- 三項演算子は無い:
a ? b : cは書けない。if/elseで2行使うか、関数を作る
2. for は3形態を1構文で
// 1. C 言語スタイル(初期化; 条件; 後処理)
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// 2. 条件のみ(他言語の while 相当)
n := 0
for n < 5 {
fmt.Println(n)
n++
}
// 3. 無限ループ(他言語の while(true) 相当)
count := 0
for {
count++
if count >= 3 {
break
}
}
fmt.Println("count =", count)Go の
forの本質他言語の
for/while/do-whileを すべてfor1個に統一したのが Go の特徴。
書き方 他言語で言うと for i := 0; i < N; i++ { }C/Java の forfor cond { }while (cond)for { }while (true)(無限ループ)for k, v := range slice { }for-of / for-in(次レッスン詳述) メリット: 構文を1つ覚えるだけ。レビュー時のパターンも統一される。
forの実務ユースケース
- N回繰り返し:
for i := 0; i < 10; i++ { ... }- コレクション処理:
for _, item := range items { ... }(次レッスン)- イベントループ:
for { selectでチャネル待つ }(並行処理で頻出)- 再試行ループ:
for retry := 0; retry < maxRetry; retry++ { ... }
forの落とし穴
for in/for ofは無い: コレクションを回したい時はfor ... rangeを使う- break / continue は使える: ただしラベル付き break は珍しい
- 無限ループは意外と頻出: サーバーのメインループ、リトライ処理。
for { ... }を見たら「これは意図的な無限ループ」とすぐ判断できるように++--は文であって式ではない:x := i++のような書き方は コンパイルエラー。後置のみ、それも単独で
3. break と continue
for i := 0; i < 10; i++ {
if i == 3 {
continue // この回をスキップ
}
if i == 7 {
break // ループ脱出
}
fmt.Println(i)
}
// 0 1 2 4 5 6
break/continueの本質
break: 一番内側のループを抜けるcontinue: 一番内側のループの次のイテレーションへJS と同じ感覚でOK。ラベル付き
break OUTERも書けるが、複雑なネストで初めて出てくる。最初は不要。
4. 早期 return パターン
func validate(name string, age int) error {
if name == "" {
return fmt.Errorf("name is required")
}
if age < 0 {
return fmt.Errorf("age must be non-negative")
}
if age > 150 {
return fmt.Errorf("age must be <= 150")
}
// ここまで来れば全部OK
return nil
}Go 流: ネストは深くせず、早期 return
Go コミュニティでは 「ガード節 + 早期 return」を強く好む。エラーや異常系を先に弾いて、本筋を最後に書く。
NG例 (Go っぽくない):
func validate(name string, age int) error { if name != "" { if age >= 0 { if age <= 150 { return nil // 本筋がネストの底 } else { return errTooOld } } else { return errNegative } } return errNoName }GoらしいOK例: 異常系を先に return、正常系は左寄せ
func validate(name string, age int) error { if name == "" { return errNoName } if age < 0 { return errNegative } if age > 150 { return errTooOld } return nil }メリット: ネストが浅い → 読みやすい → バグが減る。コードレビューでも「else 減らせる?」とよく指摘される。
セッション②: switch と defer(25-30分)
5. switch - フォールスルーしない、柔軟
day := "Wed"
switch day {
case "Mon":
fmt.Println("週始め")
case "Tue", "Wed", "Thu":
fmt.Println("中盤")
case "Fri":
fmt.Println("ハナキン")
case "Sat", "Sun":
fmt.Println("週末")
default:
fmt.Println("?")
}
switchの本質値と複数の候補を比較する分岐。C/Java の switch とは挙動が違う ので注意。
Go の switch の特徴
- フォールスルー(次の case に流れる)は デフォルトでナシ。
breakを書かなくていい- 複数値を1つの case にまとめられる:
case "Tue", "Wed", "Thu":- 任意の型を比較できる(文字列、構造体など。C みたいに整数限定じゃない)
- 条件式なしの
switch { }を書いて、各 case に式を書けば if/else の代わりにもなる
条件付き switch(if/else の代替)
score := 75 switch { case score >= 80: fmt.Println("優") case score >= 60: fmt.Println("良") default: fmt.Println("不可") }
switchに値を渡さず、各 case で真偽値を評価する書き方。if / else ifのチェーンが長くなる時はこっちの方が読みやすい。
type switch- 型で分岐func describe(v interface{}) { switch t := v.(type) { case int: fmt.Printf("int: %d\n", t) case string: fmt.Printf("string: %q\n", t) case bool: fmt.Printf("bool: %t\n", t) default: fmt.Printf("unknown: %v\n", t) } } describe(42) describe("hi") describe(true)
v.(type)という特殊構文はswitchの中だけで使える。interface{}(何でも入る型)の中身を実行時に判別する時に使う。使い所
- JSON のような動的な型を扱う時
- エラーの型を判別する時(
errors.Asの手書き版)
switchの落とし穴
- 意図的に次の case に流したい時は
fallthrough: 滅多に使わない。書くと「意図あり」が伝わる- case の値はコンパイル時に決まる必要は無い: 関数呼び出しを書ける(C と違う)。
case getDefault():も書ける- break は不要、書いてもいい: 中途脱出に使うが、フォールスルーが無いので必須ではない
6. defer - 関数終了時に実行を予約
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("/etc/hosts")
if err != nil {
fmt.Println("open failed:", err)
return
}
defer f.Close() // ← この関数を抜ける直前に必ず実行される
// f を使った処理...
fmt.Println("ファイル開けた")
} // ← ここで f.Close() が走る
deferの本質「この関数の最後で実行してね」と予約する仕組み。関数の終了直前(return の直前)に必ず呼ばれる。例外的な経路(panic 含む)でも呼ばれる。
挙動の3点セット
- 登録順とは逆順に実行(後入れ先出し / LIFO)
- 登録時点の引数の値を覚える(後で変わっても影響なし)
- 関数が return / panic / 正常終了のいずれでも実行される
JS の try/finally に近い。ただし
deferは1行で書ける、複数登録できる、ブロック単位ではなく関数単位、と Go 流のシンプルさ。
deferの実務ユースケースGo コードを開けば必ず見る、リソース解放の鉄板パターン:
// ファイル f, err := os.Open(path) if err != nil { return err } defer f.Close() // データベース行 rows, err := db.Query(sql) if err != nil { return err } defer rows.Close() // HTTP レスポンスボディ resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() // ロック mu.Lock() defer mu.Unlock()共通パターン: 「取得 → エラーチェック → defer で解放 → 本処理」。これがほぼ全ての I/O コードに登場する。早めに体に染み込ませる。
deferの落とし穴
- ループ内で
deferを多発するな:for _, f := range files { defer f.Close() }だと全部の Close が関数終了まで遅延する。ループの最後まで開きっぱなしになる。関数を切り出すかループ内で明示的に Close- 引数は登録時に固定:
i := 1; defer fmt.Println(i); i = 2は1を出力する。変数のキャプチャに注意deferの中でエラーを無視しがち:defer f.Close()でクローズエラーが捨てられる。本番のクリティカルなコードではdefer func() { _ = f.Close() }()のように受けて記録する
defer の実行順を体感する
func main() { defer fmt.Println("1番目に登録") defer fmt.Println("2番目に登録") defer fmt.Println("3番目に登録") fmt.Println("main 本体") } // 出力: // main 本体 // 3番目に登録 // 2番目に登録 // 1番目に登録LIFO(後入れ先出し)= 「あと付けしたものが先に実行」。スタックのイメージ。
7. エラーは値 - try/catch の代わり
package main
import (
"fmt"
"strconv"
)
func main() {
n, err := strconv.Atoi("not a number")
if err != nil {
fmt.Println("変換失敗:", err)
return
}
fmt.Println("変換成功:", n)
}Go のエラーは「特別な例外」ではなく「ただの戻り値」
JS/Java の
throw new Error()/try { } catch { }のような仕組みは意図的に無い。代わりに:result, err := doSomething() if err != nil { // エラー処理 }という戻り値2つ返しのパターンですべてを表現する。
メリット
- どこでエラーが起きうるか コードを読めば一目瞭然(戻り値に
errorがある関数はエラーを返す)- try-catch のような「飛ぶ」制御フローが無い → 制御が予測しやすい
- エラーの伝播を強制できない →
_で握りつぶせる(が、これは反パターン)デメリット
if err != nil { return err }を 大量に書くハメになる(俗に「if err != nil 教」)- 一見冗長だが、これが Go 流
Go 流のエラーハンドリングの型
関数を1つ書くごとに以下を考える:
func doWork() error { // 1. 何かする result, err := step1() if err != nil { return fmt.Errorf("step1 failed: %w", err) // %w でエラーラップ } // 2. 次の処理 if err := step2(result); err != nil { return fmt.Errorf("step2 failed: %w", err) } // 3. 正常終了 return nil }
- エラーは無視せず常にチェック
- ラップして「どこで起きたか」を残す(
fmt.Errorf("...: %w", err))- 正常系は最後
panicは本当に「終わり」の時だけGo にも
panicという機能はあるが、スタックを巻き戻して落ちるだけで、JS の throw とは違う。使うべき場面:
- ライブラリ初期化時の致命的エラー (これが無いと続行不可能)
- 絶対起きないはずな状態(防御的アサーション)
使うべきではない場面:
- ユーザー入力エラー
- ファイルが無い、ネットワーク失敗などの予測可能なエラー
普通の業務コードで
panicを見かけたら設計のニオイ。基本はerror戻り値で表現する。
練習課題
// main.go を以下で書き換えて実行
package main
import (
"fmt"
"strconv"
)
// FizzBuzz - 制御構文の練習に鉄板
func fizzBuzz(n int) string {
switch {
case n%15 == 0:
return "FizzBuzz"
case n%3 == 0:
return "Fizz"
case n%5 == 0:
return "Buzz"
default:
return strconv.Itoa(n)
}
}
// 文字列を渡して何回 'a' が含まれるか
func countA(s string) int {
count := 0
for _, c := range s {
if c == 'a' {
count++
}
}
return count
}
// 安全な割り算(エラー戻り値パターン)
func safeDivide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("0で割れません")
}
return a / b, nil
}
func main() {
// 1. FizzBuzz 1~20
for i := 1; i <= 20; i++ {
fmt.Println(fizzBuzz(i))
}
// 2. 文字数カウント
fmt.Println(countA("banana cake apple")) // 5
// 3. 安全割り算 + defer の確認
defer fmt.Println("プログラム終了")
if result, err := safeDivide(10, 2); err != nil {
fmt.Println("エラー:", err)
} else {
fmt.Println("10 / 2 =", result)
}
if _, err := safeDivide(10, 0); err != nil {
fmt.Println("エラー検出:", err)
}
}go run .動作確認のポイント
- FizzBuzz が正しく出る
0で割れませんのエラーがちゃんと処理されるプログラム終了が最後に表示される(defer の確認)
締め: 振り返り(10分)
1. セッション録画を終了
exit2. 今日の発見(このノートに追記)
- 一番「Go っぽい」と感じた構文:
- if err != nil を書いてみた感想:
- defer の挙動で驚いたこと:
- 明日やりたいこと:
アンチパターン集 - やらかし事例
defer の事故あるある
1. ループ内の defer で大量のファイル開きっぱなし
for _, path := range files { f, _ := os.Open(path) defer f.Close() // ← ループ全体が終わるまで Close されない // 1万ファイル開くと FD 上限に達してクラッシュ }対策: ループ本体を関数に切り出し、その関数が return する時に defer 発火させる。
2. defer の引数が登録時に固定される罠
i := 0 defer fmt.Println(i) // 「0」が登録される i = 100 // 関数終了時、出力は 100 ではなく 0後で書き換えたい値は クロージャで参照 する。
3. Close のエラーを無視
defer f.Close() // 戻り値の error は捨てられている書き込み系ファイルでは Close 時にフラッシュエラーが出ることがある。重要なファイルは
defer func() { err = f.Close() }()で受ける。4.
if err == nilの二重否定で読みづらいif err == nil { // 正常系がネストの底 } else { return err }Go 流は「異常系を先に return、正常系は左寄せ」。
対比表で違いを明確化
for の3形態
書き方 役割 他言語で言うと for i := 0; i < N; i++ {}カウンタ付き C/Java の forfor cond {}条件のみ while (cond)for {}無限ループ while (true)for k, v := range m {}コレクション反復 for-of / for-in
if と switch の選び方
状況 推奨 単一条件の分岐 if1つの値を複数候補と比較 switch x { case a: case b: }範囲条件のチェーン switch { case x>0: case x<0: }エラー値の型判別 switch err := err.(type) {}またはerrors.As列挙型の網羅 switch
自己評価チェックリスト
手を動かせた
-
if文を書いた(カッコ無し、波カッコ必須) -
if 初期化; 条件 { }を1度は書いた -
forの3形態すべてを書いた -
switchで複数 case まとめ書きをした -
deferでファイルクローズを書いた
説明できる
-
ifにカッコ不要、{は必須 を説明できる -
forだけで while も無限ループも書ける理由 -
switchがフォールスルーしないこと -
deferの LIFO 実行順序 - エラーを戻り値で扱う Go 流の意図
やらかし回避
- ループ内
deferの罠を理解した - 早期 return でネストを浅く書ける
-
panicを業務コードで使うべきでないと言える
詰まった時のチートシート
| やりたいこと | 書き方 |
|---|---|
| if 分岐 | if x > 0 { ... } |
| if + 初期化 | if v, err := f(); err != nil { ... } |
| else if | else if cond { ... } |
| N回ループ | for i := 0; i < N; i++ { } |
| while風 | for cond { } |
| 無限ループ | for { } |
| ループ抜ける | break |
| 次のループへ | continue |
| switch | switch x { case a: ... default: ... } |
| 複数値の case | case a, b, c: |
| 条件付き switch | switch { case x > 0: ... } |
| 型で switch | switch t := v.(type) { case int: ... } |
| 関数終了時に実行 | defer f.Close() |
| エラーチェック | if err != nil { return err } |
「実務OK」基準
if err != nil { return err }が無意識に書ける- ファイルや HTTP のリソース解放を
deferで書ける - switch でフォールスルーを期待しない頭になっている
- 早期 return / ガード節でネストを浅く保てる
for ... rangeの存在を知っている(次レッスンで詳述)
次のレッスン
1-4. 関数 へ。
関数定義、多値返却、named return values、可変長引数、クロージャ、first-class function を扱う。Goの関数はシンプルだが、戻り値を複数返せる点は JS/TS と大きく違う。
つながりの予告
- 本章で書いた
if err != nil { return err }は、次章では 「(result, err)を返す関数の設計」 とセットで深掘り deferは次章の named return + defer でエラーラップ という上級パターンに発展- 早期 return の発想は 2-5_エラー処理 でエラーラップ階層化に繋がる