else キーワードは、直前の if の条件が false のときにブロックを実行します。別の条件を調べるには else if と書き、必要なだけ連ねられます。else は if ブロックの閉じ波括弧と同じ行に置く必要があり、そうしないとコンパイルできません。Go らしいコードでは、ほかの多くの言語より else を使う場面が少なくなります。if ブロックが return で終わるなら、if の後ろのコードがすでに else の役割を果たしています。そのため Go のコードは早めに return し、正常系のコードをインデントせずに残します(Go spec)。
要約
if cond { ... } else { ... }は、両方のブロックに波括弧が必要で、条件を丸括弧で囲みません。} else {は 1 行に書く必要があります。}の後で改行するとif文がそこで終わり、コンパイラはsyntax error: unexpected keyword else, expected }を報告します。else ifの連鎖は上から順に評価され、最初に true になった条件が採用されます。長い連鎖は、式を持たないswitchにしたほうが読みやすくなります。ifの短い文(if v, err := f(); err != nil)で宣言した変数は、すべてのelse ifとelseの分岐から見えます。最後の}の後では使えません。ifブロックが return するならelseは外します。revivelinter は、elseが残った形をif block ends with a return statement, so drop this else and outdent its blockで指摘します。- Go に
?:演算子はありません。if/elseを使うか、ゼロ値のときのデフォルトだけが必要ならcmp.Orを使います。
Go で if/else はどう書くのか?
if は bool の条件とブロックを取ります。その後ろの else ブロックは、条件が false のときに実行されます。例として、リクエストが失敗した後のリトライ判定を使います。
example.gogopackage main import "fmt" func main() { attempt := 3 maxAttempts := 5 if attempt < maxAttempts { fmt.Printf("attempt %d failed, retrying\n", attempt) } else { fmt.Printf("attempt %d failed, giving up\n", attempt) } }
example.texttextattempt 3 failed, retrying
C、Java、JavaScript とは 3 つのルールが異なります。
- 条件を丸括弧で囲みません。
if (attempt < maxAttempts) {もコンパイルは通りますが、gofmtが丸括弧を削除します。 - 本体が 1 行でも波括弧が必要です。
if code >= 500 fmt.Println("server error")はsyntax error: unexpected name fmt, expected {で失敗します。 - 条件は
boolでなければなりません。Go には truthy な値がないので、if len(items)やif userは型エラーになります。if len(items) > 0やif user != nilと書きます。
同じ波括弧のルールは else にも当てはまります。else の後ろに文をそのまま書くと syntax error: else must be followed by if or statement block で失敗します。else の後ろに置けるのは、別の if か { ... } ブロックだけです。条件と初期化文については、if 側から Go の if キーワードで解説しています。
else を } と同じ行に書かなければならないのはなぜか?
Go の文法では文の終わりをセミコロンで示しますが、自分でセミコロンを書くことはほとんどありません。行の最後のトークンが識別子、リテラル、break、continue、fallthrough、return のいずれかのキーワード、++ などの演算子、または閉じる )、]、} のとき、字句解析器が行末にセミコロンを挿入します(Go spec, Semicolons)。
次のコードが失敗するのは、このルールのためです。
example.gogoif retries > 0 { fmt.Println("retrying") } else { fmt.Println("giving up") }
example.texttext./main.go:10:2: syntax error: unexpected keyword else, expected }
} で終わる行にはセミコロンが挿入され、そこで if 文が終わります。すると次の行は else から始まる新しい文になりますが、else で始まる文は存在しません。直すには } else { を 1 行に書きます。これは gofmt が出力する形でもあります。Go で開き波括弧を if と同じ行に書くのも、同じルールによるものです。
Allman スタイル(波括弧を独立した行に書く)に慣れた C や Java のプログラマーは、最初の 1 週間でこの問題にぶつかります。ただ、どの Go のコードベースでも if/else の書き方が同じになるので、レビューでスタイルが話題になることはありません。
Go の else if はどう動くのか?
else if は独立したキーワードではありません。else の後ろに別の if 文が続いているだけで、文法でも "else" ( IfStmt | Block ) とそのまま認められています。必要なだけ連ねられます。Go は上から順に条件を調べ、最初に true になった分岐を実行します。
次のループは、リクエストのレイテンシをメトリクスのバケットに振り分けます。
example.gogopackage main import ( "fmt" "time" ) func main() { latencies := []time.Duration{ 40 * time.Millisecond, 250 * time.Millisecond, 3 * time.Second, 90 * time.Millisecond, } var fast, slow, timedOut int for _, d := range latencies { if d < 100*time.Millisecond { fast++ } else if d < time.Second { slow++ } else { timedOut++ } } fmt.Printf("fast=%d slow=%d timed_out=%d\n", fast, slow, timedOut) }
example.texttextfast=2 slow=1 timed_out=1
順番が重要です。最初の 2 つの条件を入れ替えて d < time.Second を先にすると、出力は fast=0 slow=3 timed_out=1 になります。100ms 未満のリクエストはすべて 1 秒未満でもあるので、範囲の広い条件が先に拾ってしまい、fast の分岐は一度も実行されません。範囲の狭い条件を先に置きます。
連鎖が 3 つの分岐を超えたら、式を持たない switch のほうがたいてい読みやすくなります。各 case が条件になり、評価は同じく上から順に行われ、最後の else の代わりに default を使います。
example.gogofor _, d := range latencies { switch { case d < 100*time.Millisecond: fast++ case d < time.Second: slow++ default: timedOut++ } }
出力は同じです。Go の switch が C とどう違うかは、Go キーワード解説の switch のセクションを参照してください。switch と select では「どれにも当てはまらない」場合に default を使うので、else が現れるのは if の後ろだけです。
if 文で宣言した変数のスコープはどこまでか?
if は短い文から始められます。多くの場合は := による宣言です。そこで宣言した変数のスコープは if 文全体で、すべての else if と else の分岐も含みます。仕様では、各 if はそれぞれ暗黙のブロックに置かれ、else の分岐はその内側にあります(Go spec, Blocks)。
ここは else をほかの書き方に置き換えにくい場面です。環境変数から読んだ PORT の値をチェックするには、どの分岐でもパースした数値が必要になります。
example.gogopackage main import ( "fmt" "strconv" ) func main() { for _, raw := range []string{"8080", "80", "http"} { if port, err := strconv.Atoi(raw); err != nil { fmt.Printf("PORT=%q is not a number: %v\n", raw, err) } else if port < 1024 { fmt.Printf("PORT=%d is privileged (below 1024)\n", port) } else { fmt.Printf("PORT=%d ok\n", port) } } }
example.texttextPORT=8080 ok PORT=80 is privileged (below 1024) PORT="http" is not a number: strconv.Atoi: parsing "http": invalid syntax
port と err は 3 つの分岐すべてで使えます。閉じ波括弧の後では、もう存在しません。if の後ろに fmt.Println("listening on", port) を追加すると、ビルドは undefined: port で失敗します。後でその値が必要なら、if より前に独立した行で宣言します。
シャドーイングの落とし穴
短い文は新しい変数を宣言するので、外側にある同じ名前の変数を隠してしまうことがあります。次のコードはコンパイルが通り、go vet も通ります。
example.gogovar cfg Config if cfg, err := loadConfig(data); err != nil { log.Fatal(err) } else { fmt.Printf("loaded config, addr=%q\n", cfg.Addr) } fmt.Printf("starting server, addr=%q\n", cfg.Addr)
example.texttextloaded config, addr=":8443" starting server, addr=""
if の中の cfg は新しい変数です。外側の cfg はゼロ値のままなので、サーバーは空のアドレスで起動します。cfg, err := loadConfig(data) を独立した行に書き、err は普通の if でチェックします。シャドーイングと、それを検出する shadow アナライザーについては、Go の var キーワードで解説しています。
Go らしいコードで else を避けるのはなぜか?
Go の関数の多くは、それぞれが失敗しうる手順の連続です。各手順を前の手順の else の中に入れ子にしていくと、成功時のコードはどんどん右にずれ、エラー処理はその原因となったチェックから遠く離れていきます。次の注文ハンドラーはその書き方になっています。
example.gogofunc handleCreateOrder(w http.ResponseWriter, r *http.Request) { var req CreateOrderRequest if err := json.NewDecoder(r.Body).Decode(&req); err == nil { if req.Quantity > 0 { if id, err := saveOrder(req); err == nil { w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]string{"id": id}) } else { http.Error(w, "could not save order", http.StatusInternalServerError) } } else { http.Error(w, "quantity must be positive", http.StatusBadRequest) } } else { http.Error(w, "invalid JSON body", http.StatusBadRequest) } }
不正な JSON のときに何が起きるかを知るには、関数の一番下まで読まなければなりません。各条件を反転させて、エラーの場合を先に処理して return するようにします。
example.gogofunc handleCreateOrder(w http.ResponseWriter, r *http.Request) { var req CreateOrderRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON body", http.StatusBadRequest) return } if req.Quantity <= 0 { http.Error(w, "quantity must be positive", http.StatusBadRequest) return } id, err := saveOrder(req) if err != nil { http.Error(w, "could not save order", http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(map[string]string{"id": id}) }
4 種類のボディ(壊れた JSON、数量 0、在庫切れの SKU、正しい注文)で両方のバージョンを httptest で実行すると、ステータスコードもボディも同じになります。2 つ目のバージョンには else が 1 つもありません。各チェックはそのエラーレスポンスのすぐ隣にあり、最後の 2 行が左端にある成功時のパスです。4 つ目の検証を追加するときも、入れ子を 1 段深くするのではなく、if ブロックを 1 つ足すだけです。
Go チームはこのスタイルを Effective Go で説明しています。「エラーの場合は return 文で終わることが多いので、結果としてコードに else 文は必要なくなる。」Go Code Review Comments のページでは、これを Indent Error Flow と呼んでいます。「通常のコードパスのインデントは最小限に保ち、エラー処理をインデントして先に片付けるようにする。」
linter は else について何を指摘するか
revive linter(golint の後継としてメンテナンスされているもの)は、else に関するルールを 2 つデフォルトで有効にしています。次のコードに対して実行すると、
example.gogofunc readTimeout() (int, error) { secs, err := strconv.Atoi(os.Getenv("TIMEOUT_SECONDS")) if err != nil { return 0, fmt.Errorf("parse TIMEOUT_SECONDS: %w", err) } else { return secs, nil } } func countValid(lines []string) int { n := 0 for _, line := range lines { if line == "" { continue } else { n++ } } return n }
次のように報告されます。
example.texttextlint.go:13:9: if block ends with a return statement, so drop this else and outdent its block lint.go:23:10: if block ends with a continue statement, so drop this else and outdent its block
1 つ目は indent-error-flow によるもので、if ブロックが return で終わるときに報告されます。2 つ目は superfluous-else によるもので、continue、break、goto のほか、panic、os.Exit、log.Fatal のように戻ってこない呼び出しも対象です。revive には、else ブロックの側が return する逆の形を対象にした、オプトインの early-return ルールもあります。golangci-lint でも、revive linter を通じて同じルールを使えます。
上の入れ子のハンドラーは、どちらのデフォルトのルールにも引っかかりません。どのブロックも return しないからです。linter が検出するのは機械的に判断できるケースだけで、成功チェックのピラミッドを平らにするリファクタリングは自分で行う必要があります。
Go で else を使うべきなのはどんなときか?
else を外すのは、ブロックが処理を抜ける場合のルールです。両方の分岐で実際に処理を行い、どちらも関数を抜けないなら、「2 つのうちどちらか」を表すには else がいちばん明確です。そうした状況は 3 つあります。
1 つ目は、どちらの結果でも処理を続ける場合です。キャッシュの検索では、ヒットかミスかを数えてから処理を続けます。どちらの分岐も return しないので、インデントを戻す対象がありません。
example.gogoif entry, ok := cache[key]; ok { hits++ resp = entry } else { misses++ resp = fetchFromOrigin(key) cache[key] = resp }
2 つ目は、2 つの値から 1 つを選ぶ場合です。コストのかからない値なら、デフォルトを設定してから上書きします。
example.gogolevel := slog.LevelInfo if debug { level = slog.LevelDebug }
コネクションを開くなど、デフォルトを作るのにコストや副作用がある場合は、if/else を使って片方の分岐だけが実行されるようにします。
example.gogovar store Store if redisURL != "" { store = RedisStore{url: redisURL} } else { store = MemoryStore{} }
デフォルトを設定して上書きする形にすると、var store Store = MemoryStore{} の後で置き換えることになります。ここでそれが成り立つのは、MemoryStore{} にコストがかからないからにすぎません。デフォルトがファイルやコネクションプールを開くものなら、上書きによって一度も使わないリソースを捨てることになります。
3 つ目は、短い文で宣言した変数を使う場合です。PORT の例のように、すべての分岐でその変数が必要なら、else を使うとスコープをチェックの中に閉じ込められます。if の前で宣言しても動きますが、その場合は関数の残りの部分でも変数がスコープに残ります。
Go に三項演算子はあるか?
ありません。Go には cond ? a : b がありません。その理由は Go FAQ で説明されています。「Go に ?: がないのは、この演算が不可解なほど複雑な式を作るのに使われすぎているのを、言語の設計者たちが見てきたからである。if-else の形は長くなるが、明らかにわかりやすい。言語に必要な条件分岐の制御構造は 1 つだけである。」
代わりに使うのは、if/else か、前のセクションで紹介したデフォルトを設定して上書きする形です。「空でなければこの値を使う」というよくあるケースのために、Go 1.22 で cmp.Or が追加されました。cmp.Or は、引数のうちゼロ値でない最初のものを返します。
example.gogopackage main import ( "cmp" "fmt" "os" ) func main() { addr := cmp.Or(os.Getenv("LISTEN_ADDR"), ":8080") fmt.Println("listening on", addr) }
example.texttextlistening on :8080
cmp.Or は演算子ではなく関数なので、Go は呼び出しの前にすべての引数を評価します。cmp.Or(cachedToken, fetchToken()) は、cachedToken が設定されているときでも毎回 fetchToken を呼び出します。汎用の ternary(cond, a, b) ヘルパーにも同じ問題があり、Go のコードベースでそうしたヘルパーをめったに定義しない理由の 1 つになっています。分岐にコストがあるなら if を書きます。
LevelUpGo で学ぶ
LevelUpGo では、ブラウザ上で本物の Go コードを実行する演習を通じて Go を学べます。Go Basics では、ゼロから言語を学ぶ流れの中で、if、else if、switch、エラー時の早期リターンを扱います。Simplification には、入れ子になったコードを平らにする、真偽値のロジックを単純にする、else if の連鎖を switch に置き換える、といったレッスンがあります。残り 24 個の予約語については、Go のキーワード一覧:全 25 個の予約語を解説をご覧ください。
よくある質問
Go の else は何をしますか?
else は、直前の if の条件が false のときにブロックを実行します。後ろにはブロック(else { ... })か、別の if(else if cond { ... })を続けられます。Go の 25 個の予約語の 1 つで、if ブロックの後ろにしか書けません。
Go で「syntax error: unexpected keyword else」と表示されるのはなぜですか?
if ブロックの閉じ } の後で改行し、else を次の行に書いているからです。Go は行末の } の後にセミコロンを挿入し、そこで if 文が終わるので、次の行の else はつながる先がありません。} else { を 1 行に書くか、gofmt を実行してください。
Go に elif や elseif はありますか?
ありません。Go では else if と 2 語で書きます。これは else の後ろに新しい if 文が続いたものです。長い連鎖は、キーワードの後ろに式を持たない switch にしたほうがすっきり読めます。
Go で return の後に else を使うべきですか?
使うべきではありません。if ブロックが return で終わるなら、if の後ろのコードは条件が false だったときにしか実行されないので、else はインデントを増やすだけです。revive は indent-error-flow ルールでこれを指摘します。Go Code Review Comments も、エラーを先に処理して通常のパスをインデントしないよう勧めています。
if 文の変数を else ブロックで使えますか?
使えます。if n, err := strconv.Atoi(s); err != nil のように短い文で宣言した変数は、if ブロックと、すべての else if と else の分岐でスコープ内にあります。最後の閉じ波括弧の後でスコープから外れます。
Go で if/else を 1 行で書くには?
フォーマット済みのコードでは書けません。1 行の if/else もコンパイルは通りますが、gofmt が複数行に分割します。また、Go には三項演算子がありません。普通の if/else を使うか、デフォルトを代入して if で上書きするか、ゼロ値のときの代替値が必要なら cmp.Or を使います。
出典
- The Go Programming Language Specification, If statements: https://go.dev/ref/spec#If_statements
- The Go Programming Language Specification, Semicolons: https://go.dev/ref/spec#Semicolons
- The Go Programming Language Specification, Blocks: https://go.dev/ref/spec#Blocks
- Effective Go, If: https://go.dev/doc/effective_go#if
- Go Code Review Comments, Indent Error Flow: https://go.dev/wiki/CodeReviewComments#indent-error-flow
- Go FAQ, Does Go have the ?: operator?: https://go.dev/doc/faq#Does_Go_have_a_ternary_form
- revive rules (indent-error-flow, superfluous-else, early-return): https://github.com/revive-lint/revive/blob/master/RULES_DESCRIPTIONS.md
- cmp package, Or: https://pkg.go.dev/cmp#Or
