every Tech Blog

株式会社エブリーのTech Blogです。

Go 1.26で追加されたnew(expr)はなぜこの形なのか

Go 1.26で追加されたnew(expr)はなぜこの形なのか

こんにちは、開発1部の@uho-wqです。

本記事ではGo 1.26で追加されたnew(expr)がどのような議論の末にこの形に落ち着いたのかを説明しようと思います。

go.dev

new(expr)

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

p := new(42)       // *int, 値は42
s := new("hello")  // *string, 値は"hello"
b := new(true)     // *bool, 値はtrue

とてもシンプルな構文追加に思えますが、実はこの結論に至るまで2014年から2025年までの11年もかかりました。

この記事では、以下の2つのissueをもとに議論の流れを追っていきます。

github.com

github.com

※ この記事を作成するにあたり、これらのissueに付いたコメントすべてに目を通しました。11年分の議論は非常に膨大なため本記事では要点を絞って紹介しており、解釈の違いや抜け漏れがある可能性がありますがご了承ください。

そもそも何が問題だったのか

Goではcomposite literalは直接ポインタを取得できますが、プリミティブ型は宣言時にポインタを得ることができません。

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

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

よって従来では以下のように一度変数に代入してポインタを得る書き方をするか、ヘルパー関数を定義するしかありませんでした。

v := 42
p := &v

// ヘルパー関数
func IntPtr(v int) *int {
    return &v
}

例えば、AWS SDK for Goではaws.String()aws.Int64()といったヘルパー関数が大量に定義されています。構造体の値をaws.String()で囲むといった作業はAWS SDK for Goを使ったことがある方は経験済みなのかなと思います。

Go 1.18でGenericsが導入されたことによって、ヘルパー関数を汎用的に記述することができるようになりました。

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

しかし、これもcomposite literalのみ直接ポインタを取れるという問題の回避策にはなりましたが、根本解決には至りませんでした。


こうした背景から、言語レベルでの解決策が長年にわたって議論されてきました。以降では、その議論がなぜ最終的にnew(expr)という形に落ち着いたのかを時系列で追っていきます。

proposal: spec: add &T(v) to allocate variable of type T, set to v, and return address #9097

2014年11月にchai2010氏により最初の提案が行われました。

提案は、以下の2つの構文を追加する、というものでした。

  1. new関数の拡張: func new(Type, value ...Type) *Type
  2. &Type(value)構文の追加

例:

px := new(int, 9527)
px := &int(9527)

当初は大きな反響もなくissueは放置されていましたが、2018年にIan Lance Taylor氏が提案に再度言及しました。

&int(5)を許すならnew(int, 5)は不要であり、newを完全に削除することすら検討すべきだと述べました。そして任意の式に&を適用する際の問題点を2つ指摘しています。

  • 1つ目は任意の式vに対して&vを取れるとした場合、論理的にはアドレスのアドレス&&vを取れるべきだが、&&は異なる意味を持つ演算子なので動作しない
  • 2つ目は&varはループ内で呼び出しても毎回同じ値に解決されるが、&exprは毎回新しいインスタンスを確保するので異なる値に解決される

また2020年には、Ian Lance Taylor氏自身が「ジェネリクスが入れば新しい言語機能を必要としないので、ジェネリクスを得るまで待って、そのようなアプローチが十分かどうかを見たいと思う」とも述べています

結局#9097は2023年8月に#45624を優先する形でクローズされました。9年間で40件のコメントが寄せられ、Ian Lance Taylor氏が提示した論点は#45624でも継続して議論されます。

spec: expression to create pointer to simple types #45624

2021年4月にRob Pike氏によってissueが立てられました。

Pike氏はissueを再オープンする代わりに、新たに2つの選択肢を提示しました。

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

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

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

p1 := &int(3)
p2 := &rune(10)
p3 := &Weekday(Tuesday)

Pike氏は「両方入れてもいいかもしれない」とも述べています。

注目すべきは、この時点では最終形となるnew(expr)はまだ提案されていなかったということです。Pike氏の提案はあくまでnew(T, v)(型と値の2引数)と&T(v)の2択でした。

new(1)の提案 (2021年4月)

Pike氏の提案から数日後、Go Teamの Russ Cox氏のコメント が多くの賛同を得ました。

The overloading of & for "take address of existing value" and "allocate copy of composite literal" has always been unfortunate. An alternative to expanding the overloading of & would be to overload new instead, so that it is the generic ptrTo function as well as the original new(T), as in new(1). Then &T{...} can be explained retroactively as mere syntactic sugar for new(T{...}).

&演算子は既に「既存の値のアドレスを取得する&v」と「composite literalのコピーを割り当てる&T{...}」という2つの異なる意味を持っています。ここにさらに意味を追加するのではなく、newを拡張してnew(1)のように書けるようにすべきではないか。そうすれば&T{...}new(T{...})の糖衣構文として説明できる、という主張です。

これが最終形new(expr)の原型でした。

ジェネリクスの提案 (2021年4月)

一方でRoger Peppe氏は言語変更そのものに異を唱えました

Given this possibility, I don't see that there's any need to change new or the language syntax itself to accommodate this functionality.

Goのジェネリクスを使えば以下のように書けるのだから、newや言語仕様自体を変える必要はないのでは、というものです。

// ref returns a pointer to the value of t.
func ref[T any](t T) *T {
    return &t
}

このジェネリクス案は、その後4年にわたって繰り返される反論の原型となりました。

膠着状態 (2021年9月)

2021年9月、Ben Hoyt氏が議論の停滞を指摘し、再検討を求めました。

Looks like this was last discussed in the proposal review meeting on May 5. While there's no clear consensus here, there are a number of good options. It seems like there's a fair bit of enthusiasm for Russ's simple new(1) form, and a decent amount of support for a new builtin generic function like Roger Peppe's ptr(1) suggestion. My vote would be for ptr(1) as it just uses "ordinary" generics, but I like new(1) too. Could this be discussed at the review meetings again?

この時点で支持が集まっていたのはnewの拡張であるnew(1)とジェネリクスを使用したptr(1)の2案でしたが、コンセンサスには至りませんでした。ジェネリクスの正式リリース(Go 1.18、2022年3月)を待つ形で、議論は一時休止に入ります。

PtrTo[T any] vs &T(v) vs newの拡張 (2023年6月)

2023年6月、Go TeamのIan Lance Taylor氏がissueに戻り、選択肢を3つに絞りました

  1. PtrTo[T any]のような標準ライブラリ関数
  2. &T(v)構文
  3. new(v) / new(T, v) の拡張

そしてGo Teamの立場を明確にしました。

@griesemer, @bradfitz, and @ianlancetaylor prefer permitting both new(v) and new(T, v).

この時点では、Go Teamの主要メンバー3人がnew拡張を支持していました。

ただしnew(v)new(T, v)両方を許可する案であり、new(v)単独ではありませんでした。


また、依然として&T(v)を支持する声はあったものの、批判的な意見も支持されるようになってきました。Ben Hoyt氏の主張が端的に示しています。

I slightly prefer new(v) over &T(v) because it eliminates stuttering in cases like new(time.Now()) -- that would be &time.Time(time.Now()) with the other syntax. If new(T, v) is supported in addition for clarity in certain cases, that's fine. new() is also a bit clearer that it always creates a "new" thing.

new(time.Now()) のようなケースだと冗長な繰り返しがなくなりますが、&T(v)の構文だと &time.Time(time.Now()) になってしまいます。明確さが必要な場合に new(T, v)が追加でサポートされるのは問題にはならず、new() は常に「新しい」ものを作成することがより明確である、と主張しています。

さらにHoyt氏も&演算子が概念的に同等でないことも指摘しています。

When you do &Struct{} Go creates a new value every time and returns its address, but when you do &s Go returns the address of that same variable each time.

&Struct{}を行うと、Goは毎回新しい値を作成してそのアドレスを返しますが、&sを行うとGoは毎回その同じ変数のアドレスを返します。


この後も&T(v)案は依然として支持されるものの、議論の焦点はnewの拡張方法とジェネリクスの活用に移っていきます。

new(T, v) vs new(v) (2023年7月)

Goチームが支持しているnewの拡張方法はnew(T, v)new(v)の2パターンありました。

2023年7月、Ian Lance Taylor氏が方針転換を報告しました。Rob Pike氏とRoger Peppe氏などから「new(v)new(T, v)の両方ではなく、new(T, v)のみにすべき」という意見が出ました。

また型名が長くなるケースの大半は構造体であり、構造体には既に&S{}表記があります。単純な値vに対して複雑な型Tを書くnew(T, v)のケースはそもそもほとんど発生しないと考え、new(T, v)でも混乱を招くことは少ないだろう、という見解を示しています。

これに対してMerovius氏が具体例で切り返しました。

new(int64(42)) isn't any more to type or read than new(int64, 42), but new(time.Second) is significantly better than new(time.Duration, time.Second). I don't think having the type in there really adds anything. We are already kind of used to inferring the type from a constant literal.

new(int64(42))new(int64, 42)と比べてタイプ量も読む量も変わりませんが、new(time.Second)new(time.Duration, time.Second)よりもはるかに良いです、と述べています。

このコメントが賛同を集めた一方で、new(v)を見たときにvが型なのか値なのかを読者が把握している必要があるのでnew(v)を好まない、という意見も複数ありました。


new(T, v)は書き方として冗長である一方で明確に記述でき、new(v)は書き方として簡潔である一方で表現として曖昧であるとし、この時点ではコンセンサスには至りませんでした。

ジェネリクスの限界 (2023年 - 2024年)

「ジェネリクスでPtr[T]が書ける」という反論は依然として主張されていました。

しかし2023年12月、Rob Pike氏が改めてこの問題の本質を言い直しています

it's easier to build a pointer to a complex thing than to a simple one.

「複雑な構造体へのポインタは&T{...}で簡単に作れるのに、単純なintへのポインタは面倒」

ジェネリクスを用いたヘルパー関数を書くことは、この非対称性の問題の根本的な解決にはなっていないと言及しています。

ジェネリクスが根本の解決になっていないとするエピソードとして、perj氏の体験談が象徴的でした。

I appear to be writing this function about once every second month, when I need it in a new package. It's not very annoying, but does feel a bit like I'm littering my packages with this function, so not having to write it would be welcome. I do realise I can put it in a package I import, but that also seems overkill for a one-liner.

  • 2ヶ月に1度、新しいパッケージでこのヘルパー関数を書いている
  • パッケージをこの関数で散らかしているような感じがするので、書かなくて済むなら歓迎
  • importするパッケージに入れることもできるが、たった1行のコードのためにそれをするのはやりすぎな気がする

このコメントは20ものGood評価を集めており、ジェネリクス案の限界を端的に示しているといえます。

new(T, v)は解決策にならない (2025年3月-8月)

2025年3月、かつて「ジェネリクスで十分」と主張していたRoger Peppe氏が、new(T, v)案に対して批判を投じました。

Replacing, for example, ref(someMap[x]) with new(SomeType, someMap[x]) would be a net loss because it makes the code more verbose and a little bit more fragile, requiring update should the type of the map's values change.

ref(someMap[x])new(SomeType, someMap[x])に書き換えるのはコードが冗長になるだけでなく、mapの値の型が変わるたびに修正が必要になる。

型を2回書くnew(T, v)では、ジェネリクスのヘルパー関数からの移行メリットがない、という指摘です。


その後、2025年8月にGo TeamのAlan Donovan氏が決めてとなるコメントを投じました。

it is important not to have to redundantly state the type and the value, making new(T, v) a non-solution.

型と値を冗長に並べる必要がないことが重要であり、new(T, v)は解決策にならない、と主張し、Donovan氏は3月のPeppe氏のコメントに納得してnew(value)を支持する立場を表明しました。

デフォルトの型が合わない場合はnew(T(v))とキャストを組み合わせればよく、new(T, v)のような複雑なルールは不要だ、としています。

The proposal committeeの承認 (2025年8月)

2025年8月15日、The proposal committeeを代表してAustin Clements氏が宣言しました。

The proposal committee is happy with new(expr).

new(T)(型を渡す)とnew(expr)(式を渡す)は動作が異なり、構文的な曖昧さを欠点として持つものの、どちらも「新しいストレージを確保して返す」点で一貫しています。

そしてDonovan氏が収集したデータが決め手となりました。

the data @adonovan collected indicates that, while this can be written as a generic function, there are so many instances that it seems well-worth a standardized built-in.

ジェネリクスを用いた関数として記述することも可能ですが、その利用箇所が非常に多いため、標準化された組み込み関数として実装する価値は十分にある、としています。

Accepted (2025年9月)

2025年9月17日、Austin Clements氏が正式に採択を宣言しました。

そして2025年10月27日、実装を完了したAlan Donovan氏がissueを締めくくりました

All done, in only eleven years since #9097. ;-)

Go 1.26での仕様

The Go Programming Language Specificationでは、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.

引数が型Tの式(または、デフォルト型がTのuntyped定数式)である場合、new(expr)は型Tの変数を確保し、exprの値で初期化し、そのアドレス(型*Tの値)を返します。

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)) // *string

注意点: untyped constantの挙動

ただし1つ注意点があります。new()に定数を渡した場合、default typeが使われます。

var ui uint = 10 // OK: untyped constant 10はuintに暗黙変換される

// しかし...
uip := new(10)      // *int(10のdefault typeがint)
var ui2 uint = *uip  // コンパイルエラー: cannot use *uip (type int) as type uint

定数10がそのまま変数宣言で使われる場合はuntyped constantとして柔軟に型推論されますが、new(10)の時点で*intに確定してしまいます。明示的な型が必要な場合は型変換を組み合わせましょう。

uip := new(uint(10)) // *uint

まとめ

最後に、11年の議論で登場した各提案の結論についてまとめます。

提案 結論
&T(v) &演算子の意味の不連続性。&2は毎回新しいアドレスを返すが&vは同じアドレスを返す。混乱を招く
ref(v) / ptr(v) ジェネリクスで1行で書ける。だが逆に「全員が書いている」。組み込みとして標準化する方が合理的
new(T, v) 冗長。new(time.Duration, time.Second)はジェネリクスのref(time.Second)より後退する
new(expr) 採用。 &のセマンティクスを変えず、既存のnew関数の自然な拡張

個人的には、議論全体を通してnew(expr)という結論に至ったことがとても腑に落ちました。ジェネリクスの導入を見越して一度議論を止め、導入後も便利さに飛びつかず実運用の課題を吸い上げた上で、本質的な解決策に辿り着いています。

最終形のnew(expr)は、2021年にRuss Cox氏が投じたnew(1)の発想そのものでした。4年の間に&T(v)new(T, v)が検討され、結局最もシンプルな案に戻ってきたのが面白いなと思いました。