GoSuda

Go 1.27、あるいは私がいかにして心配するのをやめてジェネリックメソッドを愛するようになったか

By iwanhae views ...

Go 1.27の概要

Go 1.27は2026年8月にリリースされます。今回は言語そのものが変わりました。「slicesに新しい関数を追加しました」といった程度の変更ではなく、実際に言語が変わったのです。ジェネリックメソッドが導入され、11年間開かれていたissueがクローズされました。encoding/jsonは、飛行機が空を飛んでいる間に新しいエンジンへ静かに置き換えられました。

以下のすべての例はgo1.27rc3(darwin/arm64)で実行しました。すべての出力ブロックは実際の出力をそのままコピーしたもので、エラーメッセージも含みます。何か奇妙に見えるとしたら、それはコンパイラが実際にそう出力したからです。

1go install golang.org/dl/go1.27rc3@latest
2go1.27rc3 download

1. 言語が変わった(本当に)

1.1. ジェネリックメソッド

Go 1.18以降ジェネリクスは存在していましたが、Go 1.18以降、私たちは「あの会話」を繰り返してきました。それは次のようなものです。

「スライス型にMapメソッドを書いてみよう——」

method must have no type parameters

「……わかった。パッケージレベルの関数にしよう。」

メソッドが使えるのは、レシーバーで宣言された型パラメータだけでした。独自のメソッドで新たな型パラメータを導入することはできませんでした。そのため、あらゆるジェネリックな変換処理はパッケージスコープへ追いやられ、MapSliceの何らかの変種という名前の、浮いたヘルパー関数が17個も並ぶことになりました。

Go 1.27ではこれが解決されました。

 1type List[E any] []E
 2
 3// Fは、メソッド自身が宣言する型パラメータです
 4func (l List[E]) Apply[F any](f func(E) F) List[F] {
 5	r := make(List[F], len(l))
 6	for i, x := range l {
 7		r[i] = f(x)
 8	}
 9	return r
10}
11
12func main() {
13	l := List[int]{1, 2, 3}
14	fmt.Println(l.Apply(func(i int) string { return fmt.Sprint(i * 10) }))
15	// [10 20 30]
16}

レシーバー自体がジェネリックである必要さえありません。ごく普通の構造体でも、ジェネリックメソッドを持てます。

1type Bag struct{ items []any }
2
3func (b *Bag) Add[T any](v T) { b.items = append(b.items, v) }

ただし、今日の午後にコードベース全体をリファクタリングする前に、2つの制約があることを知っておいてください。見た目以上に重要です。

  1. インターフェースのメソッドは型パラメータを宣言できません。
  2. ジェネリックメソッドはインターフェースのメソッドを実装できません。

問題になるのは2つ目です。

1type Adder interface{ Add(int) int }
2
3type G struct{}
4
5func (G) Add[T any](v T) T { return v }
6
7var _ Adder = G{}
1cannot use G{} (value of struct type G) as Adder value in variable declaration:
2	G does not implement Adder (wrong type for method Add)
3		have Add[T any](T) T
4		want Add(int) int

つまり、ジェネリックメソッドと動的ディスパッチは共存しません。これはGoチームがけちなのではなく、ジェネリックメソッドには無限個のインスタンス化が存在しうる一方、インターフェースのメソッドテーブルは有限かつコンパイル時に確定している必要があるからです。無限のものを有限のテーブルに入れることはできません。宇宙がそれを許さないのです。

実践的な結論として、ジェネリックメソッドは、ユーティリティ的なAPIを持つ具象型のためのものです。インターフェースの背後に隠す必要があるものには、従来の方法がまだ必要です。

標準ライブラリはすでにこの機能を活用しています。math/rand/v2のRandにジェネリックメソッドが追加されました。

1func (r *Rand) N[Int intType](n Int) Int
1r := rand.New(rand.NewPCG(1, 2))
2fmt.Println(r.N(int32(100)))     // 76
3fmt.Println(r.N(time.Second))    // 616.436223ms

以前はパッケージレベルのrand.Nだけがジェネリックで、それはグローバルなソースを使うことを意味していました。今では、自分でシードした*Randでも同じ利便性が得られます。小さな変更ですが、良い変更です。

1.2. 構造体リテラルのフィールドセレクタ、または: Issue #9859 がついに解決される

埋め込みによりu.IDと書けます。しかし、埋め込みによってUser{ID: 1}と書けるわけではありません。代わりに書くのはUser{Base: Base{ID: 1}}であり、これはGoが「本当にそのつもりか」を問うための方法です。

Issue #9859は2015年に起票されました。このたびクローズされました。どこかで、その後2回転職したgopherが、GitHubの通知を受け取っているはずです。

 1type Object struct{ name, color string }
 2type Point3D struct {
 3	Object
 4	x, y, z float64
 5}
 6type Line struct {
 7	Object
 8	p, q Point3D
 9}
10
11// Go 1.27: 昇格したフィールドを直接使用します
12line := Line{name: "diagonal", q: Point3D{y: -4, z: 12.3}}

nameはLineのフィールドではありません。埋め込まれたObjectのフィールドです。以前はLine{Object: Object{name: "diagonal"}, ...}と書く必要がありました。

ここからは細則です。これらすべてを実際にコンパイルして確認しました。

(a) キーは依然としてただの識別子です。 任意のセレクタパスは書けません。

1_ = Line{Object.name: "x"}   // invalid field name Object.name in struct literal
2_ = Line{p.x: 1}             // invalid field name p.x in struct literal

したがって、この機能は「キーがフィールドアクセスのように動くようになった」ではありません。正確には「暗黙的に昇格したフィールド名が許可された」ということです。見出しが示唆するよりも限定的です。

(b) 埋め込みフィールドと、そこから昇格したフィールドを同時に指定することはできません。

1obj := Object{"edge", "black"}
2_ = Line{Object: obj, name: "diagonal"}
3// cannot specify promoted field name and enclosing embedded field Object

妥当な判断です。同じメモリについて矛盾する2つの指定がなされたため、コンパイラは推測することを拒否しました。

(c) ポインタ埋め込みは推奨されていません。

1type PtrEmbed struct {
2	*Object
3	z int
4}
5
6_ = PtrEmbed{name: "x"}
7// invalid implicit pointer indirection to reach name

ポインタをたどるには、たどるべきポインタが必要ですが、リテラルを構築する時点ではまだ存在しません。これも妥当です。

良い知らせとして、go fixが古いリテラルを書き換えてくれます。詳しくは後述します。

1.3. 関数の型推論がより恣意的でなくなった

ジェネリック関数の型推論は、ある場所では動き、別の場所では動きませんでした。どちらになるかを決める明確な原則はありませんでした。変数に代入するのは問題ない。全く同じ関数を構造体のフィールドに入れると?コンパイルエラーになり、「2022年のようにdouble[int]と書いてください」と言われました。

Go 1.27では、目標の型が曖昧なく分かるあらゆる文脈で推論が機能するようになりました。

 1func double[T ~int | ~float64](v T) T { return v * 2 }
 2
 3type S struct{ f func(int) int }
 4type A [1]func(int) int
 5
 6func main() {
 7	s := S{f: double}          // 1.26: double[int]が必要でした
 8	a := A{double}             // 1.26: double[int]が必要でした
 9
10	c := make(chan func(int) int, 1)
11	c <- double                // 1.26: double[int]が必要でした
12
13	var fn func(float64) float64 = double  // これは常に動作していました
14
15	fmt.Println(s.f(21), a[0](5), (<-c)(7), fn(1.5))
16	// 42 10 14 3
17}

1つのdoubleが、3か所ではfunc(int) intとして、4か所目ではfunc(float64) float64としてインスタンス化され、すべて文脈から決まります。関数値で満たされたオプション構造体やハンドラテーブルを書いているなら、コードベースから多くの[T]の雑音が消えるでしょう。

2. ランタイム: 無料のパフォーマンスとリーク検出器

2.1. サイズ特化のアロケーション

コンパイラは、小さなオブジェクトに対して、サイズに特化したアロケーションルーチンを呼び出すようになりました。リリースノートでは、80バイト未満のアロケーションで最大30%の削減、アロケーションを多用するプログラム全体では約1%の改善が主張されています。

測定せずにリリースノートを信じることはしないので、次のように試しました。

 1type small struct{ a, b, c, d int }
 2
 3var sink any
 4
 5func BenchmarkAlloc(b *testing.B) {
 6	for b.Loop() {
 7		sink = &small{}
 8	}
 9}
10
11func BenchmarkSlice(b *testing.B) {
12	for b.Loop() {
13		sink = make([]byte, 48)
14	}
15}
1# デフォルト(サイズ特化malloc有効)
2BenchmarkAlloc-12     3000000     5.358 ns/op
3BenchmarkSlice-12     3000000    13.58 ns/op
4
5# GOEXPERIMENT=nosizespecializedmalloc
6BenchmarkAlloc-12     3000000     8.722 ns/op
7BenchmarkSlice-12     3000000    20.81 ns/op
8

Apple Silicon上で、アロケーションだけを行うマイクロベンチマークでは30〜35%高速でした。つまり、これは最良のケースにすぎず、数字を良く見せるために設計されたベンチマークが、実験室条件の下で達成したものです。実際のプログラムでは、GCや実際の処理がその効果のほとんどを埋もれさせるでしょう。正直な主張は約1%のほうです。

コストはバイナリサイズです。Hello Worldでは次のとおりです。

12413202 bytes  (デフォルト)
22361826 bytes  (nosizespecializedmalloc)

約50KBが固定で増加し、プログラムの内容には左右されません。GOEXPERIMENT=nosizespecializedmallocでオプトアウトできますが、この回避策はGo 1.28で削除される予定です。ですから、生活習慣としてではなく、バグ報告の回避策として扱ってください。

2.2. goroutineリークプロファイルが実用になった

Go 1.26では実験的機能でしたが、1.27で一般提供されました。goroutineleakprofileのGOEXPERIMENTは不要になり、そのまま動作します。

 1func leak() {
 2	ch := make(chan int) // 誰も送信することはありません。誰も。
 3	go func() {
 4		<-ch
 5	}()
 6}
 7
 8func main() {
 9	for range 3 {
10		leak()
11	}
12	runtime.GC()
13	runtime.GC()
14	pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
15}
1goroutineleak profile: total 3
23 @ 0x104de3f18 0x104d7f0b0 0x104d7ec34 0x104e374f4 0x104dea024
3#	0x104e374f3	main.leak.func1+0x23	/tmp/go127/leak/main.go:12

永久に停止した3つのgoroutineが、まさにそれを作った場所を示すファイル名と行番号とともに表示されています。

この仕組みの背後にある工夫は本当に巧妙です。ガベージコレクタの到達可能性解析を再利用しています。goroutine GがプリミティブPでブロックされており、Pが実行可能なgoroutine(あるいはそれらが起こしうるもの)のいずれからも到達不能であれば、Pには二度と触れることができず、Gが目を覚ますことは決してありません。これはヒューリスティックではなく、証明です。

同じ設計により、その制約も自動的に得られます。チャネルやミューテックスがグローバル変数を通じて、あるいは実行中のgoroutineのローカル変数を通じて到達可能であれば、GCはそれを見ることができるため、ランタイムは何も結論づけられません。上の簡単な例でさえ、報告を得るためにruntime.GC()を2回呼ぶ必要があることに注意してください。すべてを捕捉できるわけではありません。

しかし、古典的な「コンテキストのキャンセルを忘れ、ワーカーのgoroutineが永久に生き続ける」という問題は捕捉できます。ほとんどの場合、これがリークの大部分です。

net/http/pprofをインポートすると、/debug/pprof/goroutineleakでも利用できます。これをステージング環境に組み込み、毎週確認して、静かに戦慄してください。

この作業はUberのVlad Saiocによって貢献されました。好きな飲み物を一杯おごるべき人物です。

2.3. トレースバックで、どのリクエストが死んだかがわかるようになった

Go 1.27以降を宣言したモジュールでは、トレースバックのヘッダー行にruntime/pprofのgoroutineラベルが含まれるようになりました。

 1func handle(ctx context.Context) {
 2	var wg sync.WaitGroup
 3	wg.Go(func() {
 4		buf := make([]byte, 4<<10)
 5		os.Stdout.Write(buf[:runtime.Stack(buf, true)])
 6	})
 7	wg.Wait()
 8}
 9
10func main() {
11	labels := pprof.Labels("request", "GET /orders/42", "tenant", "acme")
12	pprof.Do(context.Background(), labels, handle)
13}
1goroutine 3 [running] {request: "GET /orders/42", tenant: acme}:
2main.handle.func1()
3	/tmp/go127/tblabel/main.go:15 +0x40
4sync.(*WaitGroup).Go.func1()
5	...
6goroutine 1 [sync.WaitGroup.Wait] {request: "GET /orders/42", tenant: acme}:
7...

重要な点は、ラベルは子goroutineに継承されることです。goroutine 3はラベルを一度も設定していません。親から受け取ったのです。

ですから、次に本番環境でデッドロックが起き、誰かがプロセスにSIGQUITを送ったとき、全く同じに見える4,000個のgoroutineの代わりに、どのリクエストとどのテナントに属するかタグ付けされた4,000個のgoroutineが得られます。リクエストの入口でpprof.Doを3行書くだけで、それが手に入ります。

ラベルには標準エラーに出力したくないものを含めることができるため、GODEBUG=tracebacklabels=0で無効にできます。そのオプトアウトは、永久に残ることが明示されています。

2.4. asynctimerchanは完全に削除された

Go 1.23はタイマーチャネルをバッファなし(同期)にし、古い動作に戻すための手段としてasynctimerchan=1を提供していました。1.27では、この設定が恒久的に削除されました。timeパッケージのチャネルは、交渉の余地なく同期的です。

それに伴って導入されたポリシーが興味深いところです。削除されたGODEBUGがgo.modに残っていても、自動的にビルドが壊れることはありません。古い値に設定されている場合にのみ壊れます。

1# go.modの内容: godebug asynctimerchan=1
2go: error loading go.mod:
3go.mod:5: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/doc/godebug#go-127)
4
5# go.modの内容: godebug asynctimerchan=0
6(正常にビルドされます)

つまり、叱られるのは、削除された動作に実際に依存していた人だけです。最終的なデフォルト値に設定して忘れていた人は、そのことを考えずに済み続けます。これは思慮深いAPIの考古学といえるでしょう。

3. 標準ライブラリ

3.1. encoding/json/v2: 飛行中にエンジンが置き換えられた

これが大きな変更です。2年以上にわたる提案の議論が、ついに形になりました。

現在、3つのパッケージがあり、その分割を理解することがほぼ全てです。

パッケージ役割
encoding/jsonおなじみのv1 API。動作は100%変更なし。 現在はv2の上に実装されています
encoding/json/v2意味論的処理。Goの値 ↔ JSON
encoding/json/jsontext構文的処理。トークンストリームとしてのJSON

見出しとなるのは、encoding/jsonが全く異なる実装の上に再構築されたにもかかわらず、動作は完全に同一であるという点です。仕組みは次のとおりです。

1// Go 1.27のencoding/json.Unmarshalの要約
2func Unmarshal(data []byte, v any) error {
3	return jsonv2.Unmarshal(data, v, DefaultOptionsV1())
4}

v1の従来の癖はすべてオプションとしてエンコードされ、DefaultOptionsV1()にまとめられています。v1 APIは常にこのまとまりを適用します。したがって、次のコードは1.26でも1.27でも同じように動作します。

1var m map[string]int
2json.Unmarshal([]byte(`{"a":1,"a":2}`), &m)  // <nil>, map[a:2]

そうです。v1は依然として重複キーを黙って受け入れ、最後の値を採用します。これは昔からそうでしたし、今もそうです。互換性とは、悪い部分との互換性でもあるのです。

一方、v2には明確な方針があります。

1var m map[string]int
2jsonv2.Unmarshal([]byte(`{"a":1,"a":2}`), &m)
3// jsontext: duplicate object member name "a"
4
5var s string
6jsonv2.Unmarshal([]byte("\"\xff\""), &s)
7// jsontext: invalid UTF-8 after offset 1

重複キーと不正なUTF-8は拒否されます。どちらも単に見苦しいだけでなく、本当に危険です。2つのパーサーが、どの重複キーが勝つかについて食い違うと、セキュリティバグが生じます。CouchDBのCVE-2017-12635はまさにこれでした。rolesキーが2つ含まれるJSONボディに対し、検証器は一方を読み、ストレージ層はもう一方を読みました。拒否するのが正しい判断です。

オプションは可変長引数で渡します。

 1type Config struct {
 2	Name    string   `json:"name"`
 3	Tags    []string `json:"tags,omitzero"`
 4	Timeout int      `json:"timeout,omitzero"`
 5}
 6
 7out, _ := jsonv2.Marshal(Config{Name: "api"})
 8// {"name":"api"}
 9
10// マップのキーをソートし、インデントを付ける
11out, _ = jsonv2.Marshal(map[string]int{"b": 2, "a": 1},
12	jsonv2.Deterministic(true), jsontext.WithIndent("  "))
13// {
14//   "a": 1,
15//   "b": 2
16// }
17
18// 未知のフィールドを拒否する。以前はDecoderとDisallowUnknownFields()が必要でした
19var c Config
20err := jsonv2.Unmarshal([]byte(`{"name":"api","nope":1}`), &c,
21	jsonv2.RejectUnknownMembers(true))
22// json: cannot unmarshal JSON string into Go main.Config:
23//   unknown object member name "nope"

Deterministic、MatchCaseInsensitiveNames、StringifyNumbers、FormatNilSliceAsNull、OmitZeroStructFields。以前はサードパーティのライブラリや手書きのMarshalJSONで解決していた事柄の多くが、今やフラグで指定できます。

そして、移行の話こそが最良の部分です。後に指定したオプションが優先されるため、v2の厳格さを一度に1つの動作ずつ採用できます。

1// v1の意味論を維持しつつ、v2と同様に重複キーを拒否する
2jsonv2.Unmarshal(data, &v,
3	json.DefaultOptionsV1(),
4	jsontext.AllowDuplicateNames(false))
5// duplicate object member name

重複キーの受け入れを止めるために、200,000行のコードベースをv2へ移植する必要はありません。オプションを1つ切り替えるだけです。大規模なコードベースにとって、これは現実的な道筋です。

性能面では、マーシャリングはほぼ同等で、アンマーシャリングは大幅に高速化されています。問題が起きた場合は、GOEXPERIMENT=nojsonv2で古い実装に戻せます。このオプトアウトもいずれ削除される予定ですので、そのまま居座るのではなく、issueを立ててください。

jsontextは低レベルの層です。Encoder/Decoderが、状態機械によって誠実さを保ちつつ、JSONをTokenとValueとして走査します。Goの値を一切実体化したくない、ストリーミング変換器やJSONフィルタを書く場合に利用してください。

3.2. 標準のuuidパッケージ

ついに実現しました。RFC 9562が標準ライブラリに入り、go getは不要です。

 1import "uuid"
 2
 3func main() {
 4	fmt.Println(uuid.NewV4())
 5	// b97aa695-da08-472c-af81-ff088129019f
 6
 7	// v7: 上位48ビットはタイムスタンプなので、作成順に必ずソートされます
 8	a, b := uuid.NewV7(), uuid.NewV7()
 9	fmt.Println(a.Compare(b) < 0) // true
10
11	u, err := uuid.Parse("urn:uuid:0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b")
12	fmt.Println(u, err)
13	// 0198a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b <nil>
14
15	fmt.Println(uuid.Nil(), uuid.Max())
16	// 00000000-0000-0000-0000-000000000000 ffffffff-ffff-ffff-ffff-ffffffffffff
17}

API全体はNew、NewV4、NewV7、Nil、Max、Parse、MustParseと、型1つ、type UUID [16]byteだけです。それ以上ありません。コーヒーが冷めるまでの間に、パッケージのドキュメント全体を読み終えられます。

知っておく価値のある詳細は次のとおりです。

  • **UUIDは[16]byte**なので、==が使え、そのままマップのキーとして使えます。google/uuidと同じ設計です。
  • NilとMaxは変数ではなく関数です。 パッケージレベルのvar Nil UUIDは足を狙った装填済みの銃のようなもので、どこかの誰かがいずれ代入してしまうからです。
  • ランダムビットは暗号学的に安全な生成器から得られます。
  • encoding.TextMarshaler/TextUnmarshaler/TextAppenderを実装しているため、JSONの構造体にそのまま組み込めます。
  • データベースの主キーにはNewV7を使うべきです。 時間順なので、B木インデックスが断片化しなくなります。これはv4 UUIDが悪名高いほど苦手とする点です。

依存関係を1つ削除できるようになりました。v1/v3/v5が必要な場合や、より高度なパース・オプションが必要な場合は、サードパーティのライブラリにはまだ役割があります。

3.3. crypto/mldsa: 耐量子署名。ただし非常に大きい

Go 1.24では、耐量子鍵交換のためのcrypto/mlkemが提供されました。Go 1.27はもう一方の半分、FIPS 204として標準化されたML-DSA署名をもたらします。

 1sk, _ := mldsa.GenerateKey(mldsa.MLDSA65())
 2pk := sk.PublicKey()
 3
 4msg := []byte("release the gophers")
 5opts := &mldsa.Options{Context: "gosuda.org/blog"}
 6
 7sig, _ := sk.Sign(rand.Reader, msg, opts)
 8
 9fmt.Println("private key seed:", len(sk.Bytes()), "bytes")  // 32
10fmt.Println("public key:", len(pk.Bytes()), "bytes")        // 1952
11fmt.Println("signature:", len(sig), "bytes")                // 3309
12
13fmt.Println(mldsa.Verify(pk, msg, sig, opts))
14// <nil>
15fmt.Println(mldsa.Verify(pk, msg, sig, &mldsa.Options{Context: "other"}))
16// mldsa: invalid signature

この数字を見てください。1つの署名は3,309バイトです。Ed25519は64バイトです。50倍の増加であり、公開鍵はさらに2KB近くあります。これらを証明書チェーンにいくつか詰め込むと、TLSハンドシェイクには独自のMTU戦略が必要になり始めます。

これが、今日の耐量子性の実際のコストであり、誰もが明日すべてを切り替えないのはそのためです。しかし、標準ライブラリに入ったので、必要になる_前に_そこにあるのが望ましい状態です。

Options.Contextはドメイン分離です。同じ鍵を異なる目的で署名する際、それぞれ異なるコンテキストを使えば、あるコンテキストでの署名は別のコンテキストでは検証されません。上の例がまさにそれを示しています。同じ鍵、同じメッセージ、異なるコンテキストで、拒否されました。

PrivateKeyはcrypto.Signerを実装しているため、既存のインターフェースにそのまま組み込めます。crypto/x509はML-DSAの鍵と署名を扱え、crypto/tlsはTLS 1.3においてMLDSA44/MLDSA65/MLDSA87の署名方式をサポートします。

また、SignDeterministicもあります。これは乱数を使わないため、テストや再現可能なビルドに便利です。

3.4. simd: アセンブリなしのベクトル命令

Go 1.26では、アーキテクチャ固有のsimd/archsimdが実験的に導入されました。Go 1.27では、移植可能でベクトル幅に依存しないsimdが追加されます。GOEXPERIMENT=simdで有効化します。

 1// dst = a*x + y
 2func axpy(dst, x, y []float32, a float32) {
 3	va := simd.BroadcastFloat32s(a)
 4	w := va.Len()
 5
 6	i := 0
 7	for ; i+w <= len(x); i += w {
 8		vx := simd.LoadFloat32s(x[i:])
 9		vy := simd.LoadFloat32s(y[i:])
10		vx.MulAdd(va, vy).Store(dst[i:])
11	}
12	// 末尾は部分的なロード/ストアで処理する。スカラーの後処理ループは不要
13	if i < len(x) {
14		vx, _ := simd.LoadFloat32sPart(x[i:])
15		vy, _ := simd.LoadFloat32sPart(y[i:])
16		vx.MulAdd(va, vy).StorePart(dst[i:])
17	}
18}
1$ GOEXPERIMENT=simd go1.27rc3 run ./simddemo
2vector bits: 128 emulated: false
3[4 7 10 13 16 19 22 25 28 31]

重要な設計上の選択は、ベクトル幅がハードコードされないことです。実行時にva.Len()を問い合わせます。VectorBitSize()は実際の幅を、Emulated()は実際のハードウェアを使っているのか、それとも丁寧なソフトウェアによる模倣なのかを教えてくれます。私のMacは128ビットのNEONを報告しました。AVX-512のマシンはより大きな値を報告し、何もないマシンはエミュレーションを報告しますが、それでもコードは動作します。

LoadFloat32sPart/StorePartは特筆に値します。手書きのSIMDで最も面倒なのは常に、配列末尾の端数部分ですが、これは別個のスカラーループなしにそれを処理します。

まだ実験的な段階で、APIも不安定ですので、本番環境には入れないでください。しかし、これがアセンブリやcgoではなくGoのコードであるという事実は、本当に大きな進歩です。

3.5. hash/maphash.Hasher

値の型とハッシュベースのコンテナとの間の契約を記述する、新しいインターフェースです。

1type Hasher[T any] interface {
2	Hash(*Hash, T)
3	Equal(x, y T) bool
4}

なぜでしょうか。Goの組み込みマップはcomparableなキーしか受け付けないからです。スライスをキーにすることはできません。「大文字小文字を無視して等しい」と定義することもできません。Hasherは両方を一度に解決します。

 1type CaseInsensitive struct{}
 2
 3func (CaseInsensitive) Hash(h *maphash.Hash, s string) {
 4	h.WriteString(strings.ToLower(s))
 5}
 6func (CaseInsensitive) Equal(x, y string) bool {
 7	return strings.ToLower(x) == strings.ToLower(y)
 8}
 9
10var seed = maphash.MakeSeed()
11
12func hashOf[T any](hr maphash.Hasher[T], v T) uint64 {
13	var h maphash.Hash
14	h.SetSeed(seed)   // 同じシード。そうでなければこの全体が意味を持ちません
15	hr.Hash(&h, v)
16	return h.Sum64()
17}
18
19fmt.Println(hashOf(CaseInsensitive{}, "Go") == hashOf(CaseInsensitive{}, "GO"))
20// true

通常の==の意味論に対しては、ComparableHasher[T]があります。

1hashOf(maphash.ComparableHasher[int]{}, 42)

注意点として、Hasherはインターフェースであり、データ構造ではありません。 これを利用する標準のハッシュテーブルやブルームフィルタは、まだ存在しません。これは将来のコンテナパッケージへの下準備です。現時点では、独自の構造を構築するときに使うか、go/types.Hasherのような既存の実装を利用します(これにより、types.TypeをIdenticalを尊重しながらマップのキーとして使えます)。

シードの管理は利用者の責任です。上のhashOfが呼び出しごとに新しいシードを作っていたら、同じ値でも異なるハッシュになり、例はfalseを出力するでしょう。これは私が最初に試したときに書いてしまったバグそのものです。コンテナごとにシードは1つです。(シードはハッシュフラッディングによるDoS攻撃を防ぐためにランダム化されているので、単なる定数ではありません。)

3.6. httptest.NewTestServer + synctest.Sleep: スリーパーの一撃

このリリースから1つだけ採用するなら、これにしてください。

testing/synctestはGo 1.25で正式版となり、並行コードをテストするための疑似クロックを提供しました。ただし、テストが実際のネットワークに触れた瞬間、その幻想は崩れました。Go 1.27のhttptest.NewTestServerはインメモリの疑似ネットワークを使うため、バブルは保たれます。そしてsynctest.Sleep(time.Sleep + synctest.Waitと同等)が、それを完成させます。

指数バックオフ付きのリトライのテストは次のとおりです。

 1func TestRetryWithBackoff(t *testing.T) {
 2	synctest.Test(t, func(t *testing.T) {
 3		var hits int
 4		srv := httptest.NewTestServer(t, http.HandlerFunc(
 5			func(w http.ResponseWriter, r *http.Request) {
 6				hits++
 7				if hits < 3 {
 8					w.WriteHeader(http.StatusServiceUnavailable)
 9					return
10				}
11				io.WriteString(w, "ok")
12			}))
13
14		client := srv.Client()
15		start := time.Now()
16		var body string
17		for attempt := range 5 {
18			resp, err := client.Get(srv.URL)
19			if err != nil {
20				t.Fatal(err)
21			}
22			b, _ := io.ReadAll(resp.Body)
23			resp.Body.Close()
24			if resp.StatusCode == http.StatusOK {
25				body = string(b)
26				break
27			}
28			synctest.Sleep(time.Duration(1<<attempt) * time.Second)
29		}
30
31		if body != "ok" {
32			t.Fatalf("got %q", body)
33		}
34		// バックオフは正確に1s + 2s = 3sであるべき
35		if elapsed := time.Since(start); elapsed != 3*time.Second {
36			t.Fatalf("elapsed = %v, want 3s", elapsed)
37		}
38		t.Logf("hits=%d elapsed=%v", hits, time.Since(start))
39	})
40}
1=== RUN   TestRetryWithBackoff
2    x_test.go:48: hits=3 elapsed=3s
3--- PASS: TestRetryWithBackoff (0.00s)

二度読んでください。実際のHTTPサーバーに対して3秒間のバックオフをシミュレートしたのに、0.00秒で終わりました。そして、アサーションはelapsed == 3*time.Secondです。「少なくとも3秒、スケジューラのジッターを考慮して」ではなく、_正確に_3秒です。疑似クロックはジッターを起こしません。

synctest.Sleepには特定の理由があります。テストが、テスト対象のコードと同じ時間だけスリープする場合、どちらが先に目を覚ますかは誰にもわかりません。synctest.Sleepはスリープし_、そのうえで_バブル内の他のすべてのgoroutineが確実にブロックされるまで待つため、システムが落ち着いた後の状態を観測できます。

タイムアウト、リトライ、サーキットブレーカー、レートリミッタ。所有する時間依存のHTTPクライアントのテストはすべて、高速かつ決定的になり得ます。現在のテストスイートがtime.Sleep(100 * time.Millisecond)と願望で成り立っているなら、これが脱出口です。

3.7. 本番環境に実際に影響するnet/httpの変更

静かな変更ですが、メトリクスには表れるでしょう。

HTTP/1のレスポンスボディはCloseで自動的にドレインされます。 読み残しのコンテンツは(保守的な上限まで)ボディを閉じたときにドレインされるため、コネクションを再利用できます。つまり、2016年以来、同じStack Overflowの回答から誰もがコピー&ペーストしてきた次のおまじないを、ついに削除できます。

1// もはや不要
2defer func() {
3	io.Copy(io.Discard, resp.Body)
4	resp.Body.Close()
5}()

多くのプログラムでは、これは何もしないか、わずかな改善になります。もしそれで状況が_悪化_する場合は、リリースノートが控えめに説明する領域にいる可能性が高いです。Transport.MaxIdleConnsが0に設定されている、あるいはリクエストごとに新しいClientを作り、アイドルコネクションの上限を完全に迂回している場合です。Transport.DisableKeepAlives = trueで覆い隠すことはできますが、リリースノートの実際の助言は「より深く調べることがおそらく有益である」というもので、これはGoチームの言葉で言えば_もっと大きな問題があります_という意味です。

HTTP/2クライアントの優先度(RFC 9218)。 サーバーはクライアントの優先度シグナルを尊重するようになりました。従来のラウンドロビン方式のスケジューリングを好む場合は、Server.DisableClientPriority = trueを設定してください。

Server.MaxHeaderValueCount。 サーバーが受け入れるヘッダー値の数に上限を設けます。デフォルトはDefaultMaxHeaderValueCountです。「1万個のヘッダーを送って、何が起きるか見てみよう」という手段がもう1つ閉じられました。

ユーザー提供のコネクションでのALPN。 独自のnet.ConnがConnectionState() tls.ConnectionStateを実装していれば、TransportとServerはそれに対してTLSのALPNネゴシエーションを行います。そのため、プロキシを経由するカスタムダイアラーでも、HTTP/2をネゴシエートできます。

3.8. 小さいけれど嬉しい変更

strings.CutLast / bytes.CutLast。 Cutは_最初の_区切り文字で分割します。「最後」版がなかったため、誰もがLastIndexとスライスを手書きし、私たちの約30%が最初の試行でオフバイワンのバグを出していました。

1name, ext, ok := strings.CutLast("archive.tar.gz", ".")
2// archive.tar gz true

ファイル拡張子やhost:portの解析に最適です。IPv6アドレスにはコロンが多く含まれるため、_最後の_コロンを見つけなければならない場面で役立ちます。

math/big.Int.Divide。 明示的な丸めモードを持つ除算です。「QuoとDivのどちらが欲しいのか」という運任せの選択を置き換えます。

1x, y := big.NewInt(-7), big.NewInt(2)
2
3new(big.Int).Divide(x, y, new(big.Int), big.Trunc)  // q=-3 r=-1
4new(big.Int).Divide(x, y, new(big.Int), big.Floor)  // q=-4 r=1
5new(big.Int).Divide(x, y, new(big.Int), big.Round)  // q=-4 r=1
6new(big.Int).Divide(x, y, new(big.Int), big.Ceil)   // q=-3 r=-1

丸め規則が規制に明文化されている分野で働いているなら、これはあなたのためのものです。

url.URL.Cloneとurl.Values.Clone。 ディープコピーです。従来の*uによるシャローコピーはUserinfoポインタを共有していたため、見つけるのに1日かかり、直すのに5秒しかからないような種類のバグを生んでいました。

1orig, _ := url.Parse("https://gosuda.org/blog?tag=go&tag=1.27")
2clone := orig.Clone()
3clone.Host = "example.com"
4fmt.Println(orig.Host, clone.Host)  // gosuda.org example.com

net.UnixConn の読み取りメソッドは、net.OpErrorでラップせずにio.EOFを直接返すようになりました。err == io.EOFが、本来そうあるべきだったように動作するようになりました。

database/sql.ConvertAssignとdriver.RowsColumnScanner。 ドライバ作成者向けの機能です。前者はRows.Scanが行う型変換を公開し、後者はドライバが中間的なアロケーションを省いて、ユーザーの変数に直接スキャンできるようにします。

unicode 15 → 17。 一度に2つのバージョンが上がりました。文字列の分類や正規化の動作がわずかに変わる可能性があります。それに依存するテストがあれば、すぐにわかるでしょう。

compress/flateは高速化されました。 そしてその出力バイト列はGo 1.26と異なる可能性があります。 これはarchive/zip、compress/gzip、compress/zlib、image/pngにも波及します。圧縮出力のハッシュを取るゴールデンファイルのテストがあれば、それらは壊れるでしょうが、それはリグレッションではありません。アップグレードの_前に_確認してください。インシデントの振り返りの最中ではなく。

4. ツールチェーン

4.1. go fixにモダナイザーが増えた

go fixは静かにコードのモダナイズツールになりつつあります。Go 1.27では、atomictypes、embedlit、slicesbackward、unsafefuncsが追加されました。-diffでプレビューできます。

 1package fixdemo
 2
 3import (
 4	"sync"
 5	"sync/atomic"
 6)
 7
 8type Base struct{ ID int }
 9type User struct {
10	Base
11	Name string
12}
13
14func New() User {
15	return User{Base: Base{ID: 1}, Name: "gopher"}
16}
17
18var counter int64
19
20func Incr() { atomic.AddInt64(&counter, 1) }
21
22func Reverse(s []string) {
23	for i := len(s) - 1; i >= 0; i-- {
24		_ = s[i]
25	}
26}
27
28func Spawn(wg *sync.WaitGroup) {
29	wg.Add(1)
30	go func() {
31		defer wg.Done()
32	}()
33}
1$ go1.27rc3 fix -diff ./fixdemo
 1 import (
 2+	"slices"
 3 	"sync"
 4 	"sync/atomic"
 5 )
 6
 7 func New() User {
 8-	return User{Base: Base{ID: 1}, Name: "gopher"}
 9+	return User{ID: 1, Name: "gopher"}
10 }
11
12-var counter int64
13+var counter atomic.Int64
14
15-func Incr() { atomic.AddInt64(&counter, 1) }
16+func Incr() { counter.Add(1) }
17
18 func Reverse(s []string) {
19-	for i := len(s) - 1; i >= 0; i-- {
20-		_ = s[i]
21+	for _, v := range slices.Backward(s) {
22+		_ = v
23 	}
24 }
25
26 func Spawn(wg *sync.WaitGroup) {
27-	wg.Add(1)
28-	go func() {
29-		defer wg.Done()
30-	}()
31+	wg.Go(func() {
32+	})
33 }

1つの小さなファイルで4つのモダナイザーが発動しました。

  • embedlit: §1.2の新しい構文を使って、埋め込みリテラルを書き換えます
  • atomictypes: atomic.AddInt64(&x, 1)はatomic.Int64.Add(1)になります。これにより非アトミックなアクセスが不可能になり、知らなかった32ビットのアライメントのバグも静かに修正されます
  • slicesbackward: 後方へのループはslices.Backwardになります
  • waitgroupgo: Add/go/Doneの定型句はwg.Goになります(曖昧さを避けるため、1.26のwaitgroupから名前が変更されました)

go tool fix helpは26個すべてを一覧表示し、go tool fix help <name>は1つを説明します。また、fmtappendfは「文体上の懸念」を理由に削除されました。これは、そのissueスレッドで何が起きたのかを、見事に外交的に表現したものです。

古いコードベースに対してgo fix ./...を実行するのは、本当に満足のいく午後の作業です。もちろん、まず差分を読んでください。

4.2. go testはデフォルトでstdversionを実行する

これは、あなたの一日を中断させる可能性が最も高い変更です。

go testはデフォルトでstdversionのvetチェックを実行するようになり、go.modのgoディレクティブが許容する範囲より新しい標準ライブラリのシンボルにフラグを立てます。

1// go.modには: go 1.24
2package sv
3
4import "strings"
5
6func Ext(name string) string {
7	_, ext, _ := strings.CutLast(name, ".")  // CutLastは1.27のシンボルです
8	return ext
9}
1$ go1.27rc3 test ./...
2# sv
3./x.go:6:23: strings.CutLast requires go1.27 or later (module is go1.24)
4FAIL	sv [build failed]

これにより、自分のマシン(最新のツールチェーン)では全て動くのに、CIやユーザーの古い環境で爆発する、という典型的な失敗パターンが防がれます。保守的なgoディレクティブを持つライブラリを公開しているなら、これは贈り物です。

4.3. go test -jsonにOutputTypeが追加された

"Action":"output"の行に、オプションの"OutputType"フィールドが付くようになりました。値は"error"、"error-continue"、"frame"のいずれかです。

1'frame'  '=== RUN   TestFail\n'
2None     '    x_test.go:6: hello\n'
3'error'  '    x_test.go:7: boom\n'
4'frame'  '--- FAIL: TestFail (0.00s)\n'
5'frame'  'FAIL\n'

t.Logの出力(フィールドなし)、t.Errorの出力(error)、フレームワークが生成する行(frame)が区別できるようになりました。テスト出力のどの行が「本物」かを判定するために正規表現を書いたことがある人は、それを削除できます。

4.4. go docの改善

package@version構文。 モジュールに追加せずに、特定のバージョンのドキュメントを読めます。

1$ go1.27rc3 doc golang.org/x/sync/errgroup@v0.10.0
2package errgroup // import "golang.org/x/sync/errgroup"
3...

アップグレード前に何が変わったかを確認するのに便利です。

-exとサンプルのソース表示:

 1$ go1.27rc3 doc -ex strings | grep Example
 2    func ExampleClone()
 3    func ExampleCompare()
 4    func ExampleContains()
 5    func ExampleCut()
 6    func ExampleCutPrefix()
 7    ...
 8
 9$ go1.27rc3 doc strings.ExampleCut
10package main
11
12import (
13	"fmt"
14	"strings"
15)
16
17func main() {
18	show := func(s, sep string) {
19		before, after, found := strings.Cut(s, sep)
20		fmt.Printf("Cut(%q, %q) = %q, %q, %v\n", s, sep, before, after, found)
21	}
22	show("Gopher", "Go")
23	...
24}
25
26Output:
27Cut("Gopher", "Go") = "", "pher", true

サンプルのソースと期待される出力が、ターミナル上で、来週の木曜日になってもまだ開いているであろうブラウザタブを開かずに確認できます。

4.5. go mod tidyがrequireブロックを整理する

go 1.27以降のモジュールでは、go mod tidyが散在するrequireブロックを最大2つ(直接と間接)にまとめ、付随するコメントを保持します。

 1// 変更前
 2module tidydemo
 3
 4go 1.27
 5
 6require golang.org/x/sync v0.10.0
 7
 8// networking
 9require golang.org/x/net v0.33.0
10
11require (
12	golang.org/x/sys v0.28.0 // indirect
13)
14
15require golang.org/x/text v0.21.0 // indirect
 1// 変更後: go mod tidy
 2module tidydemo
 3
 4go 1.27
 5
 6require (
 7	// networking
 8	golang.org/x/net v0.33.0
 9	golang.org/x/sync v0.10.0
10)

// networkingコメントが残っていることに注意してください。これは主に、Gitのマージコンフリクトの解決の後始末をします。迷子のrequireブロックが生まれるのはそこです。チームで3人以上が依存関係を追加しているなら、go.modのコンフリクトは目に見えて静かになるでしょう。

4.6. その他

  • bzrサポートを削除しました。 これが影響する場合は、ぜひその経緯を聞かせてください。
  • go tool trace -httpは、ポートのみを指定した場合にlocalhostにバインドされます。 -http=:6060はもはや全インターフェースで待ち受けません。本当にそうしたい場合は-http=0.0.0.0:6060を使ってください。これはgo tool pprofと揃えられ、時折起きる偶発的な公開プロファイラを防ぎます。
  • **レスポンスファイル(@file)**がcompile、link、asm、cgo、cover、packでサポートされるようになりました。GCC互換の形式です。コマンドライン長の制限を超えてしまうビルドシステム向けです。Bazelの皆さん、こんにちは。

5. コンパイラ、リンカ、ポート

コンパイラ。 //lineディレクティブの相対ファイル名は、go/scannerと同様に、含まれるファイルのディレクトリを基準に解決されるようになりました。コードジェネレータを書いている場合に関係します。

関数リテラル(クロージャ)のシンボル名も単純になりました。インライン化の有無にかかわらず同じ名前になり、同じリテラルの複数のインスタンスがバイナリ内でコードを共有することがあります。機能的な変更はありませんが、_例外_として、reflect.Value.Pointerで関数の同一性を比較しているコードは、以前より頻繁に「等しい」と判定するようになります。その比較はもともと正しくありませんでしたが、それを使っているなら、今度はより大きな声で嘘をつき始めます。

リンカ。 新しい-macosと-macsdkオプションで、macOSのLC_BUILD_VERSIONロードコマンドのOSとSDKのバージョンを設定できます。

ポート。

  • Darwinは現在、macOS 13 Ventura以降を必要とします。 これは1.26で発表されたとおりです。CIのランナーを確認してください。
  • PowerPC(GOOS=linux GOARCH=ppc64)はELFv2 ABIに切り替わりました。 Linuxカーネル3.13以降が必要です(RHEL7は3.10にバックポートしています)。cgo、PIE、外部リンクがサポートされるようになりました。cgoを使っているが静的な純粋なGoのバイナリが必要な場合は、CGO_ENABLED=0を設定してください。

6. アップグレードのチェックリスト

Go 1.27は互換性を真剣に扱っていますが、バージョンを上げる前に次の点を確認してください。

  • 圧縮出力のゴールデンファイルテスト。 compress/flateはエンコーダが変わりました。gzip、zip、pngのバイト列が異なる可能性があります。
  • JSONのエラーメッセージ文字列に一致するテスト。 動作は同一ですが、エラーテキストは同一ではありません。
  • go.modにある削除されたGODEBUG。 asynctimerchan、gotypesalias、tlsrsakex、tls3des、tls10server、tlsunsafeekm、x509keypairleaf。これらは_古い_値に設定されている場合にのみビルドを失敗させます。
  • stdversion違反。 go testがこれらを検出するようになりました。保守的なgoディレクティブを持つライブラリを管理しているなら、早めに実行してください。
  • macOS 12以前のCIランナー。 サポートは終了しました。
  • 関数リテラルのシンボル名に依存するテスト、そして**reflect.Value.Pointerによる関数の比較。**
  • Transport.MaxIdleConns = 0、またはリクエストごとのClient。自動ドレインのレスポンスボディと組み合わさると、遅くなる可能性があります。

結びに

Go 1.27は、先送りされていた多くの保守作業が一度に回ってきたリリースです。

ジェネリックメソッドは、1.18のジェネリクス作業に欠けていたピースでした。構造体リテラルのフィールドセレクタは、11年間開かれていたissueを閉じました。encoding/json/v2は2年にわたる提案の議論の成果です。そしてuuidは、ほぼすべてのGoプロジェクトが10年にわたって同じサードパーティ依存関係で埋めてきた穴を埋めました。

この機能から実際に価値を得るための優先順位を挙げるなら、次のとおりです。

  1. httptest.NewTestServer + synctest.Sleep: 時間に依存するHTTPテストが高速かつ決定的になります。リリース全体で最も効果に対する労力が小さい機能です。
  2. goroutineリークプロファイル: ステージング環境でエンドポイントを1つ公開し、謙虚さを覚悟してください。
  3. トレースバックのgoroutineラベル: リクエストの入口にpprof.Doを3行書くだけで、次の午前3時のスタックダンプが読めるものになります。
  4. go fix ./...: モダナイズはツールに任せましょう。
  5. encoding/json/v2: 急ぐ必要はありません。一度に1つのオプションずつ採用してください。

go1.27rc3を入手して、今すぐテストスイートを実行してください。このチェックリストにあるすべての項目は、インシデントの最中よりも、火曜日の午後に発見するほうがはるかに快適です。

参考文献