1-3. 制御構文 - if, for, switch, defer

所要時間: 30-50分(がっつりなら2セッション分) コミット内容: ~/learn/go/day03/flow/ 一式


このレッスンのゴール

  • if/for/switch の Go 流の書き方が手に馴染む
  • if v, err := f(); err != nil { ... } の初期化付き if を書ける
  • for 1個で 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.log

main.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すべて for 1個に統一したのが Go の特徴。

書き方他言語で言うと
for i := 0; i < N; i++ { }C/Java の for
for 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点セット

  1. 登録順とは逆順に実行(後入れ先出し / LIFO)
  2. 登録時点の引数の値を覚える(後で変わっても影響なし)
  3. 関数が 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 = 21 を出力する。変数のキャプチャに注意
  • 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. セッション録画を終了

exit

2. 今日の発見(このノートに追記)

- 一番「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 の for
for cond {}条件のみwhile (cond)
for {}無限ループwhile (true)
for k, v := range m {}コレクション反復for-of / for-in

if と switch の選び方

状況推奨
単一条件の分岐if
1つの値を複数候補と比較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 ifelse if cond { ... }
N回ループfor i := 0; i < N; i++ { }
while風for cond { }
無限ループfor { }
ループ抜けるbreak
次のループへcontinue
switchswitch x { case a: ... default: ... }
複数値の casecase a, b, c:
条件付き switchswitch { case x > 0: ... }
型で switchswitch 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_エラー処理 でエラーラップ階層化に繋がる