10年越しの決着 ― Goにnew(expr)が入るまでの議論を紐解く
Go 1.26で、組み込み関数newが式(expression)を受け取れるようになった。
p := new(42) // *int, 値は42
s := new("hello") // *string, 値は"hello"
b := new(true) // *bool, 値はtrueたった3文字の構文追加に見えるが、ここに至るまでに10年以上の議論がある。最初の提案は2014年。それが却下され、2017年に別角度から再提案され、2021年にRob Pike自らが問題を整理し直し、そして2025年にAlan Donovanが最終形を仕上げた。
この記事では、その議論の流れを時系列で紐解いていく。
そもそも何が問題だったのか
Goには昔からある非対称性がある。構造体のポインタはリテラルから直接作れる。
p := &Point{X: 1, Y: 2} // OK: composite literalは&を取れるしかし、プリミティブ型のポインタは一発で作れない。
p := &42 // コンパイルエラー: cannot take address of 42
p := &int(42) // コンパイルエラー: cannot take address of int(42)仕方なく、こんな回り道を強いられる。
v := 42
p := &vあるいは、ヘルパー関数を書く。
func IntPtr(v int) *int { return &v }これはprotobufやAWS SDKのようにポインタフィールドを多用するAPIで深刻な問題になる。AWSのGo SDKにはaws.String()、aws.Int64()といったヘルパー関数がずらりと並んでいるし、Goの標準ライブラリのテストコードにも同様のヘルパーが散見される。
Generics導入後は汎用的に書けるようになったが、根本的な解決ではない。
func Ptr[T any](v T) *T { return &v }なぜ構造体だけ特別扱いなのか?この疑問が、10年にわたる議論の出発点だった。
第1章: 最初の提案 ― &T(v)とnew(T, v) (2014年)
Issue #9097 の登場
2014年11月、chai2010が最初の提案を投げた。2つの構文を提案している。
new関数の拡張:func new(Type, value ...Type) *Type&Type(value)構文の追加
px := new(int, 9527)
px := &int(9527)なぜ却下されたか
cznicが即座に3つの問題を指摘した。
1. 可変長引数の曖昧さ
newのシグネチャがfunc new(Type, value ...Type) *Typeだと、スライスの初期化でnew([]int, 1, 2, 3)のような使い方を誘発する。しかしそれではスライスのlengthとcapacityをどう指定するのか。mapは?channelは?型ごとの特殊ルールが際限なく増える。
2. &T{}と&T()の混同
既に&T{}(composite literal)が存在する言語で&T()を追加すると、「{}と()の違いは何か」という新たな混乱の種を生む。
3. そもそも需要が薄い
構造体のポインタ生成は頻出するが、プリミティブ型のポインタ生成はそこまで多くない。「Goの最大の長所は機能が少ないことだ」という一言が、この提案の空気感を物語っている。
結局、この提案はGoチームのレビューを経ることなくクローズされた。Rob Pike自身が後に「rather summarily(かなりあっさりと)却下された」と振り返っている。
第2章: 別角度からの再挑戦 ― 関数の戻り値のアドレスを取りたい (2017年)
Issue #22647 ― Ben Hoytの提案
2017年11月、Ben Hoytが少し違う角度から問題を提起した。「関数の戻り値や定数のアドレスを取れるようにしてほしい」というものだ。
mystruct.TimePtr = &time.Now() // これが書きたいHoytが特に強調したのは、AWS SDKの苦痛だった。AWSのGo SDKにはポインタフィールドに値を渡すためだけのヘルパーが大量にある。これは言語側で解決すべき問題ではないか、と。
Ian Lance Taylorの懸念
Go TeamのIan Lance Taylorは慎重な立場を示した。f()がポインタを返す場合、&f()と&f().xの意味が全く異なってしまう。前者はヒープにコピーを確保してそのアドレスを返し、後者は戻り値のフィールドを直接指す。この意味の不連続性は混乱を招く、と。
このissueは直接的な結論には至らなかったが、問題の本質を言語設計の観点から整理する重要な伏線になった。
第3章: Rob Pikeが問題を再定義する (2021年)
Issue #45624 ― Rob Pikeの提案
2021年4月、ついにGoの共同設計者であるRob Pike自身がissueを立てた。
Pikeは2つの選択肢を提示した。
Option 1: newに第2引数を追加する
p1 := new(int, 3)
p2 := new(rune, 10)
p3 := new(Weekday, Tuesday)newの既存の振る舞い(new(T)でゼロ値の*Tを返す)はそのまま維持しつつ、オプショナルな第2引数で初期値を指定できるようにする。
Option 2: 型変換の結果をアドレス可能にする
p1 := &int(3)
p2 := &rune(10)
p3 := &Weekday(Tuesday)型変換(conversion)は必ず新しいストレージを確保するため、そのアドレスを取ることには理論的な正当性がある。&3は型が曖昧だが、&int(3)なら型が明確だ、という論理だ。
Pikeは「両方入れてもいいかもしれない」と述べた。
コミュニティの反応
このissueは大きな反響を呼んだ。主要な立場を整理する。
faiface: 関数の戻り値全般をアドレス可能に
Conversions are just special functions, so this would cover them.
型変換は関数の特殊ケースに過ぎない。ならば関数の戻り値全般のアドレスを取れるようにすべきだ。この意見は107のリアクションを集め、コミュニティの共感を得た。
しかし、これは#22647でIan Lance Taylorが指摘した意味の不連続性の問題をそのまま抱えている。
benhoyt: &int(3)を支持、ただし&exprは反対
Hoytは&int(3)構文を支持しつつ、&expr(任意の式のアドレスを取る)には明確に反対した。理由は意味論的な一貫性の崩壊だ。
x := 42
p1 := &x // 常に同じアドレス
p2 := &x // p1 == p2
p3 := &f() // 毎回新しいアドレス
p4 := &f() // p3 != p4&演算子が「既存の変数のアドレスを取る」と「新しいメモリを確保する」の2つの意味を持つことになり、初学者にとっての罠になる。
clausecker: composite literalの構文をプリミティブにも
int{3}のようにcomposite literal構文をプリミティブ型にも拡張すれば、&int{3}で一貫性が保てるのでは、という提案。しかしこれは言語の根幹に関わる変更であり、議論は深追いされなかった。
peterbourgon: &T{...}派、new不要論
&T{...}パターンを推し、Pikeが支持するOption 1のnew拡張よりもOption 2を好んだ。さらにmake()の統合まで視野に入れる大胆な提案をした。
第4章: なぜ&T(v)でなくnew(expr)になったのか
ここが最も重要なポイントだ。コミュニティの人気は&int(3)構文に集まっていたように見えるが、最終的に採用されたのはnew(expr)だった。
Rob Pikeの提案にあった2つのOptionは、それぞれ異なる問題を抱えていた。
&T(v) (Option 2) が退けられた理由
&int(3)構文には根本的な曖昧さがある。GoではT(v)は型変換(conversion)を意味する。例えばfloat64(42)は整数42をfloat64に変換する式であり、現在のGoでは&float64(42)は「型変換の結果のアドレスを取る」ことになる。
しかし、「型変換の結果がアドレス可能になる」ことは言語仕様に広範な影響を与える。現在のGoの仕様では、アドレス可能(addressable)な値は厳密に定義されている。変数、ポインタのデリファレンス、スライスのインデクシング、アドレス可能な構造体のフィールド、そしてcomposite literalだけがアドレス可能だ。
ここに「型変換の結果」を追加すると、&m[k](mapの要素、アドレス不可能)との不整合が目立つ。なぜ型変換はアドレス可能で、mapのインデクシングはダメなのか?という疑問が生じ、addressabilityの定義を芋づる式に見直す必要が出てくる。
また、&T(v)は視覚的にも「関数呼び出しのアドレスを取っている」ように見え、将来的に&f()(関数戻り値のアドレス)を認めるかどうかという議論と切り離せなくなる。Issue #45653でLeigh McCullochが提案した「関数の戻り値のアドレスを取る」案は、まさにこの延長線上にあった。
new(T, v) (Option 1) の進化
Pikeの当初の提案はnew(T, v)という2引数形式だった。しかしこれにも問題がある。
p := new(MyStruct, MyStruct{Field: "value"}) // 型名の繰り返し(stuttering)型を2回書くのは冗長だ。そもそも第2引数の型は第1引数の型で決まるのだから、型を明示的に書く必要はない。
ここでAlan Donovanが最終的な形を提案した。引数を1つにして、型か式のどちらかを受け取るという設計だ。
new(int) // 従来通り: ゼロ値の*intを返す(引数は型)
new(42) // 新しい: 値42の*intを返す(引数は式)newの引数が型なのか式なのかはコンパイラが判別できる。intは型であり、42は式だ。この設計は以下の利点を持つ。
1. 後方互換性が完全
既存のnew(T)はそのまま動く。新しいnew(expr)は純粋な追加であり、既存コードを壊さない。
2. addressabilityルールに触れない
&演算子のセマンティクスを一切変更しない。アドレス可能な値の定義も変わらない。新しい機能はnew組み込み関数の拡張として閉じている。
3. 意味が明快
new(42)は「42という値を持つ新しい変数を確保し、そのポインタを返す」という意味以外に解釈しようがない。&の二重の意味(既存変数のアドレス vs 新規確保)という混乱も生じない。
4. 式を直接渡せる
type Config struct {
Timeout *time.Duration
Retries *int
Verbose *bool
}
cfg := Config{
Timeout: new(30 * time.Second),
Retries: new(3),
Verbose: new(true),
}関数の戻り値も渡せる。
p := new(time.Now()) // *time.Time
q := new(strconv.Itoa(42)) // *stringGo 1.26 での仕様
Go 1.26(2026年2月リリース)の仕様では、newは以下のように定義されている。
The built-in function
newcreates a new, initialized variable and returns a pointer to it. It accepts a single argument, which may be either an expression or a type.
引数exprが型Tの式(または、デフォルト型がTのuntyped定数式)である場合、new(expr)は型Tの変数を確保し、exprの値で初期化し、そのアドレス(型*Tの値)を返す。
注意点: untyped constantの挙動
一つ注意がある。new()に定数を渡した場合、default typeが使われる。
var ui uint = 10 // OK: untyped constant 10はuintに暗黙変換される
// しかし...
uip := new(10) // *int(intのdefault type)
var ui2 uint = *uip // コンパイルエラー: cannot use *uip (type int) as type uint定数10がそのまま変数宣言で使われる場合はuntyped constantとして柔軟に型推論されるが、new(10)の時点で*intに確定してしまう。明示的な型が必要な場合は型変換を使う。
uip := new(uint(10)) // *uintまとめ: 10年の議論が教えてくれること
| 時期 | 提案 | 結果 |
|---|---|---|
| 2014 | #9097: &T(v) / new(T, v) | 却下。需要不足、設計の曖昧さ |
| 2017 | #22647: 関数戻り値のアドレス | 直接的な結論なし。問題を言語設計の観点から整理 |
| 2021 | #45624: Rob Pikeが再定義 | 長期議論の末にnew(expr)として採用 |
| 2023 | #64749: pointerパッケージ | 却下。標準ライブラリの基準を満たさない |
| 2025-26 | Go 1.26 | new(expr)としてリリース。実装はAlan Donovan |
この10年の変遷は、Go言語の設計哲学をよく表している。
問題が本物かどうかを見極める。 2014年の時点では、プリミティブ型のポインタ生成の需要は限定的だった。しかしprotobufやAWS SDK、encoding/jsonのomitemptyパターンの普及により、この問題は無視できないレベルになった。
最小限の変更で最大の効果を。 &演算子のセマンティクス変更、addressabilityルールの拡張、新しい構文の追加 ― これらの大きな変更を避け、既存のnew関数の自然な拡張として実現した。
急がない。 2021年にRob Pikeが提案してから、実際にリリースされるまで5年かかっている。Genericsの導入(2022年)でPtr[T any]ヘルパーが書けるようになったことも、急がなかった理由の一つだろう。
Goのことわざ "A little copying is better than a little dependency" になぞらえるなら、この10年は "A little waiting is better than a wrong abstraction" だったのかもしれない。