Menu

Go言語の三項演算子:Goにない理由と代わりの書き方

Goに三項演算子(cond ? a : b)はありません。このページではその理由と、Goらしいif/elseでの書き換え、デフォルト値のためのcmp.Or、そしてジェネリックなヘルパー関数と、それが本物の三項演算子と違う点になる罠を紹介します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

Goに三項演算子はありません。cond ? a : b は存在しません。Goらしい書き換えは if 文です。

変数をデフォルト値で宣言し、if の中で変更します。両方の分岐で処理が必要なら if/else を使います。

var fee int
if member {
	fee = 0
} else {
	fee = computeFee(order)
}

Goが三項演算子を入れなかった理由

GoのFAQはこれに直接答えています。設計者たちは、?: 演算子が理解しがたいほど複雑な式を作るのに使われすぎるのを見てきたので、if/else のほうが長くても間違いなく明快だと判断しました。言語に必要な条件分岐の制御構文は1つだけです。

実際のところ、この演算子がないことで1回あたり2、3行増えますが、a ? b ? c : d : e のような入れ子の式が丸ごとなくなります。

代わりに早期リターンする

関数の中なら、変数をまったく使わない形が最もすっきりすることがよくあります。

条件を小さな名前付きの関数に切り出すのが最善の答えであることも多いです。呼び出し側は求めていた三項演算子のように読め、関数名が条件の意味を説明してくれます。

cmp.Orによるデフォルト値(Go 1.22)

他の言語で最もよく使われる三項演算子は、「この値を使う、空ならフォールバックを使う」というものです:name ? name : "anonymous"、JavaScriptなら name || "anonymous"。Go 1.22はまさにこのために cmp.Or を追加しました。これは、その型のゼロ値でない最初の引数を返します。

ゼロ値を「ない」ものとして扱うので、「ポートを意図的に0にした」のと「ポートが設定されていない」のを区別できません。ゼロが有効な値なら、ポインタ、別の bool、または明示的な if を使います。

ジェネリックなIfヘルパーとその罠

ジェネリクス(Go 1.18)を使えば、三項演算子のように見える関数を書けます。

func If[T any](cond bool, a, b T) T {
	if cond {
		return a
	}
	return b
}

status := If(ok, "pass", "fail")

単純な値ならうまく動き、これを使っているコードベースもあります。しかしこれは三項演算子 ではなく、その違いが問題になります。Goは関数を呼ぶ前にすべての引数を評価するので、ab は常に両方とも計算されます。

1回目の呼び出しは "a""b" の両方を計算し、2回目は条件が偽なのにnilポインタの参照外しでpanicします。本物の三項演算子なら選ばれた側しか評価しません。ですから次のことを守ってください。

  • 片方がポインタの参照外し、スライスのインデックス、存在しないかもしれないマップのエントリの読み取りを含むときは、こうしたヘルパーを決して使わない。
  • 片方が重い処理や副作用を持つときも決して使わない。
  • 安価で安全な値なら動きますが、それでも多くのGoのレビュアーは普通の if にするよう求めるでしょう。

関数を渡して分岐を遅延評価にすることもできます:If(ok, func() string { return a }, func() string { return b })。ただ、そこまでするなら if 文のほうが短くなります。

その他のテクニックと、避けるべき理由

マップの参照。 map[bool]string{true: "yes", false: "no"}[ok] は動きますが、評価のたびにマップを作ってハッシュを計算し、両方の値を評価します。珍しい書き方であって、イディオムではありません。

即時実行関数。 その場で呼び出す無名関数を使えば式になります。

label := func() string {
	if n > 0 {
		return "positive"
	}
	return "non-positive"
}()

これは実際に遅延評価され、文を置けない複合リテラルの中で役立つこともたまにあります。ただ、たいていはリテラルの直前で値を変数に計算しておくほうが明快です。

代わりの書き方を選ぶ

状況使うもの
2つの値から選ぶデフォルト値の後に if
両方の分岐で計算が必要if/else
値が関数の結果を決める早期 return
値が空のときのフォールバックcmp.Or(value, fallback)
ケースが3つ以上switch
意味を持つ条件を繰り返す小さな名前付きの関数

条件式を追加する提案はGoのissueトラッカーに定期的に出されますが、これまでのところ却下されています。if を書きましょう。

よくある質問

Goに三項演算子はありますか?

ありません。Goに cond ? a : b という式はありません。GoのFAQによれば、読みにくい式を作るのに使われすぎるため、設計者があえて入れなかったとのことです。代わりに if/else 文を使います。

Goで条件付きの代入を1行で書くには?

先にデフォルトを代入し、それを上書きします。

label := "odd"
if n%2 == 0 {
	label = "even"
}

これがGoらしい書き換えです。gofmtif の本体を1行にしないので、常に最低3行になります。

Goでジェネリックな三項演算子の関数は書けますか?

書けます:func If[T any](cond bool, a, b T) T { if cond { return a }; return b }。問題は、Goが関数を呼ぶ前にすべての引数を評価するので、ab の両方が計算されることです。If(p != nil, p.Name, "") はnilの p を参照外ししてpanicします。本物の三項演算子なら片方しか評価しません。

Goで文字列が空のときにデフォルト値を設定するには?

Go 1.22以降、cmp.Or(name, "anonymous") はゼロ値でない最初の引数を返すので、name"" でない限り name になります。比較可能な任意の型に使え、他の関数呼び出しと同じくすべての引数を評価します。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める