フューチャー技術ブログ

Go1.26リリース連載:newプリミティブの拡張

はじめに

製造エネルギー事業部の辻です。Go1.26ブログ連載 の5本目です。

この記事では、言語仕様のアップデートから new プリミティブの拡張を紹介します。

Go1.25までの new の挙動

アップデート内容へ入る前に、Go1.25までの new() の挙動をおさらいしておきます。Go1.25までの new() は、引数に型を指定し、指定された型のゼロ値で初期化し、そのポインタを返す関数でした。

func main() {
ptr := new(int)
fmt.Println(*ptr) // 0
}

あくまでゼロ値を作るためのものだったため、特定の値(例えば 10"hoge")が入ったポインタを作りたい場合は、以下のように2行にわけて書く必要がありました。

v := 10
ptr := &v

どうしても1行で書くためのテクニックとしては

v := &[]int{10}[0]

なども知られているかもしれません。

Go1.18でジェネリクスが導入されてからは以下のようなヘルパー関数を用意することもよくあるプラクティスでした。

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

こうしたゼロ値でない値を持つポインタを1行で記述したいニーズは 2014 年頃からありました(#9097 proposal: spec: add &T(v) to allocate variable of type T, set to v, and return address)。

Go1.26の new アップデートサマリ

The built-in new function, which creates a new variable, now allows its operand to be an expression, specifying the initial value of the variable.

組み込み関数の new() が引数に、型あるいは式を指定でき、変数の初期値を定義できるようになりました。(なお、式にはリテラル(10)、変数(v)、関数呼び出し(f())、演算(a+b)などが含まれます)

どういう場合に役に立つ?

1.構造体のオプショナル項目の初期化を簡潔に実装できる

構造体のオプショナルなフィールドで、値がない状態を nil で表現することはよくあります。

type Person struct {
Name string `json:"name"`
Age *int `json:"age"` // 不明な場合は nil
}

冒頭のようにGo1.25までは関数の戻り値や値リテラルをポインタ型のフィールドに設定するためには一度変数にするなどが必要でした。Go1.26でこのような関数の戻り値のポインタを new() を用いて初期化できるようになります。

func personJSON(name string, born time.Time) ([]byte, error) {
return json.Marshal(Person{
Name: name,
Age: new(yearsSince(born)), // 関数を new に渡せる!
})
}

func yearsSince(t time.Time) int {
return int(time.Since(t).Hours() / (365.25 * 24))
}

2.値リテラルからポインタ型へ直接変換できる

1と関連しますが 10"hoge" といったリテラル値のポインタを直接取得できませんでした。構造体の *int*string 型のフィールドを埋めるために、値からポインタ型に変換するヘルパー関数を用意することがよくありました。AWS SDK for Go v2 などで aws.Int() などを利用していた方も多いのでは、と思います。

to_ptr.go
func Int(v int) *int {
return ptr.Int(v)
}

Go 1.26 からは、new() でリテラル値からポインタ型へ直接変換できます。

type User struct {
Name string `json:"name"`
Age *int `json:"age"`
}

func main() {
u := User{
Name: "Taro",
Age: new(20),
}
fmt.Println(u)
}

なお、たとえば *int64 が欲しい場合は new(int64(20) のように記述できます。new() に任意の型を指定することで、任意の型のポインタを生成できる点もシンプルながら柔軟性がある仕様と言えます。

検討されていたが採用されなかった案たち

#45624 spec: expression to create pointer to simple types のIssueを見ますと、いろいろな案が議論されていました。最終的なアップデートは new() の拡張、という非常にシンプルな形に落ち着いたものの、代替案も興味深かったため、そのいくつかを紹介したいと思います。

new(Type, Value) 形式

new() に型と値を渡してポインタを取得する方法です。Issue #45624 の冒頭で、Rob氏がOption1として挙げており有力候補のようでした。

p1 := new(int, 3)
p2 := new(rune, 10)
p3 := new(Weekday, Tuesday)
p4 := new(Name, "unspecified")

支持されていた点

  • 型と初期値が明示されており、何が起きているか読み手にとって曖昧さがない
  • 既存の概念と整合している
    • make(Type, size) などの既存の組み込み関数との整合が取れている
  • 明確に型推論される
    • new(3)new(int64, 3) が区別できる

懸念点

  • 冗長である。特にパッケージ型が長い場合に冗長
    • new(time.Duration, time.Second) など。new(time.Second) としたい、という意見あり
    • 3int であることが自明であるが、new(int, 3) と記述しないといけないのは冗長

また、この new(Type, Value) の亜種として new[Type](Value) というジェネリクス風の案もあげられていました。こちらは既存の make() などでは型はジェネリクスで指定しておらず、一貫性がなくなる、という意見がありました。

&Type(Value) 形式

Rob氏のOption2として挙げられています。

p1 := &int(3)
p2 := &rune(10)
p3 := &Weekday(Tuesday)
p4 := &Name("unspecified")

& を拡張すると & が式と変数で異なる意味を持ち、良くない、という意見がありました。つまり & は既存の変数のアドレスを取るものだが、拡張した構文では新しいメモリを割り当ててそのアドレスを返すことになり、一貫しないということです。

ref() 関数などのヘルパー関数導入

値からポインタへ変換する ref() のような関数を、組み込み関数や標準ライブラリとして実装する案です。

ref(123)                  // *int
ref(make([]string, 0, 3)) // *[]string
ref("hello") // *string
ref(ref(string)) // **string
ref(os.Stdin.Name())

varOfnewOfref などいろいろな命名案が議論されていました。ref などは既存のコードでよく使われている命名の可能性があり、衝突の懸念もあったようです。

また、命名に関しては、新しい命名を導入するよりは、既存の new が適当であるとGoのメンバーがコメントされています。

まとめ

Go1.26 で導入される new の拡張について触れました。既存の new を拡張するというシンプルで美しいアップデート、と感じています。ニーズは多いと思うので多くのGopherにとって嬉しい拡張なのではないでしょうか。