Go 1.26 の変更の多くは、毎日書くコードにそのまま関わってきます。ポインタやエラー型まわりのボイラープレートが減り、goroutine リークを見つける新しい手段が加わり、再ビルドするだけでランタイムが速くなります。
目次
- new(expr) でポインタをすっきり初期化する
- errors.AsType による型安全なエラーチェック
- 自己参照するジェネリックな型制約
- よりシンプルになった暗号 API
- goroutine リークを見つける
- 標準ライブラリの改善
- テストとログ
- パフォーマンス向上と Green Tea GC
- ランタイムの新機能
- 既存コードの移行
new(expr) でポインタをすっきり初期化する
オプションのフィールドをポインタで表す Go の API を使ったことがあれば、次のようなコードを書いたことがあるはずです。
example.gogoport := 8080 config := &Config{Port: &port} // Can't use &8080 directly!
Go ではリテラル値のアドレスを取ることができないため、先に一時変数を用意する必要があります。intPtr(n int) *int のようなヘルパーを足すコードベースもありますが、問題が別の場所に移るだけです。
Go 1.26 は組み込み関数 new() を拡張して、これを解消しました。以前は型しか受け取れず、new(int) はゼロを指す *int を返すだけでした。今は任意の式を渡せます。
example.gogopackage main import "fmt" type ServerConfig struct { Host string Port *int Enabled *bool } func main() { config := ServerConfig{ Host: "localhost", Port: new(8080), Enabled: new(true), } fmt.Printf("Port: %d, Enabled: %t\n", *config.Port, *config.Enabled) }
new(8080) と書くと、Go は式を評価し、その型(ここでは int)のメモリを確保して値を格納し、ポインタを返します。式なら何でも使えます。計算した値なら new(x + y)、関数の結果なら new(time.Now())、文字列なら new("default") です。
いちばん出番が多いのは、JSON API や Protobuf メッセージ、そしてポインタ型のフィールドで「値のあるオプション」と「未指定」を区別する struct 全般です。
errors.AsType による型安全なエラーチェック
ラップされたエラーから特定の型を取り出すには、これまで errors.As() を使ってきました。
example.gogovar appErr *AppError if errors.As(err, &appErr) { fmt.Printf("Code: %d\n", appErr.Code) }
動きはしますが、書き心地はよくありません。if ブロックの中でしか使わない変数が、外側のスコープに漏れてしまいます。appErr がすでにポインタなのに &appErr を渡すので、つまずく人も多いです。しかも errors.As は内部でリフレクションに頼っています。
Go 1.26 では、同じことをもっと簡潔に書けるジェネリック関数 errors.AsType[T]() が追加されました。
example.gogopackage main import ( "errors" "fmt" ) type AppError struct { Code int Message string } func (e *AppError) Error() string { return fmt.Sprintf("app error %d: %s", e.Code, e.Message) } func main() { err := &AppError{Code: 404, Message: "not found"} if appErr, ok := errors.AsType[*AppError](err); ok { fmt.Printf("Code: %d, Message: %s\n", appErr.Code, appErr.Message) } // Works with wrapped errors too wrappedErr := fmt.Errorf("operation failed: %w", err) if appErr, ok := errors.AsType[*AppError](wrappedErr); ok { fmt.Printf("Unwrapped code: %d\n", appErr.Code) } }
型パラメータ [*AppError] で何を探しているのかをコンパイラに正確に伝えられ、結果のスコープも if ブロックの中に収まります。ポインタのポインタも出てきません。型がコンパイル時に分かるのでリフレクションも不要になり、エラー処理が頻繁に通るホットパスでは速くなります。
自己参照するジェネリックな型制約
Go 1.26 では、再帰的な型制約が限定的にサポートされました。これで初めて書けるようになったパターンがあります。たとえば、同じ型の別の値と自分自身を比較する型です。
example.gogopackage main import "fmt" type Comparable[T any] interface { CompareTo(other T) int } type Integer int func (i Integer) CompareTo(other Integer) int { if i < other { return -1 } else if i > other { return 1 } return 0 } func Max[T Comparable[T]](a, b T) T { if a.CompareTo(b) > 0 { return a } return b } func main() { result := Max(Integer(5), Integer(3)) fmt.Printf("Max: %d\n", result) }
制約 Comparable[T] は自身の型パラメータを参照しており、Max[T Comparable[T]] は T が自分自身と比較できることを求めます。Go 1.26 より前は、こうした自己参照するジェネリックな型制約はコンパイルできませんでした。今は、メソッドチェーンで書く fluent API、ノードが自身の型を参照する木、各ステップで正しい型を返す型安全なビルダーなどを作れます。
よりシンプルになった暗号 API
多くの Go 開発者が、次の 1 行を書いたことがあるはずです。
example.gogokey, err := rsa.GenerateKey(rand.Reader, 2048)
rand.Reader 引数は歴史的な名残です。初期の Go では、テスト用に独自の乱数源を渡せました。実際には常に crypto/rand.Reader を渡すべきで、math/rand など別のものを渡すとセキュリティホールになります。
Go 1.26 では、安全な選択肢がデフォルトになりました。nil を渡すと、関数は内部で crypto/rand.Reader を使います。
example.gogopackage main import ( "crypto/rand" "encoding/hex" "fmt" ) func main() { key := make([]byte, 32) rand.Read(key) fmt.Printf("Generated key: %s\n", hex.EncodeToString(key)) }
対象は暗号パッケージ全体の鍵生成で、rsa.GenerateKey(nil, bits)、ecdsa.GenerateKey(curve, nil)、ecdh.GenerateKey(curve, nil) がそうです。自前の reader を渡す理由が残るのは、決定的な出力が必要なテストくらいです。
HPKE による最新の暗号化
新しい crypto/hpke パッケージは、ハイブリッド公開鍵暗号の最新の標準である RFC 9180 を実装しています。RSA 単体では低速で、小さなメッセージしか暗号化できません。よくある回避策は、ランダムな共通鍵を RSA で暗号化し、その鍵で実際のデータを暗号化する方法です。これで動きますが、実装には注意が必要です。
HPKE は、このパターンを最新のアルゴリズムで標準化したものです。鍵付きの箱を思い浮かべてください。送信者は使い捨ての錠と鍵を作り、メッセージを箱に入れて錠をかけます。そして箱と一緒に、受信者の秘密鍵でしか使えない「使い捨ての鍵を作り直すための手順」を送ります。
example.gogosuite := hpke.NewSuite( hpke.DHKEM_X25519, hpke.KDF_HKDF_SHA256, hpke.AEAD_ChaCha20Poly1305, ) publicKey, privateKey, _ := suite.GenerateKeyPair(nil) sender, _ := suite.NewSender(publicKey, nil) ciphertext, _ := sender.Seal(plaintext, nil) encapsulated := sender.EncapsulatedKey() recipient, _ := suite.NewRecipient(privateKey, encapsulated) decrypted, _ := recipient.Open(ciphertext, nil)
HPKE の鍵は、RSA の 2048〜4096 ビットに対して 256 ビットで済みます。どんなサイズのメッセージも扱え、ずっと高速です。
goroutine リークを見つける
goroutine リークは見逃しやすい問題です。メモリリークはいずれプログラムをクラッシュさせます。goroutine リークはただ少しずつ遅くするだけで、止まった goroutine が何千個も溜まり、何の仕事もしないままメモリと CPU を握り続けます。
よくあるリークの書き方はこうです。
example.gogofunc handleRequest(ctx context.Context) error { results := make(chan Result) go func() { result := expensiveOperation() results <- result // Blocks forever if context cancels }() select { case <-ctx.Done(): return ctx.Err() // Goroutine still blocked on send case r := <-results: return processResult(r) } }
expensiveOperation() が戻る前に context がキャンセルされると、goroutine はバッファなしの channel に送信しようとしたまま永遠にブロックします。その channel を読む相手はもう現れません。
Go 1.26 では、こうしたリークを自動で検出する goroutineleak プロファイルと go test -goroutineleak フラグが追加されました。前後の goroutine 数を数えてリークを確かめることもできます。
example.gogopackage main import ( "fmt" "runtime" "time" ) func createLeak() { ch := make(chan int) go func() { <-ch // Blocks forever - no sender }() } func main() { before := runtime.NumGoroutine() createLeak() time.Sleep(50 * time.Millisecond) after := runtime.NumGoroutine() fmt.Printf("Before: %d, After: %d\n", before, after) if after > before { fmt.Println("Goroutine leak detected!") } }
修正は 3 つの部分からなります。送信がブロックしないためのバッファ付き channel、context がキャンセルされたら抜けられるようにする goroutine 内の select、そして呼び出し側での context のチェックです。
example.gogopackage main import ( "context" "fmt" "time" ) func expensiveWork() string { time.Sleep(50 * time.Millisecond) return "completed" } func safeHandler(ctx context.Context) (string, error) { ch := make(chan string, 1) go func() { result := expensiveWork() select { case ch <- result: case <-ctx.Done(): return } }() select { case <-ctx.Done(): return "", ctx.Err() case result := <-ch: return result, nil } } func main() { ctx1 := context.Background() result1, err1 := safeHandler(ctx1) fmt.Printf("Normal: %s, err: %v\n", result1, err1) ctx2, cancel := context.WithCancel(context.Background()) cancel() result2, err2 := safeHandler(ctx2) fmt.Printf("Canceled: %s, err: %v\n", result2, err2) }
バッファ付きの channel なので、誰も受信しなくても送信はブロックしません。select は、context がキャンセルされたときの goroutine の出口になります。処理が成功してもキャンセルされても、goroutine は終了します。
Go 1.26 では runtime/metrics に goroutine のメトリクスも加わりました。本番環境のダッシュボードで、goroutine の総数、ブロック中の数、実行可能な数を追えます。
標準ライブラリの改善
Buffer.Peek
bytes.Buffer から読み取ると、データは消費されます。メッセージの種類を見てからパーサーを選びたい、読む前にフォーマットを検証したい、といった理由で先読みしたいときは、バッファをコピーするしかありませんでした。
Go 1.26 では Peek() が追加されました。
example.gogopackage main import ( "bytes" "fmt" ) func main() { buf := bytes.NewBufferString("Hello, World!") peeked := buf.Bytes()[:5] fmt.Printf("Peeked: %s\n", peeked) fmt.Printf("Full content: %s\n", buf.String()) fmt.Printf("Length: %d\n", buf.Len()) }
IP プレフィックスの比較
IP プレフィックスをソートするには、以前はバイトを手で比較していました。今は netip.Prefix に Compare() メソッドがあり、そのまま slices.SortFunc に渡せます。
example.gogopackage main import ( "fmt" "net/netip" "slices" ) func main() { prefixes := []netip.Prefix{ netip.MustParsePrefix("192.168.0.0/16"), netip.MustParsePrefix("10.0.0.0/8"), netip.MustParsePrefix("172.16.0.0/12"), } slices.SortFunc(prefixes, func(a, b netip.Prefix) int { return a.Compare(b) }) for _, p := range prefixes { fmt.Println(p) } }
シグナルによるキャンセルの原因を取得する
グレースフルシャットダウンに signal.NotifyContext() を使っているなら、どのシグナルでキャンセルされたのかが分かるようになりました。
example.gogoctx, stop := signal.NotifyContext( context.Background(), os.Interrupt, syscall.SIGTERM, ) defer stop() <-ctx.Done() if cause := context.Cause(ctx); cause != nil { switch cause { case os.Interrupt: log.Println("User interrupted (Ctrl+C)") case syscall.SIGTERM: log.Println("Graceful shutdown requested") } }
テストとログ
slog.MultiHandler
本番のサービスでは、ログを複数の場所に出すことがよくあります。ログ集約用に JSON をファイルへ、デバッグ用にテキストを標準出力へ、エラーを監視サービスへ、といった形です。slog.MultiHandler がこの振り分けを引き受けてくれるので、自分で書く必要はありません。
example.gogopackage main import ( "log/slog" "os" ) func main() { jsonHandler := slog.NewJSONHandler(os.Stdout, nil) logger := slog.New(jsonHandler) logger.Info("Server started", "port", 8080) }
各ハンドラはそれぞれのレベルでフィルタします。ファイルには debug レベルですべてを出し、コンソールには警告とエラーだけを出す、といったことができます。
テストの成果物ディレクトリ
統合テストは、スクリーンショット、SQL ダンプ、生成したレポートなどのファイルを書き出すことがよくあります。これまでは /tmp などのディレクトリに散らばり、誰も片付けませんでした。
t.ArtifactDir() は、その置き場所を標準で用意します。テストが成功すれば削除され、失敗すれば残ります。
example.gogofunc TestGenerateReport(t *testing.T) { artifactDir := t.ArtifactDir() reportPath := filepath.Join(artifactDir, "report.txt") err := os.WriteFile(reportPath, []byte("Test Report\n"), 0644) if err != nil { t.Fatalf("Failed to write: %v", err) } }
便利なのは、失敗時にファイルが残る点です。失敗したテストの出力を、探し回らずに開けます。
httptest の改善
外部ホストと通信する HTTPS のコードをテストするには、証明書の検証を切る(テストでも避けたいやり方です)か、面倒な証明書の設定をするしかありませんでした。Go 1.26 の httptest.Server.Client() は、example.com へのリクエストをテストサーバーに向け、TLS も処理してくれます。
example.gogofunc TestExternalAPIClient(t *testing.T) { server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status": "ok"}`)) })) defer server.Close() client := server.Client() resp, err := client.Get("https://example.com/api/status") // Request goes to test server, not real example.com }
パフォーマンス向上と Green Tea GC
ここで紹介する改善は、どれもコードの変更を必要としません。Go 1.26 で再ビルドすれば、プログラムが速くなります。
新しい Green Tea ガベージコレクタは、P99 レイテンシを 30〜40%、メモリのオーバーヘッドを 10〜15% 減らし、スループットを 5〜10% 上げます。短命なオブジェクトを大量に確保するプログラムほど効果が大きくなります。改善の理由は、GC 処理のスケジューリングが良くなったことと、アロケーションの追跡にかかるオーバーヘッドが減ったことです。
システムコールと cgo の呼び出しは約 30% 速くなりました。すべての syscall にオーバーヘッドを加えていた _Psyscall プロセッサ状態を、ランタイムが廃止したためです。小さなオブジェクト(1〜512 バイト)のアロケーションも約 30% 速くなっています。汎用的な処理経路ではなく、ジャンプテーブルを使う専用のアロケーションルーチンのおかげです。
標準ライブラリの一部の関数も速くなりました。fmt.Errorf は約 30%、io.ReadAll は 28% 高速です。本番環境でのテストでは、毎秒 10,000 リクエストを処理する HTTP API サーバーの P99 レイテンシが 45ms から 32ms に下がりました。
ランタイムの新機能
context に対応したネットワーク接続
net パッケージは、接続そのものに加えて DNS の名前解決にも context のデッドラインを適用するようになりました。
example.gogoctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var dialer net.Dialer conn, err := dialer.DialContext(ctx, "tcp", "example.com:443") // Timeout now covers DNS + connect time combined
機密データの保護
実験的な runtime/secret パッケージを使うと、機密データがメモリダンプに残りにくくなります。
example.gogosecretKey := secret.New([]byte("sk_live_secret")) defer secretKey.Destroy() apiKey := secretKey.Bytes() makeAuthenticatedRequest(apiKey)
Destroy() でメモリを消去すると、秘密情報がコアダンプやスワップファイルに出てしまう時間が短くなります。
SIMD 演算
新しい simd/archsimd パッケージで、amd64 のハードウェアのベクトル命令を使えます。
example.gogofunc vectorAdd(a, b [4]float32) [4]float32 { va := archsimd.Float32x4FromArray(a) vb := archsimd.Float32x4FromArray(b) return va.Add(vb).ToArray() }
SIMD は 1 つの命令で複数の値を処理するので、画像処理、音声や動画のコーデック、科学計算などの数値処理でスループットが 4〜16 倍になります。プラットフォーム固有の機能なので、他のアーキテクチャでも動かす必要があるならスカラー版のフォールバックを書いておきましょう。
既存コードの移行
Go 1.26 では go fix が改良され、よくある移行を自動で行えるようになりました。
example.bashbashgo fix -diff ./... # Preview changes go fix ./... # Apply all fixes
rsa.GenerateKey(rand.Reader, 2048) は rsa.GenerateKey(nil, 2048) に書き換えられ、errors.As のパターンも可能な箇所は errors.AsType に変換されます。差分を確認し、テストを実行してからコミットしてください。
書き換えの全一覧はgo fix 1.26 に含まれる全モダナイザーにまとめています。
コードへの影響
最初に触れることになるのは new(expr) と errors.AsType で、その移行の一部は go fix が肩代わりしてくれます。並行処理のコードには go test -goroutineleak をかけて、本番に出る前にリークを見つけましょう。暗号の鍵生成関数は、nil を渡せば安全な乱数源を選んでくれるようになりました。GC とランタイムの高速化に必要なのは、再ビルドだけです。
