📝

Go 1.25で始める“本番に強い”開発

に公開

はじめに

  • 2025年8月にリリースされたGo 1.25は、診断、テスト、パフォーマンスを大きく底上げするアップデートが満載です
    • 特に、Flight Recorder(異常直前を“巻き戻して”取得する実行トレース)、testing/synctestの正式化(非同期テストが以前より書きやすくなった)、encoding/json/v2の実験提供(環境変数GOEXPERIMENT=jsonv2設定必須)は要チェックです
  • この記事では最小構成の動くサンプル付きでこれらの要点を解説します
  • すべてを網羅しているわけではありません。詳細なリリースノートは公式のGo 1.25 Release Notesを参照してください

1. Go 1.25 ハイライト

  • Flight Recorderが標準搭載されました。
    • 直近の実行トレースをメモリに保持し、異常検知時に直前数秒をスナップショットとして切り出し可能です。ロングランサービスでの障害発生の原因追跡が楽になります
    • Flight Recorder in Go 1.25 参照
  • testing/synctestがGA(正式化)されました
  • encoding/json/v2が実験提供されました。
    • GOEXPERIMENT=jsonv2を指定すると新実装が試験運用可能になります
    • ただし、将来のAPI進化を見据えつつ、互換性検証の呼びかけがあるのでご注意ください 
  • コンテナ対応のGOMAXPROCS、DWARF v5など、ツールチェーンやランタイムの改善も多数あります 

2. Flight Recorderの設計・導入・運用

2.1 何が嬉しい?

  • 本番で「遅延が散発的に発生する」、「再現が難しい」といった問題に強い
  • 常時トレースでなく、必要な瞬間だけ直前の履歴を切り出せます
    • ストレージ逼迫とオーバーヘッドを低減できます
  • go tool traceで、原因の少し手前からゴルーチン/スケジューラの挙動を可視化できます

設計の勘どころ

  • MinAge
    • 遅延/ハングといった観察したい現象の想定時間の約2倍を目安にすること
  • MaxBytes
    • 上限ヒント(厳密制限ではない)のこと
    • 高トラフィックでは生成レートが大きくなる前提で余裕を持つようにする
  • スナップショット頻度制御
    • レート制限/クールダウン/一度きり取得のガード(sync.Once)を導入すること 

2.3 mainだけで動く最小例

  • サンプルとしてHTTP遅延で自動スナップショットを取ってみましょう
  • ソースコードはGitHubで公開しています
package main

import (
  "fmt"
  "log"
  "net/http"
  "os"
  "os/signal"
  "runtime/trace"
  "sync"
  "syscall"
  "time"
)

// より実用的なスナップショット管理
type SnapshotManager struct {
  cooldown time.Duration
  lastSnap time.Time
  mu       sync.Mutex
  fr       *trace.FlightRecorder
}

func NewSnapshotManager(fr *trace.FlightRecorder, cooldown time.Duration) *SnapshotManager {
  return &SnapshotManager{
    cooldown: cooldown,
    fr:       fr,
  }
}

func (sm *SnapshotManager) TrySnapshot() bool {
  sm.mu.Lock()
  defer sm.mu.Unlock()

  if time.Since(sm.lastSnap) < sm.cooldown {
    return false // クールダウン中
  }

  f, err := os.CreateTemp("", "snap-*.ctrace")
  if err != nil {
    log.Printf("temp create: %v", err)
    return false
  }
  defer func() {
    if err := f.Close(); err != nil {
      log.Printf("file close: %v", err)
    }
  }()

  if _, err := sm.fr.WriteTo(f); err != nil {
    log.Printf("trace write: %v", err)
    return false
  }

  sm.lastSnap = time.Now()
  log.Printf("trace snapshot: %s", f.Name())
  return true
}

func main() {
  // 直前5s保持、最大16MiB目安(ヒント)
  fr := trace.NewFlightRecorder(trace.FlightRecorderConfig{
    MinAge:   5 * time.Second,
    MaxBytes: 16 << 20,
  })
  if err := fr.Start(); err != nil {
    log.Fatalf("flight recorder start: %v", err)
  }
  defer fr.Stop()

  // クールダウン期間を10秒に設定(連続スナップショットを防ぐ)
  sm := NewSnapshotManager(fr, 10*time.Second)

  // 手動トリガ(SIGUSR1)、終了時(SIGTERM)でスナップショット
  // SIGINT(Ctrl-C)は通常終了として扱う
  sigc := make(chan os.Signal, 1)
  signal.Notify(sigc, syscall.SIGUSR1, syscall.SIGTERM)
  go func() {
    for s := range sigc {
      log.Printf("signal %v -> attempting snapshot", s)
      if sm.TrySnapshot() {
        log.Printf("snapshot completed")
      } else {
        log.Printf("snapshot skipped (cooldown)")
      }

      // SIGTERMの場合は終了
      if s == syscall.SIGTERM {
        log.Println("received SIGTERM, shutting down...")
        os.Exit(0)
      }
    }
  }()

  mux := http.NewServeMux()
  mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    start := time.Now()
    // 疑似遅延
    time.Sleep(200 * time.Millisecond)
    if _, err := fmt.Fprintln(w, "hello"); err != nil {
      log.Printf("response write: %v", err)
    }
    if d := time.Since(start); d > 100*time.Millisecond {
      go func() {
        if sm.TrySnapshot() {
          log.Printf("auto snapshot triggered by slow request (%v)", d)
        }
      }()
    }
  })

  log.Println("listen :8080")
  if err := http.ListenAndServe(":8080", mux); err != nil {
    log.Fatal(err)
  }
}
  • 起動後にcurl localhost:8080/を叩くと、条件次第でsnap-*.ctraceが生成されます
  • go tool trace snap-XXXX.ctraceでブラウザが開き、G/Proc/Net/Flowの因果が追えて可視化できます 

2.4 運用レシピ例

  • SLO違反の検知 → Webhook → SIGUSR1 で自動スナップショット&回収する
  • トレースに機微が載る可能性があるので、保存先のアクセス制御と保存期間を明文化しましょう
  • 重さ検知をしてからFlight Recorderで切り出すといった、既存のpprof・OTelとの役割を分担しましょう
  • Testing concurrent code with testing/synctestを参照してください

3. testing/synctestでの非同期テストの新しい常識

3.1 何が変わる?

  • 仮想時間の“バブル”内でテストが動作します
    • time.Sleepなしで即時に時間を進められます
  • フレーク/遅いテストに効きます
    • CI環境の“うっかり数秒”にも強い
  • Go 1.24で実験提供され、1.25で正式化し標準提供されました 

3.2 最小テスト例

  • 期限超過を確実に検証するサンプルです
  • synctestはテスト用パッケージのため、ここは_test.goでの最小例を示します
  • ソースコードはGitHubで公開しています
package main

import (
  "context"
  "testing"
  "testing/synctest"
  "time"
)

func TestDeadlineExceeded(t *testing.T) {
  synctest.Test(t, func(t *testing.T) {
    ctx, cancel := context.WithDeadline(t.Context(), time.Now().Add(100*time.Millisecond))
    defer cancel()

    err := doWithDeadline(ctx, 200*time.Millisecond)
    synctest.Wait() // バブル内の待ちを収束させる

    if err != context.DeadlineExceeded {
      t.Fatalf("want DeadlineExceeded, got %v", err)
    }
  })
}

4. encoding/json/v2での実験実装

4.1 encoding/json/v2の重要な互換性問題

  • GOEXPERIMENT=jsonv2を設定すると、新しい実装でのencoding/jsonが動作し、encoding/json/v2も利用可能になります
  • ⚠️ 重要: omitemptyタグの挙動が変更され、本番環境で予期しない問題が発生する可能性があります!

4.1.1 何が問題になるのか?

  • 従来(v1)の挙動
    • time.Timeのゼロ値はomitemptyでも出力される("0001-01-01T00:00:00Z"
    • 空の構造体も出力される
  • v2での変更
    • time.Timeのゼロ値がomitemptyで省略される可能性があります
    • 新しいomitzeroタグでより厳密なゼロ値制御が可能になりました

4.1.2 実際の問題シナリオ

  1. APIレスポンス互換性破綻

    • クライアントがstart_timeフィールドの存在を前提とした処理をしている
    • v2移行後、フィールドが突然消失してエラーになってしまう
  2. データベース同期問題

    • NULL vs ゼロ値の判定が困難になりえます
    • 既存のORMやマッピングロジックが破綻しかねません
  3. JSONスキーマ検証失敗

    • 既存のスキーマ定義と不整合が発生する場合がありえます
    • バリデーションライブラリでエラーになってしまう可能性があります

4.1.3 対策と移行戦略

4.1.3.1. 安全な構造体設計**
// 危険: time.Timeの直接使用
type Event struct {
    Name      string    `json:"name"`
    StartTime time.Time `json:"start_time,omitempty"` // v2で省略される可能性
}

// 安全: ポインタ使用でnilチェック可能
type SafeEvent struct {
    Name      string     `json:"name"`
    StartTime *time.Time `json:"start_time,omitempty"` // nilで明示的制御
    EndTime   *time.Time `json:"end_time,omitzero"`    // v2の新機能
}
4.1.3.2. 段階的検証
# 現在の挙動確認
go run .

# v2での挙動確認
GOEXPERIMENT=jsonv2 go run .

# 差分検証をCIに組み込み
4.1.3.3. 互換性テスト
  • 既存のAPIエンドポイントでv1/v2の出力をかならず比較してください
  • クライアントアプリケーションでの影響範囲を必ず調査してください
  • データベース連携部分での動作確認が必須です

4.2 mainだけで動く最小例

  • v1とv2の挙動差を実際に確認できるサンプルです
  • ソースコードはGitHubで公開しています
  • v1とv2の比較用シェルスクリプトもGitHubで公開しています
package main

import (
  "encoding/json"
  "fmt"
  "os"
  "time"
)

type User struct {
  ID   int    `json:"id"`
  Name string `json:"name"`
}

// omitemptyの挙動変更を示すための構造体
type Event struct {
  Name        string    `json:"name"`
  Description string    `json:"description,omitempty"`
  StartTime   time.Time `json:"start_time,omitempty"`
  EndTime     time.Time `json:"end_time,omitzero"` // jsonv2の新機能
  Count       int       `json:"count,omitempty"`
  Tags        []string  `json:"tags,omitempty"`
  Metadata    *Metadata `json:"metadata,omitempty"`
}

type Metadata struct {
  Version string `json:"version,omitempty"`
  Author  string `json:"author,omitempty"`
}

func main() {
  fmt.Println("=== 基本的なJSON操作 ===")
  in := []byte(`{"id":1,"name":"gopher"}`)
  var u User
  if err := json.Unmarshal(in, &u); err != nil {
    panic(err)
  }
  out, _ := json.Marshal(u)
  fmt.Println("User:", string(out))

  fmt.Println("\n=== omitempty vs omitzero の挙動比較 ===")

  // ケース1: 完全にゼロ値の構造体
  event1 := Event{Name: "Empty Event"}
  data1, _ := json.Marshal(event1)
  fmt.Println("ゼロ値フィールド含む:", string(data1))

  // ケース2: 一部フィールドに値がある構造体
  event2 := Event{
    Name:        "Partial Event",
    Description: "説明あり",
    Count:       0,          // ゼロ値だがomitemptyで省略される
    Tags:        []string{}, // 空スライスでomitemptyで省略される
  }
  data2, _ := json.Marshal(event2)
  fmt.Println("一部値あり:", string(data2))

  // ケース3: time.Timeのゼロ値問題を示す
  event3 := Event{
    Name:      "Time Zero Value Event",
    StartTime: time.Time{}, // ゼロ値 - v1では省略されない、v2では省略される可能性
    EndTime:   time.Time{}, // ゼロ値 - omitzeroで確実に省略
  }
  data3, _ := json.Marshal(event3)
  fmt.Println("time.Timeゼロ値:", string(data3))

  // ケース4: 実際の時刻値
  now := time.Now()
  event4 := Event{
    Name:      "Actual Time Event",
    StartTime: now,
    EndTime:   now.Add(time.Hour),
    Count:     5,
    Tags:      []string{"important", "scheduled"},
    Metadata:  &Metadata{Version: "1.0", Author: "gopher"},
  }
  data4, _ := json.Marshal(event4)
  fmt.Println("実際の値:", string(data4))

  fmt.Println("\n=== 互換性問題の例 ===")
  fmt.Println("【重要】time.Timeのゼロ値の扱い:")
  fmt.Println("- v1: omitemptyでもゼロ値が出力される (0001-01-01T00:00:00Z)")
  fmt.Println("- v2: omitemptyでゼロ値が省略される可能性")
  fmt.Println("- 対策: omitzeroタグを明示的に使用してゼロ値制御")

  fmt.Println("\n【実際の問題シナリオ】")
  fmt.Println("1. APIレスポンスでtime.Timeフィールドが突然消える")
  fmt.Println("2. クライアントアプリがフィールド存在を前提とした処理でエラー")
  fmt.Println("3. データベース同期でNULL vs ゼロ値の判定が困難")
  fmt.Println("4. 既存のJSONスキーマ検証が失敗する可能性")

  fmt.Println("\n構造体ポインタのnil vs ゼロ値:")
  eventWithNil := Event{
    Name:     "Nil Metadata Event",
    Metadata: nil, // nilポインタ - 常に省略
  }
  dataWithNil, _ := json.Marshal(eventWithNil)
  fmt.Println("nilポインタ:", string(dataWithNil))

  eventWithEmpty := Event{
    Name:     "Empty Metadata Event",
    Metadata: &Metadata{}, // 空の構造体 - v2では省略される可能性
  }
  dataWithEmpty, _ := json.Marshal(eventWithEmpty)
  fmt.Println("空構造体:", string(dataWithEmpty))

  fmt.Println("\n=== 移行対策コード例 ===")

  // 安全なtime.Time処理の例
  type SafeEvent struct {
    Name      string     `json:"name"`
    StartTime *time.Time `json:"start_time,omitempty"` // ポインタ使用でnilチェック可能
    EndTime   *time.Time `json:"end_time,omitzero"`    // omitzeroで明示的制御
  }

  safeEvent := SafeEvent{Name: "Safe Event"}
  safeData, _ := json.Marshal(safeEvent)
  fmt.Println("安全な実装:", string(safeData))

  now2 := time.Now()
  safeEventWithTime := SafeEvent{
    Name:      "Safe Event With Time",
    StartTime: &now2,
    EndTime:   &now2,
  }
  safeDataWithTime, _ := json.Marshal(safeEventWithTime)
  fmt.Println("時刻あり:", string(safeDataWithTime))

  // 実験ON: GOEXPERIMENT=jsonv2 でビルド/実行
  // 例) Linux: GOEXPERIMENT=jsonv2 go run .
  // 参考: v2はAPI/実装が進化中(将来変更の可能性あり)
  fmt.Println("\n実験フラグ: GOEXPERIMENT=jsonv2 go run .")
  _, _ = os.Stdout.Write([]byte("ok\n"))
}

5 コンテナ対応のGOMAXPROCS

  • cgroupのCPU制限を検知して、より適切なGOMAXPROCSを選ぶ挙動が改善されました。コンテナでのパフォーマンス安定化に寄与します。 

6 DWARF v5

  • デバッグ情報のv5化でリンク時間が短縮し、サイズも削減されます
  • 必要に応じて実験フラグでオフにする回避策も案内されています
    • 環境依存の話題はリリースノートなどを参照してください

7. 実験的ガベージコレクタ(greenteagc)

  • GOEXPERIMENT=greenteagcを指定すると、新しいGCが有効化されます
    • 10〜40%のGC性能向上が期待できます
    • 本番導入前にベンチマークで効果と安定性を必ず検証しましょう

8. セキュリティ機能の強化

  • net/http.CrossOriginProtectionが追加されました
    • CSRF対策などクロスオリジン保護が簡単に実装できます
    • 具体的な実装例やセキュリティベストプラクティスも公式ドキュメントを参照してください

9. 開発体験の向上

  • sync.WaitGroup.Go()が追加され、並行処理の記述がより簡潔になりました
  • go doc -httpでローカルAPIドキュメントサーバを起動でき、開発効率が向上します
  • 新しいテスト機能も追加されており、より柔軟なテストが可能です

10. まとめと導入チェックリスト

  • Flight Recorder
    • 異常直前の因果関係を後追いする新機能です
    • MinAge/MaxBytesと頻度制御の設計が肝になります 
  • `testing/synctest
    • 時間依存テストが安定します
    • synctest.Testsynctest.Waitを覚えましょう 
  • JSON v2
    • ⚠️ 重要: omitemptyの挙動変更で互換性問題が発生する可能性
    • time.Timeのゼロ値処理に特に注意
    • GOEXPERIMENT=jsonv2で早期検証して影響範囲を把握しましょう

チェックリスト

  • Flight RecorderMinAge/MaxBytes設計(観察窓×2、余裕あるヒント)
  • スナップショット頻度ガード(レート制限/クールダウン/sync.Once)
  • 取得ファイルの回収・アクセス制御・保存期間
  • 主要な非同期テストのsynctestでの置き換え
  • JSON v2の互換性検証(必須
    • 既存APIでv1/v2の出力差分を確認
    • time.Timeフィールドを持つ構造体の動作検証
    • クライアントアプリケーションでの影響調査
    • CIでGOEXPERIMENT=jsonv2による回帰テスト実施

Discussion