Go 1.27.2 でビルドしたバイナリに GODEBUG=x509sha1=1 を渡すと、main に入る前に exit 2 で止まります。同じ環境変数でも、Go 1.26.4 でビルドしたバイナリは何事もなく起動する。GODEBUG の値はツールチェーンと go.mod と環境変数から決まり、Go 1.27 はそのうち削除済み設定の扱いを変えました。
GODEBUG の値はツールチェーンと go.mod と環境変数で決まる
環境変数にはカンマ区切りで複数並べる
公式ドキュメント “Go, Backwards Compatibility, and GODEBUG” の定義では、GODEBUG の1設定は key=value の組です。環境変数には GODEBUG=http2client=0,http2server=0 のようにカンマ区切りで複数書けます。この例なら、net/http の HTTP/2 がクライアントとサーバーの両方で既定オフになる。
同じページに “Unrecognized settings in the GODEBUG environment variable are ignored.” とあり、存在しない名前を書いてもエラーにはなりません。ただし削除済みの設定はこの「未知」に含まれず、Go 1.27 では起動が止まります(後述)。
互換用の設定とランタイムの診断用変数は別の表にある
gctrace=1 も同じ環境変数に書きますが、出自が違います。gctrace は runtime パッケージのデバッグ変数で、GC 1回ごとにログを1行出す(GOMEMLIMITとGOGCの違い—上限を詰めると10倍遅くなるのはなぜ? で読んだのがこの行)。一方の panicnil や httpmuxgo121 は、Go の更新で変わった挙動を元に戻すための互換用の設定です。一覧はツールチェーンの src/internal/godebugs/table.go にあります。
go.mod の godebug に書けるのは表にある名前だけで、gctrace はエラーになります。
$ cat go.mod
module example.com/misc
go 1.22
godebug gctrace=1
$ go list -f '{{.DefaultGODEBUG}}' .
go: error loading go.mod:
go.mod:5: unknown godebug "gctrace"
DNS をGo実装のリゾルバに固定する netdns=go は表に載っているので、godebug netdns=go と書けます。逆に、挙動が変わっても互換用の設定が付かない変更もある。Go 1.27 の Response.Body 自動ドレインがそうで、ツールチェーンのバージョンだけで決まり、GODEBUG では戻せません(Go 1.27のresp.Body自動ドレイン—io.Copyはまだ必要か)。
ビルド時の3段と実行時の環境変数
環境変数で指定しなかった設定について、”Default GODEBUG Values” セクションはこう説明しています。
its value is derived from three sources: the defaults for the Go toolchain used to build the program, amended to match the Go version listed in go.mod, and then overridden by explicit //go:debug lines in the program.
ツールチェーンの既定値を出発点に、go.mod の go 行に合わせて古い値へ寄せ、ソースの //go:debug で上書きする。ここまでがバイナリに埋め込まれ、実行時の GODEBUG がさらに上に乗ります。Go 1.23 で入った go.mod の godebug ブロックは、2段目と3段目の間で効きます。
go.mod の go 行を上げると既定値がまとめて変わる
go list -f ‘{{.DefaultGODEBUG}}’ で差分を見る
埋め込まれる値は go list -f '{{.DefaultGODEBUG}}' で確認でき、表示されるのはツールチェーン本来の既定値との差分だけです。以下の検証は macOS(darwin/arm64)上の go1.27.2 と go1.26.4 で行いました。
$ go version
go version go1.27.2 darwin/arm64
$ head -3 go.mod
module example.com/m
go 1.20
$ go list -f '{{.DefaultGODEBUG}}' . | tr ',' '\n' | head -5
containermaxprocs=0
cryptocustomrand=1
decoratemappings=0
gotestjsonbuildtext=1
httpcookiemaxnum=0
$ go list -f '{{.DefaultGODEBUG}}' . | tr ',' '\n' | wc -l
27
go 1.20 と書いただけのモジュールで、27個の設定が旧値になっていました。ツールチェーンは 1.20 より古い go 行を 1.20 として扱います。
go 行ごとの件数
go 行だけを変えた空のモジュールを、go1.27.2 で並べた結果です。
| go.mod の go 行 | 旧値になる設定の数 | 含まれる設定の例 |
|---|---|---|
| go 1.20 | 27 | panicnil=1, httpmuxgo121=1 |
| go 1.22 | 24 | x509negativeserial=1, winsymlink=0 |
| go 1.23 | 20 | tlsmlkem=0, urlmaxqueryparams=0 |
| go 1.25 | 6 | cryptocustomrand=1, httpservecontentmaxranges=1 |
| go 1.26 | 2 | tracebacklabels=0, x509sslcertoverrideplatform=0 |
| go 1.27 | 0 | なし |
go 1.23 以前に入る tlsmlkem=0 は、TLS のポスト量子鍵交換 X25519MLKEM768 を既定で切る設定。CurvePreferences を指定していないクライアントとサーバーは、ツールチェーンが新しくても古典的な鍵交換で動きます。tlsmlkem が効く範囲は Go 1.27のポスト量子署名ML-DSA—TLSハンドシェイクが2.75倍に で測りました。
依存モジュールの go 行と godebug は使われない
ツールチェーンが既定値の計算に使うのは、メインモジュールの go.mod(ワークスペースなら go.work)だけです。godebug についても “Only the work module’s go.mod is consulted for godebug directives. Any directives in required dependency modules are ignored.” とある。ライブラリが go 1.27 に上げていても、import するアプリが go 1.22 なら 1.22 の既定値で動きます。
godebug ブロックと //go:debug で既定値を固定する
default キーで既定値だけ新しくする
$ cat go.mod
module example.com/misc
go 1.22
godebug default=go1.27
$ go list -f '{{.DefaultGODEBUG}}' . | wc -c
0
wc -c が 0 なので、差分はありません。特殊キー default は、指定しなかった設定をどの Go バージョンの既定値で埋めるかを決めます。go 行は言語仕様のバージョンも兼ねているので、言語側は 1.22 に据え置いたまま GODEBUG だけ 1.27 にそろえられる。複数の設定は godebug ( ... ) のブロックに1行ずつ並べます。Go 1.22 以前のツールチェーンは godebug 行を一切受け付けず、go.mod の読み込みでエラーになります。
//go:debug は main パッケージとテストにだけ効く
main パッケージのファイル先頭、package 文より前に //go:debug panicnil=1 と書くと、そのバイナリだけ既定値を変えられます。*_test.go に書いた行は、go test がテスト用の main パッケージへの指定として扱う。では、それ以外のパッケージに書くとどうなるか。
$ head -2 lib/lib.go
//go:debug panicnil=1
package lib
$ go build ./... && echo "build ok"
build ok
$ go vet ./lib
lib/lib.go:1:1: //go:debug directive only valid in package main or test
ドキュメントの “In any other context, //go:debug lines are ignored by the toolchain” のとおり、ビルドは通って go vet だけが報告しました。ライブラリに書いた //go:debug は、そのライブラリを import するバイナリの既定値を変えない。
環境変数、//go:debug、go.mod の順に強い
panic(nil) を recover した値の型で、どの指定が勝ったかを見ます。panicnil=0(Go 1.21 以降の既定)なら *runtime.PanicNilError、panicnil=1 なら nil です。
package main
import "fmt"
func main() {
defer func() { fmt.Printf("recover()=%T\n", recover()) }()
panic(nil)
}
# A: go.mod に godebug panicnil=1
$ go run .
recover()=<nil>
$ go build -o app . && go version -m app | grep DefaultGODEBUG
build DefaultGODEBUG=panicnil=1
# B: A のまま main.go の先頭に //go:debug panicnil=0
$ go run .
recover()=*runtime.PanicNilError
# C: B のまま環境変数で上書き
$ GODEBUG=panicnil=1 go run .
recover()=<nil>
go.mod より //go:debug、それより実行時の環境変数が優先されました。go version -m で読める DefaultGODEBUG はバイナリ自体に残ります。本番のバイナリが何を埋め込んでいるか、ソースがなくても確かめられる。
セキュリティ修正の既定値も go 行で旧値に戻る
“Default GODEBUG Values” セクションには、例外を定めた一文があります。
As an exception, GODEBUGs introduced for security releases will have the new behavior apply to all versions.
セキュリティリリースで入った設定は、go 行に関係なく新しい挙動になる、と読めます。ところが件数の表には urlmaxqueryparams=0 と httpservecontentmaxranges=1 が出ていた。どちらも DoS 対策としてマイナーリリースへバックポートされた設定です。
go 1.23 以前では10,001個のクエリパラメータが通る
urlmaxqueryparams は net/url が受け付けるクエリパラメータ数の上限で、既定値は 10000 です。Go チームは Go 1.25.6 と Go 1.24.12 にもこの上限をバックポートしています。
q := strings.Repeat("a=1&", 10_000) + "a=1" // 10,001 個
v, err := url.ParseQuery(q)
fmt.Printf("params=%d err=%v\n", len(v["a"]), err)
$ for v in 1.23 1.24 1.27; do go mod edit -go=$v; echo "go $v"; go run .; done
go 1.23
params=10001 err=<nil>
go 1.24
params=0 err=number of URL query parameters exceeded limit
go 1.27
params=0 err=number of URL query parameters exceeded limit
table.go の定義は {Name: "urlmaxqueryparams", Package: "net/url", Changed: 24, Old: "0"} で、go 行が 1.24 未満ならツールチェーンは Old の "0"(上限なし)を使います。Cookie の個数を制限する httpcookiemaxnum も同じ Changed: 24, Old: "0" です。go 1.23 のまま運用しているサービスでは、最新のツールチェーンでビルドしてもこの2つの DoS 対策が効いていない。
go1.27.2 の Range 上限は go 1.25 以前で1になる
10月8日の go1.27.2 と go1.26.9 で、http.ServeContent や http.FileServer が受け付ける Range の範囲数に上限が付きました。CVE-2026-78667(issue #81858)の修正で、コミットメッセージには “The default is 200 (same as Apache).” とある。table.go の定義は {Name: "httpservecontentmaxranges", Package: "net/http", Changed: 26, Old: "1"} で、net/http/fs.go はこの値を上限の数としてそのまま使います。100バイトを ServeContent で返すサーバーに、範囲1個と2個のリクエストを送りました。
body := strings.Repeat("0123456789", 10) // 100 bytes
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, "data.txt", time.Time{}, strings.NewReader(body))
}))
defer srv.Close()
for _, rg := range []string{"bytes=0-9", "bytes=0-9,20-29"} {
req, _ := http.NewRequest("GET", srv.URL, nil)
req.Header.Set("Range", rg)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
b, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Printf("Range: %-16s -> %d, %3d bytes, %s\n", rg, resp.StatusCode, len(b), resp.Header.Get("Content-Type"))
}
$ go mod edit -go=1.25 && go run .
Range: bytes=0-9 -> 206, 10 bytes, text/plain; charset=utf-8
Range: bytes=0-9,20-29 -> 200, 100 bytes, text/plain; charset=utf-8
non-default events: 1
$ go mod edit -go=1.26 && go run .
Range: bytes=0-9 -> 206, 10 bytes, text/plain; charset=utf-8
Range: bytes=0-9,20-29 -> 206, 366 bytes, multipart/byteranges; boundary=1ab49671d036099a6037e7de55c5ddabf961a792e9ec2f54268921f081ae
non-default events: 0
go 1.26 では 206 の multipart/byteranges、go 1.25 では 200 で全体の100バイトが返りました。同じ go 1.25 でも、go1.26.4 でビルドすれば 206。パッチリリースを当てただけで、go 行が 1.25 以前のサーバーは複数範囲の Range を無視するようになる。GODEBUG=httpservecontentmaxranges=200 を付けると、go 1.25 でも 206 に戻りました。最終行の non-default events は後で触れる runtime/metrics のカウンタです。
table.go の Old フィールドには “value that restores behavior prior to Changed” というコメントが付いています。修正前の Range には上限がなく、fs.go で上限なしを表すのは "0" です。同じ DoS 対策の urlmaxqueryparams と httpcookiemaxnum は、Old を "0" にしている。なぜ "1" なのか、コミットメッセージに理由は書かれていません。2026年10月10日時点では、master・release-branch.go1.27・release-branch.go1.26 の table.go がどれも同じ定義だった。go 行を据え置くなら、前の節の godebug default=go1.27 でどちらの設定も新しい既定値になります。
Go 1.27 で削除された設定の旧値はビルドと起動で止まる
Go 1.27 は互換用の設定をまとめて削除しました。下の表は “GODEBUG History” の “Go 1.27” セクションに並ぶものに、Go 1.24 で消えた2つを加えたもの。止まる値は table.go の Removed から取りました。
| 設定 | 止まる値 | 削除 |
|---|---|---|
| asynctimerchan | 1 / 2 | Go 1.27 |
| gotypesalias | 0 | Go 1.27 |
| tls10server / tls3des / tlsrsakex / tlsunsafeekm | 1 | Go 1.27 |
| x509keypairleaf | 0 | Go 1.27 |
| x509sha1 | 1 | Go 1.24 |
| tlskyber | 0 | Go 1.24 |
go.mod と //go:debug に書いた旧値はビルドエラー
$ cat go.mod
module example.com/removed
go 1.22
godebug asynctimerchan=1
$ go build -o /dev/null .
go: error loading go.mod:
go.mod:5: removed GODEBUG "asynctimerchan" set to old value "1" (https://go.dev/doc/godebug#go-127)
//go:debug asynctimerchan=1 も invalid //go:debug: removed GODEBUG "asynctimerchan" set to old value "1" でコンパイルが止まります。最終的な既定値の asynctimerchan=0 なら通る。リリースノートの “GODEBUG” 項目にある “It accepts these settings if they are set to the final default value established before the setting was removed.” のとおりです。
緩くなった面もあります。Go 1.24 で消えた x509sha1 を godebug x509sha1=0 と書いた go.mod を、go1.26.4 は拒否し、go1.27.2 は通しました。
$ go1.26.4 build -o /dev/null .
go: error loading go.mod:
go.mod:5: use of removed godebug "x509sha1", see https://go.dev/doc/godebug#go-124
$ go build -o /dev/null . && echo ok
ok
環境変数に残った旧値は起動時に fatal error
ビルドで止まるなら CI で気づけます。Dockerfile の ENV や Kubernetes マニフェストの env に残った GODEBUG は、ビルドでは検出されません。go.mod の go 行に関係なく、Go 1.27 でビルドしたバイナリは起動時に落ちる。同じソースを go1.26.4 と go1.27.2 でビルドして比べました。
$ go version app126 app127
app126: go1.26.4
app127: go1.27.2
$ GODEBUG=x509sha1=1 ./app126; echo "exit=$?"
started
exit=0
$ GODEBUG=x509sha1=1 ./app127; echo "exit=$?"
fatal error: removed GODEBUG "x509sha1" set to old value "1" in environment (https://go.dev/doc/godebug#go-124)
goroutine 1 [running, locked to thread]:
runtime.fatal({0x11c1690f4000, 0x62})
/Users/moha/sdk/go1.27.2/src/runtime/panic.go:1267 +0x58
runtime.main()
/Users/moha/sdk/go1.27.2/src/runtime/proc.go:224 +0x214
runtime.goexit({})
/Users/moha/sdk/go1.27.2/src/runtime/asm_arm64.s:1039 +0x4
exit=2
x509sha1 が消えたのは Go 1.24 です。Go 1.24〜1.26 のバイナリはこの値を無視していたので、SHA-1 証明書のために昔入れた環境変数が残っていても起動はしていた。ツールチェーンを 1.27 に上げた時点で、そのコンテナは起動しなくなります。ほかの組み合わせの結果は以下のとおりです。
tls10server=1、asynctimerchan=1→ app126 は起動、app127 は exit 2http2client=0,asynctimerchan=1→ 正しい設定と並んでいても app127 は exit 2asynctimerchan=0(最終値)、nosuch=1(存在しない名前)→ 両方とも起動
この判定は runtime/runtime1.go の起動処理にあります。コミット f5eb66f のメッセージにも “Detect uses of a removed GODEBUG with old value in the environment and terminate with a fatal error at startup.” とある。リリースノートの “GODEBUG” 項目が書いているのは go コマンドが失敗する話だけで、実行時に止まる点は GODEBUG のドキュメントにも載っていません。起動後に os.Setenv で入れた旧値は検出しない、ともコミットメッセージにあります。
ツールチェーンを上げる前にデプロイ定義を検索すれば防げます。手元のサンプルで試した結果です。
$ grep -rnE '(asynctimerchan=[12]|gotypesalias=0|tls10server=1|tlsrsakex=1|tlsunsafeekm=1|tls3des=1|x509keypairleaf=0|x509sha1=1|tlskyber=0)' deploy/
deploy/Dockerfile:3:ENV GODEBUG=http2client=0,x509sha1=1
deploy/k8s/api.yaml:10: value: "asynctimerchan=1"
Kubernetes では name: GODEBUG と value: が別の行に分かれるため、GODEBUG= で探すと漏れます。設定名と値の組で探すほうが確実です。
go 1.22 のままでもタイマーの挙動は変わる
go.mod に何も書いていなくても、削除の影響は受けます。go 1.22 のモジュールなら、go1.26.4 は既定値に asynctimerchan=1 を入れ、time.Timer のチャネルをバッファ1の旧挙動で動かしていた。go1.27.2 にはこの設定自体がありません。
t := time.NewTimer(10 * time.Millisecond)
time.Sleep(30 * time.Millisecond) // 発火済み、まだ受信していない
fmt.Println("cap(t.C):", cap(t.C))
fmt.Println("Stop():", t.Stop())
t.Reset(100 * time.Millisecond)
start := time.Now()
<-t.C
fmt.Println("received after:", time.Since(start).Round(time.Millisecond))
$ cat go.mod
module example.com/timer
go 1.22
$ go1.26.4 run .
cap(t.C): 1
Stop(): false
received after: 0s
$ go run . # go1.27.2
cap(t.C): 0
Stop(): true
received after: 102ms
旧挙動では、発火済みで受信されていない値がバッファに残り、Reset 後の受信がその古い値を0秒で受け取っていました。go1.27.2 の Timer.Stop のドキュメントには “any receive from t.C after Stop has returned is guaranteed to block rather than receive a stale time value from before the Stop” とあり、102ms 待ってから届いた結果と合う。古い値を捨てる if !t.Stop() { <-t.C } はどうなるか。この場面では Stop が true を返すので読み出しは走らず、そのまま動きます。困るのは「Reset 直後に古い値が届く」前提で書いたコードで、go.mod を触らなくても挙動が変わります。
非デフォルト挙動のカウンタが0のままになる条件
/godebug/non-default-behavior/<name>:events を読む
多くの設定には、runtime/metrics のカウンタ /godebug/non-default-behavior/<name>:events が付いています。既定値以外の値で挙動が変わった回数で、Range の検証で出していた non-default events: 1 がこれ。本番でこの値が増えていれば、その旧値にまだ頼っているコードが動いている。asynctimerchan のように “There are no runtime metrics for this change.” と書かれた設定もあります。
internal/godebug をリンクしないプログラムでは数えない
fmt と runtime/metrics だけを import した CLI で、panicnil=1 の panic(nil) を起こしてもカウンタは 0 のままでした。_ "net/url" を1行足すと 1 になります。runtime 側のコメントが “Calls before internal/godebug registers itself are dropped on the floor.” と説明しているとおりで、0 だからといって旧挙動が使われていないとは限らない。依存に internal/godebug が入っているかは go list -deps . | grep internal/godebug で分かります。
まとめ
- GODEBUG の既定値は、ツールチェーン、メインモジュールの go 行、
godebugブロック、//go:debugの順で決まり、実行時の環境変数が最後に上書きする - go1.27.2 では
go 1.20のモジュールで27個が旧値になる。urlmaxqueryparamsなどセキュリティ由来の設定も例外ではなく、httpservecontentmaxrangesは go 1.25 以前で上限1になる - 削除済み設定の旧値は、go.mod と
//go:debugならビルドエラー、環境変数なら起動時に exit 2。Go 1.24 で消えたx509sha1=1も対象 - go 行を上げられないときは
godebug default=go1.27で既定値だけ新しくできる