if キーワードは、bool の条件が真のときにコードのブロックを実行します。Go の if には、C、JavaScript、Python から来た人が戸惑うルールが 3 つあります。条件に括弧は付けず、波括弧は常に必須で、条件は本物の bool でなければなりません。Go には truthy や falsy という概念がないからです。また、if は短い文から始めることもできます。if err := f(); err != nil がほぼすべての Go のファイルに登場するのはそのためです(Go spec)。
要約
if status >= 500 { ... }のように、条件を括弧で囲みません。本体が 1 行でも波括弧は必須です。- 条件の型は
boolでなければなりません。if retries { ... }はnon-boolean condition in if statementで失敗します。 elseは閉じ波括弧}と同じ行に書く必要があります。次の行に単独で書くと構文エラーになります。if v, err := parse(s); err != nil { ... }で宣言した変数は、そのifとすべてのelse分岐の中にだけ存在し、文の後では使えません。ifブロックの中の:=は外側の変数をシャドーイングすることがあります。コードはコンパイルでき、古い値が黙って使われます。- Go らしいコードでは、エラーが起きたらすぐにリターンし、正常系のコードをインデントさせずに書きます。
returnの後のelseはたいてい削除します。 - Go に三項演算子はありません。代入と
ifの組み合わせ、デフォルト値にはcmp.Or、範囲の制限にはminとmaxを使います。 &&と||は短絡評価されるので、req.User != nil && req.User.IsAdminが nil ポインタを参照外しすることはありません。- 長い
if/else ifの連鎖は、たいてい式のないswitchにしたほうが読みやすくなります。
Go で if 文を書くには?
if、bool の式、波括弧で囲んだブロックの順に書きます。次の例は、HTTP クライアントのリトライ判定です。
example.gogopackage main import "fmt" func shouldRetry(status, attempt, maxAttempts int) bool { if attempt >= maxAttempts { return false } return status == 429 || status >= 500 } func main() { fmt.Println(shouldRetry(503, 1, 3)) fmt.Println(shouldRetry(503, 3, 3)) fmt.Println(shouldRetry(404, 1, 3)) }
example.texttexttrue false false
attempt >= maxAttempts は括弧で囲まれていません。括弧を書いてもコンパイルは通りますが、次にファイルを保存したときに gofmt が取り除きます。Go が譲らないのは波括弧のほうです。if attempt >= maxAttempts return false を 1 行で書いてもパースできませんし、条件の次の行に文を書いた場合も同じです。
example.gogoif status >= 500 fmt.Println("server error")
example.texttext./main.go:8:19: syntax error: unexpected newline, expected { after if clause
波括弧が必須なので、C でよくあるバグが起きません。波括弧のない if の下に 2 行目を追加すると、条件の一部に見えるのに毎回実行されてしまうというバグです。2014 年に Apple で起きた TLS の「goto fail」バグは、まさにこの間違いでした。Go ではすべての if の本体に明確な境界があり、gofmt がどのコードベースでも同じようにインデントします。
Go に truthy や falsy の値はあるのか?
ありません。条件は bool 型の式でなければなりません。int、文字列、ポインタ、slice、error が自動的に true や false に変換されることはありません。
example.gogoretries := 3 if retries { fmt.Println("retrying") }
example.texttext./main.go:7:5: non-boolean condition in if statement
retries > 0、name != ""、user != nil、len(items) > 0、err != nil のように、意図した比較をそのまま書きます。数文字増えるだけで、何を確認しているかが明確になります。JavaScript の if (count) は count が 0 のときにブロックを飛ばすので、0 が正当な値である場合にはバグになりがちです。Go では、どの条件を判定しているのかが読む人に常に見えます。
Go の else と else if はどう動くのか?
else は条件が偽のときに実行され、else if は別の条件を判定します。次の例では、リクエストのロガーがステータスコードからログレベルを選びます。
example.gogofunc logRequest(ctx context.Context, logger *slog.Logger, path string, status int) { var level slog.Level if status >= 500 { level = slog.LevelError } else if status >= 400 { level = slog.LevelWarn } else { level = slog.LevelInfo } logger.Log(ctx, level, "request", "path", path, "status", status) }
example.texttextlevel=INFO msg=request path=/api/orders status=200 level=WARN msg=request path=/api/orders/99 status=404 level=ERROR msg=request path=/api/checkout status=502
分岐は上から順に判定され、最初に真になったものが実行されます。502 は先に status >= 500 に一致するので、status >= 400 の判定まで進みません。else と else if の連鎖については、Go の else キーワードで詳しく解説しています。
else を閉じ波括弧と同じ行に書かなければならないのはなぜか?
セミコロンの自動挿入があるからです。Go の文法では文の終わりにセミコロンを使い、字句解析器は }、識別子、リテラルなど、いくつかのトークンで終わる行の末尾にセミコロンを自動で追加します(Go spec)。そのため、単独の行にある } で if 文が終わります。すると次の行の else は新しい文の始まりになりますが、else で始まる文は存在しません。
example.gogoif status >= 500 { fmt.Println("server error") } else { fmt.Println("ok") }
example.texttext./main.go:10:2: syntax error: unexpected keyword else, expected }
直し方は、} else { を 1 行に書くことです。if、for、func の開き波括弧を単独の行に置けないのも同じルールによるものです。また、Go のプロジェクトでは波括弧のスタイルで揉めることがありません。コンパイラが受け付ける位置は 1 つだけで、それ以外の整形は gofmt が行います。
短い文を伴う if 文とは?
if は短い文から始めることができ、条件とはセミコロンで区切ります。その文が先に実行され、そこで宣言した変数のスコープは if に限られます。最もよく使われるのはエラー処理です。
example.gogofunc loadConfig(path string) (Config, error) { var cfg Config data, err := os.ReadFile(path) if err != nil { return cfg, fmt.Errorf("read config: %w", err) } if err := json.Unmarshal(data, &cfg); err != nil { return cfg, fmt.Errorf("parse %s: %w", path, err) } return cfg, nil }
json.Unmarshal は error しか返さないので、呼び出しと判定が 1 行に収まり、その err は閉じ波括弧の後には存在しません。一方、os.ReadFile はその後で必要なデータを返すので、独立した行に書きます。これが Go のコードでよく見る使い分けです。結果が判定にしか必要ないなら文を if の中に置き、関数の残りの部分で使うなら外に置きます。
値と bool を返す式なら、どれでも同じ形が使えます。Go ではこれを comma-ok イディオムと呼びます。map を検索すると、キーが存在したかどうかがわかります。
example.gogoroles := map[string]string{"u_42": "admin", "u_7": "billing"} if role, ok := roles["u_42"]; ok { fmt.Println("u_42 is", role) } if _, ok := roles["u_99"]; !ok { fmt.Println("u_99 has no role") }
example.texttextu_42 is admin u_99 has no role
型アサーションを使えば、panic の危険なしにオプションの振る舞いを確認できます。次のストリーミングハンドラーは、http.ResponseWriter が対応している場合にだけフラッシュします。
example.gogoif f, ok := w.(http.Flusher); ok { f.Flush() }
Go 1.26 では errors.AsType が追加されました。これは errors.As のジェネリック版で、一致したエラーと bool を返すので、同じパターンにそのまま収まります。次の例のサービスは、設定ファイルがなければデフォルト値で動きますが、壊れた設定ファイルでは失敗します。
example.gogofor _, path := range []string{"missing.json", "bad.json"} { _, err := loadConfig(path) if pathErr, ok := errors.AsType[*fs.PathError](err); ok { fmt.Println("no config file at", pathErr.Path, "so using defaults") } else if err != nil { fmt.Println("fatal:", err) } }
example.texttextno config file at missing.json so using defaults fatal: parse bad.json: invalid character '}' looking for beginning of object key string
以前の errors.As では、if の前に var pathErr *fs.PathError を宣言して &pathErr を渡すので、判定の後も変数が残り続けます。
if 文で宣言した変数のスコープは?
その変数は、条件、if ブロック、そしてそれに続くすべての else if と else のブロックの中に存在します。文が終わった後には存在しません(Go spec)。
example.gogoraw := "abc" if n, err := strconv.Atoi(raw); err != nil { fmt.Println("bad MAX_CONNS:", err) } else { fmt.Println("max conns", n) } fmt.Println(n)
else の分岐では n を使えますが、最後の行は undefined: n で失敗します。これは意図的なものです。変数が、それに依存すべきでないコードに漏れ出すことはなく、次の if で同じ名前をまた使えます。1 つの Go の関数に if err := ...; err != nil の判定が 10 回並んでも、10 個の別々のエラー名を用意せずに済むのはこのためです。
if の中の := がシャドーイングのバグを生むのはなぜか?
ブロックごとに新しいスコープが作られ、:= は常に現在のスコープで変数を宣言します。if ブロックの中から := で外側の変数に代入しようとすると、代わりに同じ名前の新しい変数ができてしまいます。
example.gogofunc requestTimeout() time.Duration { timeout := 5 * time.Second if raw := os.Getenv("HTTP_TIMEOUT"); raw != "" { timeout, err := time.ParseDuration(raw) if err != nil { fmt.Println("ignoring HTTP_TIMEOUT:", err) return 5 * time.Second } fmt.Println("HTTP_TIMEOUT set to", timeout) } return timeout }
example.texttextHTTP_TIMEOUT set to 30s using 5s
ブロックの中では err が新しいので := が使え、同時に 2 つ目の timeout もこっそり宣言されます。パースした値は内側の変数に入り、閉じ波括弧で消えてしまいます。コンパイラはこれを受け入れ、go vet のデフォルトのチェックも警告しません。直し方は、err を var err error で宣言して = を使うことです。そうすれば外側の timeout に代入されます。シャドーイングについては、これを検出する shadow アナライザーも含めて、Go の var キーワードで詳しく解説しています。
Go らしいコードで else を避けるのはなぜか?
Go の else 分岐の多くは、すでにリターンした if の後に続くものだからです。Effective Go は次のように述べています。if 文の本体が break、continue、goto、return で終わり、次の文へ処理が流れないなら、不要な else は省略する(Effective Go)。その結果、エラーは発生した場所で処理してリターンし、成功時の処理は関数の左端をまっすぐ下っていくコードになります。
次の返金ハンドラーは、チェックのたびに if を入れ子にしています。
example.gogofunc handleRefund(w http.ResponseWriter, r *http.Request) { user, ok := userFromContext(r.Context()) if ok { if user.CanRefund { var req RefundRequest if err := json.NewDecoder(r.Body).Decode(&req); err == nil { if req.AmountCents > 0 { fmt.Fprintf(w, "refund of %d cents queued for %s", req.AmountCents, req.OrderID) } else { http.Error(w, "amount_cents must be positive", http.StatusBadRequest) } } else { http.Error(w, "invalid JSON body", http.StatusBadRequest) } } else { http.Error(w, "forbidden", http.StatusForbidden) } } else { http.Error(w, "unauthorized", http.StatusUnauthorized) } }
動きはしますが、肝心の返金処理は 5 段階も深い位置にあり、各エラーメッセージはその原因となった条件から離れています。リクエストが 401 になる理由を知るには、最後の else と最初の if を対応させなければなりません。各条件を反転させて早めにリターンすれば、同じ動作のまま次のように書けます。
example.gogofunc handleRefund(w http.ResponseWriter, r *http.Request) { user, ok := userFromContext(r.Context()) if !ok { http.Error(w, "unauthorized", http.StatusUnauthorized) return } if !user.CanRefund { http.Error(w, "forbidden", http.StatusForbidden) return } var req RefundRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON body", http.StatusBadRequest) return } if req.AmountCents <= 0 { http.Error(w, "amount_cents must be positive", http.StatusBadRequest) return } fmt.Fprintf(w, "refund of %d cents queued for %s", req.AmountCents, req.OrderID) }
両方のバージョンに httptest で同じ 5 つのリクエストを送ると、まったく同じレスポンスが返ります。
example.texttext401 unauthorized 403 forbidden 400 invalid JSON body 400 amount_cents must be positive 200 refund of 4999 cents queued for ord_1001
フラットなバージョンの各チェックはガード節で、条件のすぐ下にそのレスポンスがあります。返金額の上限のような新しいルールを加えるときも、全体をもう 1 段階包むのではなく、最後の行の上に if ブロックを 1 つ追加するだけで済みます。
「drop this else and outdent its block」とはどういう意味か?
revive の indent-error-flow ルールが出すメッセージです。revive は golint の後継となったリンターで、golangci-lint の中で動いています。このルールは、return で終わる if ブロックの後に else が続く場合に警告します。
example.gogofunc parsePort(raw string) (int, error) { if port, err := strconv.Atoi(raw); err != nil { return 0, fmt.Errorf("invalid port %q: %w", raw, err) } else if port < 1 || port > 65535 { return 0, errors.New("port out of range") } else { return port, nil } }
example.texttextmain.go:14:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)
括弧内のヒントは if のスコープに関するものです。port は if 文の中で宣言されているので、else を削除すると return port, nil がスコープの外に出てしまいます。直し方は、port, err := strconv.Atoi(raw) を if の上の独立した行に移すことです。そうすれば、各チェックはリターンするだけの素直なガード節になります。
revive の superfluous-else ルールは、break、continue、goto、panic、os.Exit の後の同じパターンを扱います。オプションの early-return ルールは、else 分岐が return で終わる if/else を反転させるよう提案します。とはいえ、else が間違いだというわけではありません。前述の logRequest の例は else を正しく使っています。どの分岐も値を代入し、文の後も処理が続くからです。else が適切な場面と、そうでない場合のリファクタリング方法は、Go の else キーワードで解説しています。
Go に三項演算子はあるのか?
ありません。Go FAQ がこの質問に直接答えています。設計者たちは ?: が「理解しがたいほど複雑な式を作るのにあまりにも頻繁に使われる」のを見てきたので、「言語に必要な条件分岐の制御構文は 1 つだけ」と判断しました(Go FAQ)。FAQ が示す代わりの方法は、変数に代入する if です。実際には、先にデフォルト値を設定してから上書きするのが最も短い書き方です。
example.gogotimeout := 5 * time.Second if cfg.Debug { timeout = 5 * time.Minute }
三項演算子のよくある使い道のうち 2 つには、今では専用のヘルパーがあります。Go 1.22 で追加された cmp.Or はゼロ値でない最初の引数を返すので、「これを使い、なければあれにフォールバックする」というケースのほとんどをカバーします。範囲の制限には、Go 1.21 の組み込み関数 min と max が使えます。
example.gogoport := cmp.Or(os.Getenv("PORT"), "8080") fmt.Println("listening on :" + port) requested := 500 pageSize := max(1, min(requested, 100)) fmt.Println("page size", pageSize)
example.texttextlistening on :8080 page size 100
cmp.Or は引数をいくつでも受け取るので、cmp.Or(flagAddr, os.Getenv("ADDR"), ":8080") はフラグ、環境変数、デフォルト値の順に確認します。
ジェネリックな三項関数を書かないのはなぜか?
func ternary[T any](cond bool, a, b T) T は書けますし、実際に使っているコードベースもあります。しかし、if の形にはない落とし穴があります。Go は関数を呼び出す前にすべての引数を評価するので、両方の分岐が常に実行されます。
example.gogovar u *User name := ternary(u != nil, u.Name, "anonymous")
example.texttextpanic: runtime error: invalid memory address or nil pointer dereference
本物の ?: 演算子なら、u が nil のときに u.Name を評価しません。関数版は先に u.Name を読んでしまい、クラッシュします。コストの高い呼び出しも同じで、結果が使われるかどうかにかかわらず実行されます。if なら、選ばれた分岐だけが評価されます。
if の中で && と || はどう短絡評価されるのか?
&& は左辺が true のときだけ右辺を評価し、|| は左辺が false のときだけ右辺を評価します(Go spec)。そのため、条件の順序しだいでコードが panic するかどうかが変わります。nil チェックはフィールドへのアクセスより前に書かなければなりません。
example.gogoif req.User != nil && req.User.IsAdmin { fmt.Println("admin panel") } else { fmt.Println("access denied") }
req.User が nil なら、このコードは access denied と出力します。req.User.IsAdmin && req.User != nil のように 2 つの条件を入れ替えると、チェックの前にフィールドを読むので、同じリクエストが invalid memory address or nil pointer dereference で panic します。
|| も、早い段階で拒否するのに同じように使えます。次のヘルパーは Authorization ヘッダーをパースし、最初に満たされなかった条件で処理を止めます。
example.gogofunc bearerToken(header string) (string, bool) { scheme, token, found := strings.Cut(header, " ") if !found || !strings.EqualFold(scheme, "Bearer") || token == "" { return "", false } return token, true }
"Bearer eyJhbGciOi" ならトークンと true を返します。"Basic dXNlcjpwYXNz" や、"Bearer" だけの場合は false を返します。軽いチェックを先に、データベースの検索のような遅いチェックを最後に置けば、遅い呼び出しは結果を変えうるときにしか実行されません。&& は || より優先順位が高いので、a || b && c は a || (b && c) という意味になります。両方を混ぜるときは括弧を付けてください。gofmt はその括弧を削除しません。
if/else if の代わりに switch を使うべきなのはどんなときか?
1 つの値を複数のケースと比較するときや、else if の分岐が 2 つか 3 つを超えるときです。式のない switch は、各 case を bool として上から順に判定します。これは if/else if の連鎖とまったく同じ動きです。
example.gogoswitch { case status >= 500: level = slog.LevelError case status >= 400: level = slog.LevelWarn default: level = slog.LevelInfo }
条件が 1 列にそろい、break もありません。Go の switch は最初に一致したケースで止まるからです。switch ext := filepath.Ext(name); ext { ... } のように、switch も短い文を受け付け、スコープのルールは if と同じです。条件が 1 つか 2 つなら if を使いましょう。特にエラーチェックは、Go を読む人が見慣れた if err != nil の形で書きます。式による switch、型 switch、fallthrough については、Go のキーワード一覧の switch のセクションで解説しています。
LevelUpGo で学ぶ
LevelUpGo では、ブラウザ上で本物の Go コードを実行する演習を通じて Go を学べます。Go Basics では、序盤で if、else、論理演算子を扱い、条件を自分で書く演習に取り組みます。Simplification には、早期リターンで入れ子のコードを平らにする方法、bool のロジックを単純にする方法、長い if の連鎖を switch に置き換える方法のレッスンがあります。Training Ground には、コースの外で練習できる短い単独の演習があります。残り 24 個の予約語については、Go のキーワード一覧:全 25 個の予約語を解説をご覧ください。
よくある質問
Go に三項演算子はありますか?
ありません。Go には ?: 演算子がなく、Go FAQ によると、読みにくい式を作るのによく使われていたため設計者があえて入れなかったそうです。デフォルト値を持つ変数と、それを上書きする if を使ってください。フォールバック値には、ゼロ値でない最初の引数を返す cmp.Or(a, b) が使えます。範囲の制限には min と max を使います。
Go の if で波括弧が必須なのはなぜですか?
波括弧があると、すべての if の本体の範囲が明確になるので、行を追加しても、条件が制御する文がうっかり変わることはありません。また、{ が条件の終わりを示すので、Go では条件を囲む括弧が不要になっています。さらに gofmt が、すべての if を同じ形に整形します。
Go に truthy や falsy の値はありますか?
ありません。if の条件は bool 型でなければなりません。数値、文字列、ポインタ、slice、error が自動的に変換されることはなく、if count { ... } は non-boolean condition in if statement で失敗します。count > 0、s != ""、err != nil のように、比較を明示的に書いてください。
Go の if 文で変数を宣言できますか?
できます。if err := save(order); err != nil のように、if は短い文から始められます。そこで宣言した変数は、条件、if ブロック、そして else if や else のブロックの中で有効で、文が終わると消えます。
Go で else を閉じ波括弧と同じ行に書かなければならないのはなぜですか?
Go の字句解析器は、} で終わる行の末尾にセミコロンを挿入します。次の行が else で始まると、その時点で if 文はすでに終わっているので、コンパイラは syntax error: unexpected keyword else, expected } を報告します。} else { を 1 行に書けば、セミコロンは挿入されません。
出典
- 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
- The Go Programming Language Specification, Declarations and scope: https://go.dev/ref/spec#Declarations_and_scope
- The Go Programming Language Specification, Logical operators: https://go.dev/ref/spec#Logical_operators
- Go FAQ, Why does Go not have the ?: operator?: https://go.dev/doc/faq#Does_Go_have_a_ternary_form
- Effective Go, If: https://go.dev/doc/effective_go#if
- Effective Go, Redeclaration and reassignment: https://go.dev/doc/effective_go#redeclaration
- Go 1.22 Release Notes, cmp: https://go.dev/doc/go1.22#minor_library_changes
- revive rule descriptions (indent-error-flow, superfluous-else, early-return): https://github.com/revive-lint/revive/blob/master/RULES_DESCRIPTIONS.md
