Go: Goの制御フロー:4つの主要な制御フロー文――if、switch、for、およびdefer
制御フローはプログラムに「思考」をもたらします。Go言語は、たった4つのシンプルなキーワードであらゆる制御フローのシナリオを網羅しており、
whileやdo-whileループのような冗長性を排除しています。
Goの制御フローの設計哲学は、「Less is more」です。if、switch、for、deferの4つのキーワードに、break、continue、labelを組み合わせれば、ビジネスロジックの99%を処理するのに十分です。このレッスンでは、Goの制御フローに関するすべての核心的な概念を習得します。
1. 学習内容
- if 条件文(初期化文を含む)
- ブランチを切り替える(自動で中断されるため、手動での操作は不要)
- forループの4つの形式(whileやdo-whileの代替)
- 延期:実行の延期とLIFO順序
- ブレーク、コンティニュー、ラベルを用いた正確なジャンプ
- 制御フローを用いたオンライン試験採点システムの構築
2. オンライン教育プラットフォームに関する実話
(1) 課題:試験の採点に500行ものC++コードを使用しているにもかかわらず、バグが後を絶たない
チャーリーは、あるオンライン教育プラットフォームのバックエンドエンジニアです。彼はつい先日、自動採点システムのコードを引き継いだばかりです:
「首相によると、現在、1,000枚の試験答案を処理するのに8時間かかっており、バグが発生するたびに、『範囲外』エラーか、C言語の
if文から{}が抜け落ちているかのどちらかで、その結果、その学年の成績全体がめちゃくちゃになってしまうそうだ……」
彼は前任者が残していったC++のコードを開いて、目を通した:
// C++ Style: Verbose + Prone to Errors
if (score >= 60) {
grade = "Passing Grade";
}
else // Note: C++ else on a different line is valid, but team style is inconsistent
grade = "Fail";
// Omission of a loop condition = an infinite loop
while (true) { // Wanted 3 iterations, but forgot the counter
// ... Didn't realize until 500 lines later
}
引き継いだ後、チャーリーはそれをGoで書き直すことにした。
(2) Go言語での解決策
Goでは、このプロセス全体を4つのキーワードで網羅しています:
// score_evaluator.go
package main
import "fmt"
func evaluateScore(score int) string {
// (1) if contains an initialization statement
if grade := calculateGrade(score); grade != "" {
return fmt.Sprintf("Score: %d, Level: %s", score, grade)
}
return "Invalid Score"
}
func calculateGrade(score int) string {
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
case score >= 60:
return "C"
default:
return "D"
}
}
func main() {
// (2) for 4 forms
scores := []int{95, 82, 67, 45, 88}
for i, s := range scores { // range form
fmt.Printf("Question %d: %d points -> %s\n", i+1, s, evaluateScore(s))
}
}
出力:
Question 1: 95 points -> Score: 95, Level A
Question 2: 82 points -> Score: 82, Level B
Question 3: 67 points -> Score: 67, Level C
Question 4: 45 points -> Score: 45, Level D
Question 5: 88 points -> Score: 88, Level B
(3) メリット:プロセス制御の簡便性の比較
| 言語 | if キーワード | ループ キーワード | switch の自動ブレーク | 採点システムのコード量 |
|---|---|---|---|---|
| C/C++ | if |
for/while/do-while |
手動での break が必要 |
約500行 |
| Java | if |
for/while/do-while |
手動での break が必要 |
約400行 |
| Python | if |
for/while |
該当なし(switch なし) |
約350行 |
| Go | if |
for (4つの形式) |
自動改行 | 約200行 |
forに統一し、switchのデフォルトの挙動からbreakを削除します。キーワードが1つ減り、バグの種類も10種類減ります。
3. if文
(1) 基本的な if / else
package main
import "fmt"
func main() {
score := 85
if score >= 90 {
fmt.Println("Excellent")
} else if score >= 60 {
fmt.Println("Passing Grade")
} else {
fmt.Println("Fail")
}
}
出力:
Passing Grade
else キーワードは } と同じ行に記述する必要があります(中括弧形式が必須です)。}else{ という構文では、コンパイルエラーが発生します。
▶ サンプル:成績判定
package main
import "fmt"
func judgeGrade(score int) string {
if score >= 90 {
return "A"
} else if score >= 80 {
return "B"
} else if score >= 70 {
return "C"
} else if score >= 60 {
return "D"
} else {
return "F"
}
}
func main() {
fmt.Println(judgeGrade(95)) // A
fmt.Println(judgeGrade(72)) // C
fmt.Println(judgeGrade(50)) // F
}
出力:
A
C
F
(3) if 初期化文(Go言語の機能)
if では、最初に初期化文を実行し、その後に条件チェックを行うことができます。これにより、変数のスコープを制限することができます:
package main
import (
"fmt"
"strconv"
)
func main() {
// Syntax: if initialization statement; condition { ... }
// The variable `err` is scoped only within the `if-else` block.
if n, err := strconv.Atoi("123"); err == nil {
fmt.Printf("Conversion successful: %d\n", n)
} else {
fmt.Printf("Conversion failed: %v\n", err)
}
}
出力:
Conversion successful: 123
▶ サンプル:ファイルの読み込みと閉じ方 defer
package main
import (
"fmt"
"os"
)
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // automatically close file before function returns
data := make([]byte, 1024)
n, _ := file.Read(data)
fmt.Printf("%d bytes read\n", n)
return nil
}
func main() {
readFile("config.txt")
}
(5) if ステートメントにおけるエラー処理の一般的な手法(Go言語の特徴)
Go言語におけるエラー処理は、if文を用いて行われます。一般的な使用法:
if err := doSomething(); err != nil {
return err // early return
}
// Normal Logic
error 型について詳しく解説し、このパターンにおけるベストプラクティスについても説明します。
4. ブランチの切り替え(自動ブレーク)
(1) スイッチの基礎:自動ブレーク
デフォルトでは、Goのswitch文は伝播しません。つまり、いずれかのcaseに一致すると自動的に終了します:
package main
import "fmt"
func main() {
day := "Monday"
switch day {
case "Monday":
fmt.Println("Monday: Meeting Day")
case "Tuesday":
fmt.Println("Tuesday: Development Day")
case "Friday":
fmt.Println("Friday: Demo Day")
default:
fmt.Println("Weekend: Day off")
}
}
出力:
Monday: Meeting Day
(2) C/Javaとの比較:breakは不要
| 言語 | 構文 | 自動改行 |
|---|---|---|
| C/C++ | case 1: ...; case 2: ... |
マニュアルが必要 break; |
| Java | case 1: ...; case 2: ... |
手動での操作が必要 break; |
| Go | case "Monday": ... |
自動改行 |
fall-through を使用したい場合は、fallthrough キーワードを使用してください:
switch n {
case 1:
fmt.Println("One")
fallthrough // force execution of the next case
case 2:
fmt.Println("Two")
}
出力:
One
Two
(3) 表現形式を切り替える(タグなし)
switch の後に変数を続けてはいけません。各 case の条件は個別に記述してください:
package main
import "fmt"
func gradeLevel(score int) string {
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
case score >= 60:
return "C"
default:
return "D"
}
}
func main() {
fmt.Println(gradeLevel(95))
fmt.Println(gradeLevel(72))
}
出力:
A
C
▶ サンプル:複数値の一致による切り替え + HTTP ステータスコードによるルーティング
package main
import "fmt"
func routeStatus(code int) string {
switch code {
case 200, 201, 204:
return "Success"
case 301, 302:
return "Redirect"
case 400, 401, 403, 404:
return "Client Error"
case 500, 502, 503:
return "Server Error"
default:
return "Unknown"
}
}
func main() {
fmt.Println(routeStatus(200)) // Success
fmt.Println(routeStatus(404)) // Client Error
fmt.Println(routeStatus(500)) // Server Error
}
出力:
Success
Client Error
Server Error
5. forループの4つの形式
Go言語では、すべてのループがforという構文の下で統一されており、while、do-while、および無限ループを実装するために異なる構文が使用されています。
(1) 古典的な3部構成の「for」ループ
package main
import "fmt"
func main() {
for i := 1; i <= 3; i++ {
fmt.Printf("The %dth time\n", i)
}
}
出力:
The 1th time
The 2th time
The 3th time
▶ サンプル:「while」スタイル(条件付きループ)
package main
import "fmt"
func main() {
n := 1
for n < 5 { // equivalent to while (n < 5)
fmt.Printf("n=%d\n", n)
n++
}
}
出力:
n=1
n=2
n=3
n=4
(3) 無限ループ
package main
import (
"fmt"
"time"
)
func main() {
// Classic Infinite Loop (In real-world scenarios, it would use `break` or `return`)
for {
fmt.Println("Print once per second...")
time.Sleep(1 * time.Second)
break // prevent actual infinite loop, for demo only
}
}
出力:
Print once per second...
▶ サンプル:range を使用してスライス、マップ、または文字列を反復処理する
package main
import "fmt"
func main() {
scores := []int{95, 82, 67}
// Iterate through the slice: index, value
for i, score := range scores {
fmt.Printf("Problem %d: %d points\n", i+1, score)
}
// If only value is present: use _ to ignore index
for _, score := range scores {
fmt.Printf("Score: %d\n", score)
}
// Iterate through a map (key-value)
ages := map[string]int{"Alice": 28, "Bob": 32}
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
// Iterate over a string (by rune, not by byte)
for i, r := range "Go" {
fmt.Printf("Position %d: %c (code point %d)\n", i, r, r)
}
}
出力(マップの順序はランダムです):
Problem 1: 95 points
Problem 2: 82 points
Problem 3: 67 points
Score: 95
Score: 82
Score: 67
Alice is 28 years old
Bob is 32 years old
Position 0: G (code point 71)
Position 1: o (code point 111)
range map の順序は保証されていません。実行のたびに異なる場合があります。一貫した順序が必要な場合は、反復処理を行う前にキーをソートしてください。
(5) 「for」の4つの用法に関するクイックリファレンス
| 形式 | 構文 | 代替案 |
|---|---|---|
| 標準 | for i := 0; i < n; i++ {} |
C/Java for |
| 「while」スタイル | for condition {} |
C/Java while |
| インフィニット・ループ | for {} |
while(true) |
| 範囲の反復処理 | for i, v := range slice {} |
Python for...in |
flowchart LR
A[for init; condition; post] --> B{condition true?}
B -->|yes| C[execute loop body]
C --> D[execute post statement]
D --> B
B -->|no| E[exit loop]
style A stroke-width:2px
style E stroke:#f44,stroke-width:2px
6. defer:実行の延期(LIFO順序)
defer は、現在の関数が戻る直前に、ステートメントの実行を延期します。これは、リソースのクリーンアップ(ファイルの閉じ、リソースのロック解除、接続の切断など)によく使用されます。
(1) defer の基本
package main
import "fmt"
func main() {
fmt.Println("1: Start of function")
defer fmt.Println("3: defer execution (before the function returns)")
fmt.Println("2: Function Body")
}
出力:
1: Start of function
2: Function Body
3: defer execution (before the function returns)
(2) LIFO(後入れ先出し)順序を延期する
package main
import "fmt"
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("Main logic complete")
}
出力:
Main logic complete
3
2
1
なぜLIFOなのか? それは、deferが「スタックベースのリソース管理」をシミュレートしているためです。つまり、最後に開かれたリソースが最初に閉じられます。
▶ サンプル:deferの実用例 — データベース接続の閉じ方
package main
import (
"database/sql"
"fmt"
"log"
)
func queryUser(db *sql.DB, userID int) {
// Get the connection
rows, err := db.Query("SELECT name FROM users WHERE id = ?", userID)
if err != nil {
log.Fatal(err)
}
// Key Point: `defer` releases resources (LIFO)
defer rows.Close() // Close result set when function returns
// Business logic...
for rows.Next() {
var name string
rows.Scan(&name)
fmt.Printf("User: %s\n", name)
}
}
func main() {
fmt.Println("Example of Resource Management with `defer`")
// Real-world scenario: db, _ := sql.Open("mysql", "...")
// queryUser(db, 1)
}
return の途中で defer file.Close() が呼び出されても、defer file.Close() は実行されます。これは Go のリソース管理の代表的なユースケースです。注:この関数内で開いたリソースのクリーンアップは defer のみで行ってください。db が渡されており、そのライフサイクルは呼び出し元によって管理されているため、ここで defer db.Close() を行わないでください。
7. break / continue / label:正確なジャンプ
(1) break:ループを終了する
package main
import "fmt"
func main() {
for i := 1; i <= 10; i++ {
if i == 5 {
break // exit the entire loop
}
fmt.Printf("%d ", i)
}
fmt.Println("End of loop")
}
出力:
1 2 3 4 End of loop
▶ サンプル:スキップを継続し、奇数を1ずつ増やす
package main
import "fmt"
func sumOdd(n int) int {
sum := 0
for i := 1; i <= n; i++ {
if i%2 == 0 {
continue // skip even numbers
}
sum += i
}
return sum
}
func main() {
fmt.Printf("1-10: Sum of odd numbers: %d\n", sumOdd(10))
}
出力:
1-10: Sum of odd numbers: 25
(3) ネストされたループから抜け出すには label を使用する
label はループに名前を付けます。break label は指定されたループを終了します:
package main
import "fmt"
func main() {
outer:
for i := 1; i <= 3; i++ {
for j := 1; j <= 3; j++ {
if i*j == 6 {
fmt.Printf("Hit: i=%d, j=%d\n", i, j)
break outer // directly exit the outer loop
}
fmt.Printf("(%d, %d) ", i, j)
}
}
fmt.Println("Done")
}
出力:
(1, 1) (1, 2) (1, 3) (2, 1) (2, 2) (2, 3) (3, 1) (3, 2) Hit: i=3, j=2
Done
(4) 3種類のリダイレクトの比較
| キーワード | 機能 | 対象外 |
|---|---|---|
break |
現在のループまたはswitch文を終了する | 単一レベル |
continue |
この反復の残りをスキップ | 単一レベル |
break label |
指定されたラベルでループを終了 | ネスト |
8. 完全な例:オンライン試験採点システム
このレッスンで学んだすべての制御フローを連携させ、多次元試験採点システムを実装しましょう:
// exam_evaluator.go
package main
import "fmt"
// Structure of the Problem
type Question struct {
Type string // single/multi/judge
Correct string // correct answer
Student string // student answer
Score float64 // score value
}
// Scoring Function: Initialized with an `if` statement + `switch` expression + `for range`
func evaluateExam(name string, questions []Question) (total float64, details []string) {
defer func() {
// defer: Collect results (ensures that results are collected even if a return statement is encountered midway)
details = append(details, fmt.Sprintf("[%s] Total score: %.1f", name, total))
}()
for i, q := range questions {
var earned float64
// switch expression format: multi-condition branching
switch q.Type {
case "single":
if q.Student == q.Correct {
earned = q.Score
}
case "judge":
if q.Student == q.Correct {
earned = q.Score
} else {
earned = 0
}
case "multi":
// Multiple-choice question: Points are awarded only for exact matches
if q.Student == q.Correct {
earned = q.Score
} else {
earned = 0
}
default:
continue // skip unknown question type
}
total += earned
details = append(details,
fmt.Sprintf("Problem %d (%s): Scored %.1f points", i+1, q.Type, earned))
}
return total, details
}
func main() {
questions := []Question{
{Type: "single", Correct: "B", Student: "B", Score: 2},
{Type: "judge", Correct: "true", Student: "true", Score: 1},
{Type: "multi", Correct: "A,B,C", Student: "A,B", Score: 3},
{Type: "single", Correct: "D", Student: "D", Score: 2},
}
// Use a `for range` loop to iterate through the student list
students := []string{"Alice", "Bob", "Charlie"}
for _, name := range students {
total, details := evaluateExam(name, questions)
fmt.Println("========================================")
for _, d := range details {
fmt.Println(d)
}
}
}
期待される出力(各学生の詳細な成績+成績概要):
========================================
Problem 1 (single): Scored 2.0 points
Problem 2 (judge): Scored 1.0 points
Problem 3 (multi): Scored 0.0 points
Problem 4 (single): Scored 2.0 points
[Alice] Total score: 5.0
========================================
Problem 1 (single): Scored 2.0 points
Problem 2 (judge): Scored 1.0 points
Problem 3 (multi): Scored 0.0 points
Problem 4 (single): Scored 2.0 points
[Bob] Total score: 5.0
========================================
Problem 1 (single): Scored 2.0 points
Problem 2 (judge): Scored 1.0 points
Problem 3 (multi): Scored 0.0 points
Problem 4 (single): Scored 2.0 points
[Charlie] Total score: 5.0
defer 内の名前付き戻り値を修正する必要があります。上記の例では、details が名前付き戻り値 (total float64, details []string) を使用しているため、defer 内の append が有効になります。
❓ よくある質問
whileというキーワードがないのですか?forを使用します。for condition {}はwhileと同等であり、for {}はwhile(true)と同等です。キーワードが1つ少ないことで、チームはwhileとdo-whileのどちらを使うかについて議論する必要がなくなります。switch がデフォルトでフォールスルーしないのは面倒ではないですか?switch のケースの 90% は自己完結的であるべきだと考えています。C や Java における「break の記述を忘れてフォールスルーを引き起こしてしまう」という問題は、典型的なバグの一つです。Goでは、fallthroughを使用してフォールスルーする意図を明示的に示すため、より安全です。for range で反復処理を行うと、マップの順序がランダムになるのはなぜですか?keys := make([]string, 0, len(m)); for k := range m { keys = append(keys, k) }; sort.Strings(keys)。defer はいつ実行されますか?複数の defer ステートメントの実行順序はどのようになりますか?defer は、現在の関数が戻る前に実行されます(つまり、return ステートメントが実行された後、関数が実際に終了する前です)。複数の defer ステートメントは LIFO(後入れ先出し)順に実行され、スタックベースのリソース管理をシミュレートします。つまり、スタックに最後にプッシュされたリソースが最初に解放されます。if 初期化文のスコープはどのようになりますか?if 初期化文で宣言された変数は、if-else ブロック内でのみ参照可能であり、if-else ブロックを離れると無効になります。これにより、「変数が外側のスコープに漏れ出す」というよくあるバグを防ぐことができます。labelは実際のプロジェクトでよく使われますか?labelは主にネストされたループから抜け出すために使われますが、99%のケースでは、breakとreturnを組み合わせれば十分です。labelを使いすぎるとコードの可読性が低下します。これはgotoの時代から学んだ教訓です。switchという記述は型チェックに対応していますか?switch x := y.(type)はインターフェースの型チェックに使用されます。これについては第7課で詳しく説明します。ただし、この構文はinterface{}型の変数にのみ適用されます。continue と break を switch ステートメント内で使用できますか?break は switch ステートメントを終了させますが、for ループ内では for ループを終了させます。continueはforループ内でのみ使用でき、switchステートメント内では使用できません(switchはループとは見なされないため)。📖 まとめ
- Goでは、わずか4つのキーワード(
if/switch/for/defer)で、すべてのフロー制御構造を網羅しています。 ifは初期化文をサポートしており、これにより変数のスコープがif-elseブロック内に限定されますswitchはデフォルトで自動的に改行されます。ステートメントをそのまま実行したい場合は、明示的にfallthroughを使用してください。whileおよびdo-whileの代わりとなるforの4つの形式:for init;cond;post{}/for cond{}/for{}/for rangedeferは関数が戻る前に実行されます。複数のdeferステートメントは LIFO(後入れ先出し)の順序で実行されます。breakはループを終了し、continueは現在の反復をスキップし、break labelは指定されたラベルの位置でループを終了します- Goのフロー制御設計の哲学:キーワードの削減 + 自動
break+ LIFOリソース管理
📝 練習問題
-
基本問題(難易度 ⭐):
forとswitchを使用して FizzBuzz プログラムを作成してください。1 から 100 までの数字を出力します。3の倍数に遭遇した場合は「Fizz」を、5の倍数に遭遇した場合は「Buzz」を、15の倍数に遭遇した場合は「FizzBuzz」を出力してください。 -
上級問題(難易度 ⭐⭐):1 から n までの間の素数の個数を返す関数
countPrimes(n int) intを実装してください。要件: (1)forループを使用すること;(2)if文を使用して素数であるかどうかを判定すること;(3) 関数が戻る前に、deferを使用して実行時間(time.Now()を使用)を出力すること。 -
チャレンジ問題(難易度 ⭐⭐⭐):学生の成績解析ツールを実装してください。一連の得点 ([]float64) が与えられたとき、(1) 平均得点、(2) 合格率(60点以上)、 (3) 最高点と最低点;(4) 成績別内訳(A/B/C/D/F)。要件:(1)
for rangeを使用すること;(2)switchステートメントを使用して成績を判定すること;(3) 関数が戻る前にdeferを使用して要約情報を出力すること。