Skip to content
AIこの記事はAIによって生成されたコンテンツです。

10年越しの決着 ― Goにnew(expr)が入るまでの議論を紐解く

Go 1.26で、組み込み関数newが式(expression)を受け取れるようになった。

go
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には昔からある非対称性がある。構造体のポインタはリテラルから直接作れる。

go
p := &Point{X: 1, Y: 2} // OK: composite literalは&を取れる

しかし、プリミティブ型のポインタは一発で作れない。

go
p := &42      // コンパイルエラー: cannot take address of 42
p := &int(42) // コンパイルエラー: cannot take address of int(42)

仕方なく、こんな回り道を強いられる。

go
v := 42
p := &v

あるいは、ヘルパー関数を書く。

go
func IntPtr(v int) *int { return &v }

これはprotobufやAWS SDKのようにポインタフィールドを多用するAPIで深刻な問題になる。AWSのGo SDKにはaws.String()aws.Int64()といったヘルパー関数がずらりと並んでいるし、Goの標準ライブラリのテストコードにも同様のヘルパーが散見される。

Generics導入後は汎用的に書けるようになったが、根本的な解決ではない。

go
func Ptr[T any](v T) *T { return &v }

なぜ構造体だけ特別扱いなのか?この疑問が、10年にわたる議論の出発点だった。

第1章: 最初の提案 ― &T(v)new(T, v) (2014年)

Issue #9097 の登場

2014年11月、chai2010が最初の提案を投げた。2つの構文を提案している。

  1. new関数の拡張: func new(Type, value ...Type) *Type
  2. &Type(value)構文の追加
go
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. そもそも需要が薄い

the best feature of Go is its lack of features

構造体のポインタ生成は頻出するが、プリミティブ型のポインタ生成はそこまで多くない。「Goの最大の長所は機能が少ないことだ」という一言が、この提案の空気感を物語っている。

結局、この提案はGoチームのレビューを経ることなくクローズされた。Rob Pike自身が後に「rather summarily(かなりあっさりと)却下された」と振り返っている。

第2章: 別角度からの再挑戦 ― 関数の戻り値のアドレスを取りたい (2017年)

Issue #22647 ― Ben Hoytの提案

2017年11月、Ben Hoytが少し違う角度から問題を提起した。「関数の戻り値や定数のアドレスを取れるようにしてほしい」というものだ。

go
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を立てた。

This notion was addressed in #9097, which was shut down rather summarily. Rather than reopen it, I'll take another approach.

Pikeは2つの選択肢を提示した。

Option 1: newに第2引数を追加する

go
p1 := new(int, 3)
p2 := new(rune, 10)
p3 := new(Weekday, Tuesday)

newの既存の振る舞い(new(T)でゼロ値の*Tを返す)はそのまま維持しつつ、オプショナルな第2引数で初期値を指定できるようにする。

Option 2: 型変換の結果をアドレス可能にする

go
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(任意の式のアドレスを取る)には明確に反対した。理由は意味論的な一貫性の崩壊だ。

go
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引数形式だった。しかしこれにも問題がある。

go
p := new(MyStruct, MyStruct{Field: "value"})  // 型名の繰り返し(stuttering)

型を2回書くのは冗長だ。そもそも第2引数の型は第1引数の型で決まるのだから、型を明示的に書く必要はない。

ここでAlan Donovanが最終的な形を提案した。引数を1つにして、型か式のどちらかを受け取るという設計だ。

go
new(int)    // 従来通り: ゼロ値の*intを返す(引数は型)
new(42)     // 新しい: 値42の*intを返す(引数は式)

newの引数が型なのか式なのかはコンパイラが判別できる。intは型であり、42は式だ。この設計は以下の利点を持つ。

1. 後方互換性が完全

既存のnew(T)はそのまま動く。新しいnew(expr)は純粋な追加であり、既存コードを壊さない。

2. addressabilityルールに触れない

&演算子のセマンティクスを一切変更しない。アドレス可能な値の定義も変わらない。新しい機能はnew組み込み関数の拡張として閉じている。

3. 意味が明快

new(42)は「42という値を持つ新しい変数を確保し、そのポインタを返す」という意味以外に解釈しようがない。&の二重の意味(既存変数のアドレス vs 新規確保)という混乱も生じない。

4. 式を直接渡せる

go
type Config struct {
    Timeout *time.Duration
    Retries *int
    Verbose *bool
}

cfg := Config{
    Timeout: new(30 * time.Second),
    Retries: new(3),
    Verbose: new(true),
}

関数の戻り値も渡せる。

go
p := new(time.Now())       // *time.Time
q := new(strconv.Itoa(42)) // *string

Go 1.26 での仕様

Go 1.26(2026年2月リリース)の仕様では、newは以下のように定義されている。

The built-in function new creates 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が使われる。

go
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に確定してしまう。明示的な型が必要な場合は型変換を使う。

go
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-26Go 1.26new(expr)としてリリース。実装はAlan Donovan

この10年の変遷は、Go言語の設計哲学をよく表している。

問題が本物かどうかを見極める。 2014年の時点では、プリミティブ型のポインタ生成の需要は限定的だった。しかしprotobufやAWS SDK、encoding/jsonomitemptyパターンの普及により、この問題は無視できないレベルになった。

最小限の変更で最大の効果を。 &演算子のセマンティクス変更、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" だったのかもしれない。