GoSuda

Go 1.27、あるいは私がどのようにして心配するのをやめ、Generic Methodsを愛するようになったか

By iwanhae
views ...

Overview of Go 1.27

Go 1.27は2026年8月にリリースされます。今回は、言語そのものに変更が加えられました。「slicesパッケージに新しい関数を追加した」といったレベルではなく、真の意味での変更です。ジェネリックメソッドが導入され、11年越しに未解決だった課題がクローズされました。また、encoding/jsonは、システムが稼働したままの状態で、静かに新しいエンジンへと置き換えられました。

以下のすべての例は、go1.27rc3 (darwin/arm64) 上で実行したものです。すべての出力ブロックは、エラーメッセージを含め、実際の出力をそのままコピー&ペーストしたものです。もし不自然に見える箇所があれば、それはコンパイラが実際にそのように出力したためです。

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

1. 言語の変更(真実です)

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

Go 1.18でジェネリクスが導入されて以来、私たちは「ある議論」を続けてきました。それは以下のようなものです。

「スライス型にMapメソッドを定義したいのだが――」

method must have no type parameters

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

これまでは、メソッドが使用できる型パラメータは、レシーバーによって宣言されたものに限られていました。メソッド自身が新しい型パラメータを導入することはできなかったのです。そのため、あらゆるジェネリックな変換処理はパッケージスコープに追いやられ、MapSliceのバリエーションといった名前の、無数に存在する独立したヘルパー関数の隣に放置されてきました。

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/v2Randはジェネリックメソッドを得ました。

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年に報告され、今回ついにクローズされました。どこかの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}}

nameLineのフィールドではありません。埋め込まれた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. 関数型推論の明確化

ジェネリック関数の型推論は、ある場所では機能し、別の場所では機能しないという、一貫性のない状態でした。変数への代入はOKでしたが、同じ関数を構造体のフィールドに入れるとコンパイルエラーになり、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}

ひとつのdouble関数が、文脈から完全に推論され、ある場所ではfunc(int) intとして、別の場所では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# default (size-specialized malloc on)
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

Apple Silicon上でアロケーションのみを行うマイクロベンチマークでは30〜35%の高速化が見られました。これはあくまで好条件下の数値であり、実際のプログラムではGCやその他の処理が支配的になるため、約1%という主張は正直な数値と言えます。

コストはバイナリサイズです。Hello Worldの場合:

12413202 bytes  (default)
22361826 bytes  (nosizespecializedmalloc)

プログラムに関わらず、約50KBの増加です。GOEXPERIMENT=nosizespecializedmallocで無効化できますが、この回避策はGo 1.28で削除予定であるため、ライフスタイルではなくバグ報告のワークアラウンドとして扱うべきです。

2.2. ゴルーチンリークプロファイルの正式対応

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つのゴルーチンが永久にスタックしており、ファイル名と行番号によって、どこでリークが発生したかが正確に示されます。

この仕組みは非常に巧妙で、ガベージコレクタの到達可能性分析を再利用しています。ゴルーチンGがプリミティブPでブロックされており、かつPが実行可能などのゴルーチン(またはそれらが起床させうるもの)からも到達不能であれば、Gが再び起床することはありません。これはヒューリスティックではなく、証明です。

この設計には当然制限があります。チャネルやミューテックスがグローバル変数や実行中のゴルーチンのローカル変数を通じて到達可能であれば、GCはそれを認識できるため、結論を出すことはできません。上記の例でも、報告には2回のruntime.GC()が必要であることに注意してください。すべてを検知できるわけではありません。

しかし、「コンテキストのキャンセル忘れによりワーカーゴルーチンが永久に生存する」といった典型的なリークは検知可能です。

net/http/pprofをインポートすれば、/debug/pprof/goroutineleakでも確認できます。ステージング環境に組み込み、毎週チェックして密かに戦慄してください。

この機能はUberのVlad Saioc氏による貢献であり、彼には好きな飲み物を贈るべきでしょう。

2.3. トレースバックによるリクエストの識別

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

 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...

重要な詳細は、ラベルは子ゴルーチンに継承される点です。ゴルーチン3はラベルを設定していませんが、親から継承しました。

プロダクション環境でデッドロックが発生し、SIGQUITを送信した際、すべて同一に見える4,000個のゴルーチンの代わりに、どのリクエスト、どのテナントに属するかタグ付けされた4,000個のゴルーチンが得られます。リクエストの入り口でpprof.Doを3行書くだけでこれが実現します。

ラベルにstderrへの出力に適さない情報が含まれる場合は、GODEBUG=tracebacklabels=0でオフにできます。この無効化オプションは今後も提供され続けます。

2.4. asynctimerchanの恒久的な削除

Go 1.23でタイマーチャネルは非バッファ型(同期)になり、asynctimerchan=1によって旧挙動に戻すことができました。1.27では、この設定は恒久的に削除されます。timeパッケージのチャネルは同期型であり、交渉の余地はありません。

面白いのは、これに伴って導入されたポリシーです。go.modに残された削除済みのGODEBUGは、自動的にビルドを壊すわけではありません。古い値に設定されている場合にのみエラーとなります。

1# go.mod contains: 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 contains: 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はまさにこれでした。拒否するのは正しい判断です。

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

 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"

DeterministicMatchCaseInsensitiveNamesStringifyNumbersFormatNilSliceAsNullOmitZeroStructFieldsなど、以前はサードパーティライブラリや自作のMarshalJSONで解決していた多くのことが、フラグとして提供されます。

そして移行ストーリーが最高です。後から指定したオプションが優先されるため、v2の厳格さを一つずつ採用できます。

1// v1のセマンティクスを維持しつつ、v2のように重複キーを拒否する
2jsonv2.Unmarshal(data, &v,
3	json.DefaultOptionsV1(),
4	jsontext.AllowDuplicateNames(false))
5// duplicate object member name

重複キーを拒否するためだけに20万行のコードベースをv2に移植する必要はありません。オプションを一つ切り替えるだけです。大規模なプロジェクトにとって、これは現実的なパスです。

パフォーマンス:マーシャリングはほぼ同等、アンマーシャリングは大幅に高速化されています。問題がある場合は、GOEXPERIMENT=nojsonv2で旧実装に戻せます。ただし、この回避策も将来的に削除される予定なので、甘んじるのではなくイシューを立ててください。

jsontextは低レイヤー層です。Encoder/DecoderがJSONをTokenValueとして歩行し、ステートマシンが整合性を保ちます。ストリーミング変換やJSONフィルタを作成する際、Go値を具体化したくない場合に利用してください。

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はNewNewV4NewV7NilMaxParseMustParse、そして型type UUID [16]byteのみです。コーヒーを飲んでいる間にドキュメントをすべて読めるほど簡潔です。

知っておくべき詳細:

  • **UUID[16]byte**であるため、==が機能し、マップのキーとして直接使用可能です。google/uuidと同じ設計です。
  • NilMaxは変数ではなく関数です。 パッケージレベルのvar Nil UUIDは、自身の足を撃ち抜くようなものであり、いつか誰かが代入してしまう可能性があるためです。
  • ランダムビットは暗号学的に安全なジェネレータから生成されます。
  • encoding.TextMarshaler/TextUnmarshaler/TextAppenderを実装しているため、JSON構造体にそのまま渡せます。
  • NewV7は、データベースの主キーに推奨されます。 時間順であるためBツリーインデックスの断片化が抑えられます。v4 UUIDはこれが苦手なことで有名です。

依存関係を一つ削除できます。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

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

これが現在の耐量子化のコストであり、誰もが明日すべてを移行しない理由です。しかし、標準ライブラリに入ったことで、必要になる前に利用できるようになりました。

Options.Contextはドメイン分離です。同じ鍵で異なる目的の署名を行う場合、コンテキストを分ければ、片方で署名したものが他方で検証されることはありません。上記の例はまさにそれを示しており、同じ鍵、同じメッセージでもコンテキストが違えば拒否されます。

PrivateKeycrypto.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のように既存の実装を消費するために使用します。

シード管理は自分で行う必要があります。もし上のhashOfが呼び出しごとにシードを新しく作成したら、同一値でもハッシュ値が異なり、例はfalseを出力したでしょう。コンテナごとに一つのシードが必要です。(シードはハッシュフラッディングDoS攻撃を回避するためにランダム化されるため、定数ではありません。)

3.6. httptest.NewTestServer + synctest.Sleep: 必携のテストツール

今回のリリースで一つだけ採用するなら、これをお勧めします。

testing/synctestはGo 1.25で導入され、並行コードテスト用の偽の時計を提供しましたが、ネットワークに触れた瞬間に幻想は崩壊しました。Go 1.27のhttptest.NewTestServerインメモリの偽ネットワークを使用するため、バブルが維持されます。そしてsynctest.Sleeptime.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)

2回読んでください。実際のHTTPサーバーに対して3秒間のバックオフをシミュレートし、0.00秒で完了しました。アサーションはelapsed == 3*time.Secondであり、スケジューラのジッターに左右されない、正確な3秒です。偽の時計はジッターを起こしません。

synctest.Sleepには理由があります。テストがテスト対象コードと同じ時間だけスリープすると、どちらが先に起きるかは予測できません。synctest.Sleepはスリープした後に、バブル内の他のすべてのゴルーチンが確実にブロックされるまで待機するため、システムが落ち着いた後に観測できます。

タイムアウト、リトライ、サーキットブレーカー、レートリミッターなど、時間依存のHTTPクライアントテストはすべて、高速で決定論的なものになります。テストスイートがtime.Sleep(100 * time.Millisecond)と希望的観測で成り立っているなら、これが脱出の手段です。

3.7. プロダクションに影響するnet/httpの変更

静かですが、メトリクスに現れます。

HTTP/1レスポンスボディのClose時自動ドレイン。 未読コンテンツはBodyを閉じた時に(保守的な制限まで)ドレインされるため、接続を再利用できます。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万個送って何が起きるか見てみよう」という攻撃への扉が一つ閉じられました。

ユーザー提供接続でのALPN。 net.ConnConnectionState() tls.ConnectionStateを実装している場合、TransportServerは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 丸めモードを明示的に指定する除算。QuoDivのどちらを使うべきかというコイン投げに代わるものです。

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.Cloneurl.Values.Clone ディープコピーです。古い*uのシャローコピーはUserinfoポインタを共有しており、見つけるのに一日、直すのに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.ConvertAssigndriver.RowsColumnScanner ドライバ作成者向けの機能です。前者はRows.Scanが行う型変換を露出させ、後者はドライバがユーザーの宛先に直接スキャンできるようにして中間アロケーションをスキップさせます。

unicode 15 → 17。 一気に2バージョン更新されました。文字列の分類や正規化の挙動が微妙に変わる可能性があります。依存するテストがあれば、判明するでしょう。

compress/flateが高速化されましたが、出力バイトがGo 1.26と異なる可能性があります。 これはarchive/zipcompress/gzipcompress/zlibimage/pngに連鎖します。圧縮出力をハッシュ化するゴールデンファイルテストがあれば、壊れるでしょう。これはリグレッションではありません。アップグレード中ではなく、前に確認してください。

4. ツールチェーン

4.1. go fixのモダン化

go fixはコードモダン化ツールになりつつあります。Go 1.27ではatomictypesembedlitslicesbackwardunsafefuncsが追加されました。-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 }

4つのモダン化が適用されました:

  • embedlit — §1.2の新構文を使用するように埋め込みリテラルを書き換え
  • atomictypesatomic.AddInt64(&x, 1)atomic.Int64.Add(1)に変換。非アトミックアクセスを不可能にし、32ビットアライメントバグを修正
  • slicesbackward — 後方ループをslices.Backwardに変換
  • waitgroupgoAdd/go/Doneの儀式をwg.Goに変換(曖昧さを避けるため1.26のwaitgroupから改名)

go tool fix helpに全26個がリストされています。古いコードベースでgo fix ./...を実行するのは、非常に満足感のある午後の過ごし方です。もちろん、先にdiffを読んでください。

4.2. go testがstdversionをデフォルトで実行

最も日常を中断させる可能性が高い変更です。

go testはデフォルトでstdversion vetチェックを実行し、go.modで許可されたバージョンよりも新しい標準ライブラリシンボルを使用している場合にフラグを立てます。

1// go.mod says: 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// Before
 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// After: 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コメントが残っている点に注目してください。これは主に、散発的なrequireブロックが生まれる原因であるGitのマージコンフリクト解決後の掃除に役立ちます。

4.6. その他

  • bzrサポート削除。 これが影響するなら、ぜひ話を聞かせてください。
  • go tool trace -httpのポート指定。 ポートのみ指定した場合、localhostにバインドされます。-http=:6060は全インターフェースをリッスンしなくなりました。必要なら-http=0.0.0.0:6060を使用してください。
  • レスポンスファイル (@file)。 GCC互換形式でcompilelinkasmcgocoverpackがサポートされました。コマンドライン長制限を超えるビルドシステム(Bazelなど)向けです。

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

コンパイラ。 //lineディレクティブ内の相対パスは、包含ファイルが存在するディレクトリに対して解決されるようになり、go/scannerと整合しました。コード生成を行う場合に重要です。

関数リテラル(クロージャ)のシンボル名も単純化されました。インライン化に関わらず同一名になり、同一リテラルの複数インスタンスでコードが共有される場合があります。機能変更はありませんが、reflect.Value.Pointerで関数同一性を比較するコードは、以前よりも「等しい」と判定される機会が増えます。その比較は元々有効ではありませんでしたが、もし行っているなら、嘘をつく頻度が増えるでしょう。

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

ポート。

  • DarwinはmacOS 13 Ventura以降を必須とします。 CIランナーを確認してください。
  • PowerPC (GOOS=linux GOARCH=ppc64)はELFv2 ABIに移行しました。 Linuxカーネル3.13以降が必要です。cgo、PIE、外部リンキングがサポートされます。cgoを使用していて静的な純Goバイナリが必要な場合は、CGO_ENABLED=0を設定してください。

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

Go 1.27は互換性を重視していますが、バージョンを上げる前に以下を確認してください。

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

結び

Go 1.27は、長年先送りされてきたメンテナンスが一気に解決されるリリースです。

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

価値を得るための優先順位:

  1. httptest.NewTestServer + synctest.Sleep — 時間依存のHTTPテストが高速かつ決定論的になります。費用対効果が最高です。
  2. ゴルーチンリークプロファイル — ステージングでエンドポイントを一つ公開し、謙虚になる準備をしてください。
  3. トレースバックのゴルーチンラベル — リクエストの入り口でpprof.Doを3行書けば、次の深夜3時のスタックダンプが読みやすくなります。
  4. go fix ./... — ツールにモダン化させましょう。
  5. encoding/json/v2 — 急ぐ必要はありません。オプションごとに採用してください。

go1.27rc3を取得し、テストスイートを実行してください。チェックリストの項目は、事件発生時ではなく、火曜日の午後に発見する方がはるかに快適です。

参考文献