ブログに戻る

Go 1.26 の新機能:完全ガイド

errors.AsType によるエラー処理の強化、シンプルになった暗号 API、goroutine リークの検出、Green Tea GC によるパフォーマンス向上など、Go 1.26 の新機能を網羅的に解説します。実行できるコード例も掲載しています。

Go 1.26 の新機能:完全ガイド

Go 1.26 の変更の多くは、毎日書くコードにそのまま関わってきます。ポインタやエラー型まわりのボイラープレートが減り、goroutine リークを見つける新しい手段が加わり、再ビルドするだけでランタイムが速くなります。

目次


new(expr) でポインタをすっきり初期化する

オプションのフィールドをポインタで表す Go の API を使ったことがあれば、次のようなコードを書いたことがあるはずです。

example.gogo
port := 8080
config := &Config{Port: &port}  // Can't use &8080 directly!

Go ではリテラル値のアドレスを取ることができないため、先に一時変数を用意する必要があります。intPtr(n int) *int のようなヘルパーを足すコードベースもありますが、問題が別の場所に移るだけです。

Go 1.26 は組み込み関数 new() を拡張して、これを解消しました。以前は型しか受け取れず、new(int) はゼロを指す *int を返すだけでした。今は任意の式を渡せます。

example.gogo
package 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.gogo
var 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.gogo
package 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.gogo
package 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.gogo
key, err := rsa.GenerateKey(rand.Reader, 2048)

rand.Reader 引数は歴史的な名残です。初期の Go では、テスト用に独自の乱数源を渡せました。実際には常に crypto/rand.Reader を渡すべきで、math/rand など別のものを渡すとセキュリティホールになります。

Go 1.26 では、安全な選択肢がデフォルトになりました。nil を渡すと、関数は内部で crypto/rand.Reader を使います。

example.gogo
package 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.gogo
suite := 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.gogo
func 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
ctx, 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.gogo
package 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.gogo
func 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.gogo
func 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.gogo
ctx, 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.gogo
secretKey := secret.New([]byte("sk_live_secret"))
defer secretKey.Destroy()

apiKey := secretKey.Bytes()
makeAuthenticatedRequest(apiKey)

Destroy() でメモリを消去すると、秘密情報がコアダンプやスワップファイルに出てしまう時間が短くなります。

SIMD 演算

新しい simd/archsimd パッケージで、amd64 のハードウェアのベクトル命令を使えます。

example.gogo
func 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.bashbash
go 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 とランタイムの高速化に必要なのは、再ビルドだけです。

シニアエンジニアのように Go を書く

ブラウザで学べるインタラクティブなレッスン。最初のレッスンは無料です。

無料レッスンを試すまたは無料アカウントを作成