range構文と変数スコープの誤用をどうレビューするか?

Goのrange構文は、スライスやマップに対する繰り返し処理を簡潔に記述できる便利な構文です。
一方で、暗黙的な変数の扱いやクロージャとの関係が見落とされやすく、初級者による構造的なミスが頻発します

この記事では、range構文の適切な使い方を提示した上で、設計意図が不明確になりやすい典型的な誤用例をレビューアー視点で解説します。


1. 意図に沿ったrangeの基本構造と命名の整合

for i, user := range users {
    fmt.Printf("User %d: %s\n", i, user.Name)
}

この構造では以下の点が明示されています:

  • インデックスと値の両方が実際に使用されており、構造として過不足がない
  • インデックス変数iの使用目的(番号の表示)が明確
  • 命名と処理の意味づけが整合しており、読み手に誤解を与えない

不要な変数は捨て、必要な変数は明確に使い、意味が明示されていればrange構文は読みやすくなる。


2. 使用されない変数を放置する構文

for i, v := range items {
    fmt.Println(v)
}
@Reviewer
`i` が使われていないにも関わらず定義されています。読み手に「この変数は後で使われるのか?」と誤解を与えるため、`_` に置き換えて明示的に捨てましょう。
改善後
for _, v := range items {
    fmt.Println(v)
}

未使用の変数をそのまま残しておくと、「無駄な設計意図」を読み取らせる構造的ノイズになります。意図がないなら明示的に捨てるべきです。


3. クロージャとrange変数の罠

func createCallbacks() []func() {
    var callbacks []func()
    for _, label := range []string{"A", "B", "C"} {
        callbacks = append(callbacks, func() {
            fmt.Println(label)
        })
    }
    return callbacks
}
@Reviewer
このクロージャでは `label` がループ後の最終値をすべての関数で共有してしまいます。ループ変数のスコープを考慮し、新しい変数にコピーする必要があります。
改善後
for _, label := range []string{"A", "B", "C"} {
    label := label // コピーによるスコープ分離
    callbacks = append(callbacks, func() {
        fmt.Println(label)
    })
}

range変数はループごとに新しく定義されているように見えて、実際は1つの変数が再利用されている。クロージャによるキャプチャではこの点を明確に見直す必要がある。


4. インデックス使用が意味を持っていない/説明不足

for i, v := range values {
    fmt.Println(v)
    if i == 2 {
        break
    }
}
@Reviewer
`i == 2` という条件がマジックナンバーになっており、文脈が不明確です。定数化またはコメントを添えて意味を明確にしましょう。
改善後
const MaxProcessed = 2
for i, v := range values {
    fmt.Println(v)
    if i == MaxProcessed {
        break
    }
}

インデックス変数を使うのであれば、その意味と意図をコード上に可視化することがレビューの基本視点となる。


5. 回数ループとしてのrange利用の違和感

for range make([]struct{}, 5) {
    doSomething()
}
@Reviewer
回数ループに range を使う構文は、文法的には問題ありませんが、設計意図の明示性が下がります。`for i := 0; i < 5; i++` の方が読み手に意図が伝わりやすくなります。
改善後
for i := 0; i < 5; i++ {
    doSomething()
}

range構文を「意図的に選んだ」のでなければ、読み手にとって意図の明確な構文に置き換えることで設計の意図が伝わりやすくなります。


6. mapでの順序依存処理の危険性

for key := range map[string]int{"a": 1, "b": 2} {
    fmt.Println("last seen:", key)
}
@Reviewer
Goのmapはイテレーション順序が保証されません。「最後に処理したキー」に意味を持たせる設計は破綻する可能性があります。順序依存のロジックは避けましょう。

mapの順序は定義されていないため、最後のキーや順序を前提とした処理は全て非決定的なバグの温床になります。


7. range構文レビューのチェックリスト

観点 チェック内容
未使用変数 使用されないインデックスや値が _ で捨てられているか?
クロージャ range変数がクロージャにキャプチャされても誤動作しない構造か?
スコープ意識 range内で定義した変数がループ外で誤用されていないか?
順序前提 mapなど順序が保証されないデータに順序処理をしていないか?
意図の可視化 インデックス使用の理由が読み手に伝わるようになっているか?
range選択の妥当性 「回数ループ」なのに range を使っていないか?

あとがき:range構文と設計意図の整合を見極める

rangeはGoらしい簡潔な構文である一方、スコープや順序、変数の扱いに対する理解が浅いまま使うと設計上の歪みを生むことがある

レビューアーは構文そのものの正当性ではなく、次の問いに答える必要がある:

  • このrangeは「なぜ」この形で書かれているのか?
  • 変数は何を意味していて、どこで使われているのか?
  • 読み手はこの処理の意図を読み取れるか?

::: range構文をレビューする際は、「便利な構文が設計の意図を曖昧にしていないか?」という問いを常に持っておくことが重要です。
構文の省略ではなく、設計の意図を丁寧に表現するコードが、長く保守されるコードの条件となります。 :::