📝
Go 1.25で始める“本番に強い”開発
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
(正式化)されました- 時間・並行性に依存するテストが仮想時間で安全・高速・安定化しました
- Testing Time (and other asynchronicities) 参照
-
encoding/json/v2
が実験提供されました。-
GOEXPERIMENT=jsonv2
を指定すると新実装が試験運用可能になります - ただし、将来のAPI進化を見据えつつ、互換性検証の呼びかけがあるのでご注意ください 
-
- コンテナ対応のGOMAXPROCS、DWARF v5など、ツールチェーンやランタイムの改善も多数あります 
Flight Recorder
の設計・導入・運用
2. 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を参照してください
testing/synctest
での非同期テストの新しい常識
3. 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)
}
})
}
- 実時間で100ms/200msを待たないため、速く安定します
- Testing Time (and other asynchronicities)参照
- 使い方や落とし穴は公式解説が詳しいです
encoding/json/v2
での実験実装
4.
encoding/json/v2
の重要な互換性問題
4.1 -
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 実際の問題シナリオ
-
APIレスポンス互換性破綻
- クライアントが
start_time
フィールドの存在を前提とした処理をしている - v2移行後、フィールドが突然消失してエラーになってしまう
- クライアントが
-
データベース同期問題
- NULL vs ゼロ値の判定が困難になりえます
- 既存のORMやマッピングロジックが破綻しかねません
-
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の出力をかならず比較してください
- クライアントアプリケーションでの影響範囲を必ず調査してください
- データベース連携部分での動作確認が必須です
main
だけで動く最小例
4.2
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"))
}
GOMAXPROCS
5 コンテナ対応の- cgroupのCPU制限を検知して、より適切な
GOMAXPROCS
を選ぶ挙動が改善されました。コンテナでのパフォーマンス安定化に寄与します。 
DWARF v5
6 - デバッグ情報の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.Test
やsynctest.Wait
を覚えましょう 
-
JSON v2
-
⚠️ 重要:
omitempty
の挙動変更で互換性問題が発生する可能性 -
time.Time
のゼロ値処理に特に注意 -
GOEXPERIMENT=jsonv2
で早期検証して影響範囲を把握しましょう
-
⚠️ 重要:
チェックリスト
-
Flight Recorder
のMinAge/MaxBytes
設計(観察窓×2、余裕あるヒント) - スナップショット頻度ガード(レート制限/クールダウン/sync.Once)
- 取得ファイルの回収・アクセス制御・保存期間
- 主要な非同期テストの
synctest
での置き換え -
JSON v2
の互換性検証(必須)- 既存APIでv1/v2の出力差分を確認
-
time.Time
フィールドを持つ構造体の動作検証 - クライアントアプリケーションでの影響調査
- CIで
GOEXPERIMENT=jsonv2
による回帰テスト実施
Discussion