Go 1.27の変更と設計背景
generic methodがinterfaceを実装しない理由、JSON v2のAPIと既定動作、GCによるリーク検出を、図とコードで説明します。
Go 1.27.0は2026年8月19日に公開されました。 2026年10月24日までに公開された1.27.3までを対象に、言語、JSON、runtimeの変更を解説します。 1.27.0ではgeneric method、JSON v2、goroutineリークプロファイル、割り当ての特殊化、SIMDなどが加わっています。
続く1.27.1は9月1日公開で、cgo、compiler、runtime、go fixの不具合を修正しています。 ライブラリ側の修正対象は、database/sql、debug/elf、encoding/json、net/http、os、simd、simd/archsimdです。 1.27.2は10月8日公開で、goコマンド、TLS、テンプレート、HTTP、MIMEヘッダー、osのセキュリティ修正を含みます。 compiler、linker、runtime、各種ツールとライブラリの不具合修正も入っています。 1.27.3は10月19日公開で、crypto/x509の証明書チェーン検証を修正しています(Go公式リリース履歴)。
generic methodの追加とinterfaceへの制約
Go 1.27では、methodがreceiverのtype parameterとは別のtype parameterを宣言できます。 次のプログラムでは、non-generic typeのRegistryに、intまたはstringを受け取るmethodを定義しています(accepted proposal #77273)。
package main
import "fmt"
type Registry struct {
prefix string
}
func (r Registry) Label[T ~int | ~string](v T) string {
return fmt.Sprintf("%s:%v", r.prefix, v)
}
func main() {
r := Registry{prefix: "item"}
fmt.Println(r.Label(42))
fmt.Println(r.Label("ready"))
}
Go 1.27.3のdarwin/arm64で実行すると、type argumentは呼び出しごとに推論され、次の出力になります。
item:42
item:ready
r.Label(42)では、receiverがRegistry、type argumentがintと、呼び出し箇所で決まります。 しかし、generic methodをinterface経由で呼べると仮定すると、receiverの具体型が実行時まで確定しない場合があります。 具体型とtype argumentが別packageに分かれると、直接呼び出しだけでは必要なinstantiationを集められません(Type Parameters Proposal)。
2021年のproposalで問題になった4つのpackage
2021年のType Parameters Proposalで、著者のIan Lance TaylorとRobert Griesemerは、4つのpackageを使った例を示しています。 掲載例は、proposalの例の名前と戻り値を変え、必要な部分を抜き出しています。 contracts.Echoerの宣言は、当時検討していた「type parameterを持つinterface method」の仮想構文です。 Go 1.27でも、contracts.Echoerのinterface宣言は使えません(Type Parameters Proposal)。

// provider/provider.go
package provider
type Holder struct{}
func (Holder) Echo[T any](value T) T { return value }
// contracts/contracts.go
package contracts
type Echoer interface {
Echo[T any](T) T
}
// consumer/consumer.go
package consumer
import "example.test/demo/contracts"
func Extract(value any) int {
if receiver, ok := value.(contracts.Echoer); ok {
return receiver.Echo[int](7)
}
return 0
}
// entry/entry.go
package entry
import (
"example.test/demo/consumer"
"example.test/demo/provider"
)
var Result = consumer.Extract(provider.Holder{})
entryが渡す値はprovider.Holder{}なので、consumer.Extract内のEcho[int](7)はHolder.Echo[int]の呼び出しになります。 Holderとintの組み合わせが必要ですが、consumerのコンパイル時に見えるのはintだけで、引数の実体がHolderだとはわかりません。 methodの本体を持つproviderにもEcho[int]の呼び出しはなく、定義だけから必要なtype argumentを選び出すことはできません。 両packageをimportするentryもEcho[int]を直接呼ばないため、packageごとの直接呼び出しだけでは必要なinstantiationを集められません。
必要なHolder.Echo[int]を生成するには、package内の直接呼び出し以外から、型の組み合わせを集める仕組みが必要です。 TaylorとGriesemerは、リンク時に組み合わせを調べる方式と、実行時に処理する2つの方式を検討しています。
リンク時にinstantiationするなら、プログラム全体の呼び出し関係から、型とtype argumentの組み合わせを探します。 しかし、実行時の文字列を使ってmethodをreflectionで検索する場合、静的な呼び出し関係だけでは候補を限定できません。
実行時にinstantiationする案は、JITなどのコード生成機構を追加し、必要な組み合わせの実装を生成します。 TaylorとGriesemerは、実装が複雑になり、実行時にも遅くなると指摘しています。 reflectionによる汎用実装も、具体型ごとの事前生成を避ける代わりに、実行時に型を調べる処理が必要です。 著者らは、汎用実装にも複雑さと実行速度の問題があると説明しています(Type Parameters Proposalの実装案)。
採用されたのは、具体型を静的に解決できる呼び出し
Go 1.27で使えるgeneric methodは、具体型に対して呼び出すもので、interface methodを実装しません。 具体型のmethodであれば、receiver付きのgeneric functionとして実装できます。 Robert Griesemerは、interface実装とは別に、型の名前空間とx.a().b()の構文にもmethodの用途があるとして、proposal #77273を提出しました。
冒頭のr.Label(42)なら、receiverのRegistryとtype argumentのintを呼び出し箇所で解決できます。 概念上は、receiverを第1引数に移し、func label[T ~int | ~string](r Registry, v T) stringへの呼び出しとして扱えます。 receiverを通常の引数として扱えば、動的な受け手の列挙は不要になります。 具体型への呼び出しは、既存のgeneric functionと同じ仕組みでinstantiationできます(proposal #77273のImplementation節)。
interface methodを実装しない制約は、代入の可否にも現れます。 たとえば、次の宣言ではRegistryをIntLabelerへ代入できません。
type IntLabeler interface {
Label(int) string
}
var _ IntLabeler = Registry{}
Label[int]という呼び出しが可能でも、型Registryのmethod宣言はLabel[T ...](T) stringのままです。 Label(int) stringへ自動的に固定してinterfaceを満たす規則はありません。 var _ IntLabeler = Registry{}は、Go 1.27.3でもmethodの型が一致せず、コンパイルエラーになります。 instantiation前のgeneric methodをreflectionから名前や番号で取得する操作も対象外です(proposal #77273)。
標準ライブラリでの採用例は、math/rand/v2の*Randに加わったN[Int intType](Int) Intです。 従来のpackage functionrand.Nと同じ型の整数範囲を、個別の乱数生成器から生成できます(math/rand/v2.Rand.N)。
JSON v2のAPIと互換性
Go 1.27では、GOEXPERIMENT環境変数を設定せずにencoding/json/v2とencoding/json/jsontextをimportできます。 既存のencoding/jsonも、内部ではv2の実装を使うようになりました。 ただし旧APIは互換オプションを使うため、importパスを変えなければ、既定動作までv2へ切り替わるわけではありません(encoding/jsonの移行仕様)。
なぜ既存のAPIを修正するだけでは足りなかったのか
旧APIで型ごとのJSON表現を定めるには、MarshalJSON() ([]byte, error)で完全なJSONのバイト列を返します。 method側は返却用のバイト列を組み立て、呼び出したJSON実装は、受け取った内容を再検査して出力形式を調整します。 完全なバイト列を返す契約があるため、methodとJSON実装の間で中間データの生成と走査が発生します(JSON v2の設計背景)。
読み込み側のUnmarshalJSON([]byte) errorも完全なJSON値を要求するので、呼び出し元は値の終端まで走査してからmethodへ渡します。 methodは受け取ったJSONを再び解析するため、再帰的な変換では、階層を下るたびに同じ範囲の一部を走査し直します。 内部実装だけを交換しても、完全な値を受け渡すmethodの契約を保つ限り、再走査の原因は残ります。 Joe Tsaiらは、バイト列生成と再走査を既存APIの契約から生じる問題として説明しています(2025年のJSON v2解説)。
新APIは、完全なバイト列の代わりに、進行中のEncoderやDecoderを型のmethodへ渡します。 書き出しにはMarshalJSONTo(*jsontext.Encoder) error、読み込みにはUnmarshalJSONFrom(*jsontext.Decoder) errorを使います。 methodは渡されたEncoderへ直接書き、またはDecoderから直接読みます。 変換途中の処理をmethodへ引き継げるため、methodとの境界で値全体を別のバイト列にする必要がなくなります(MarshalerTo、UnmarshalerFrom)。

EncoderとDecoderが属するjsontextは、JSONのtokenや値を読み書きし、配列やobjectの途中状態など、構文の整合性を管理します。 Goのstructなどへ変換する場合は、構文層の上にあるjson/v2が、JSONとGoの値を対応付けます。 反対に、token単位で入力を加工するだけなら、structへ変換せずにjsontext.Decoderを使えます(jsontextの概要、v2のfunction一覧)。
json/v2から変換を始める場合も、入出力の形式に応じて入口を選べます。 バイト列ならMarshalとUnmarshal、io.Writer・io.ReaderならMarshalWriteとUnmarshalReadを使います。 jsontextのEncoder・Decoderを既に使っているなら、MarshalEncodeとUnmarshalDecodeへ渡せます。 各functionは可変長のOptionsを受け取るので、入口が異なっても呼び出しごとに動作を指定できます(encoding/json/v2)。
新旧の既定動作を、同じ入力で比較する
encoding/json/v2は、重複したobject名と不正なUTF-8を既定で拒否します。 旧APIは不正なUTF-8を置換文字へ変えて受け入れ、重複名の処理は変換先の型に依存します。 例えば{"role":"user","role":"admin"}をmap[string]stringへ読むと、旧APIではadminを格納します。
nilのスライスとmapをmarshalする場合、旧APIの出力はnullですが、v2ではスライスが[]、mapが{}になります。 field名の照合も変わり、旧APIではNameへキーnameを読めますが、v2の既定では一致しません。 未知のキーを無視する既定動作によって、v2のNameは空のままです(v1とv2の動作比較、v2のSecurity Considerations)。
次のプログラムは、不正なUTF-8を含む入力もバイト列で明示して、新旧の結果を比較します。
package main
import (
jsonv1 "encoding/json"
jsonv2 "encoding/json/v2"
"fmt"
)
func main() {
for _, input := range []struct {
name string
data []byte
}{
{"duplicate", []byte(`{"role":"user","role":"admin"}`)},
{"invalid-utf8", []byte{'"', 0xff, '"'}},
} {
var a, b any
e1 := jsonv1.Unmarshal(input.data, &a)
e2 := jsonv2.Unmarshal(input.data, &b)
fmt.Printf("%s: v1-ok=%t v2-ok=%t\n", input.name, e1 == nil, e2 == nil)
}
var a, b struct{ Name string }
if err := jsonv1.Unmarshal([]byte(`{"name":"Go"}`), &a); err != nil {
panic(err)
}
if err := jsonv2.Unmarshal([]byte(`{"name":"Go"}`), &b); err != nil {
panic(err)
}
fmt.Printf("case: v1=%q v2=%q\n", a.Name, b.Name)
for _, value := range []any{[]int(nil), map[string]int(nil)} {
old, err := jsonv1.Marshal(value)
if err != nil {
panic(err)
}
current, err := jsonv2.Marshal(value)
if err != nil {
panic(err)
}
compat, err := jsonv2.Marshal(value, jsonv1.DefaultOptionsV1())
if err != nil {
panic(err)
}
fmt.Printf("nil: v1=%s v2=%s compatibility=%s\n", old, current, compat)
}
}
Go 1.27.3での出力は次のとおりです。
duplicate: v1-ok=true v2-ok=false
invalid-utf8: v1-ok=true v2-ok=false
case: v1="Go" v2=""
nil: v1=null v2=[] compatibility=null
nil: v1=null v2={} compatibility=null
末尾のcompatibility=nullは、同じv2の実装でも、jsonv1.DefaultOptionsV1()を渡すと旧APIのnil表現になることを示しています。 nilの表現だけを変えるなら、FormatNilSliceAsNull(true)やFormatNilMapAsNull(true)を選べます。 名前の対応付けにはMatchCaseInsensitiveNames(true)、未知のfieldの拒否にはRejectUnknownMembers(true)があります(v2のOptions)。
Go 1の互換性文書では、互換性を保証する対象を次のように定めています。
Compatibility is at the source level.
出典:Go 1 and the Future of Go Programs。
互換性の対象は、ソースコードの水準でコンパイル・実行できることです。 不具合修正、未規定の動作、仕様上の誤り、セキュリティ、標準ライブラリのstructをキーなしで初期化するコードなどは例外です。 JSONでは、旧APIの振る舞いを互換オプションで残し、新しいimport先でnil表現や入力拒否の既定値を変える形を採っています(互換性文書のExpectations、v1の移行仕様)。
実験版のサンプルから変わったAPI
Go 1.25以降の実験版からは、タグやmethodの契約も変わっています。 inlineタグはembedへ改名され、formatタグ、unknownタグ、DiscardUnknownMembersは削除されました。 独自のJSON処理から標準動作へ委ねる指定も、削除されたSkipFuncからerrors.ErrUnsupportedへ変わっています。 数値を取り出すjsontext.Tokenのmethodは、値に加えてエラーも返します(Go 1.27のJSON変更)。
stringタグは、スライスなどの複合型へ再帰的に適用されなくなりました。 v2の意味付けでは、複合型への指定は実行時エラーになります(proposal #79065)。 MatchCaseInsensitiveNamesも、複数のfieldが候補になり、完全一致がなければエラーを返します。 以前のように宣言順の最初のfieldを選ぶ動作はなくなりました(曖昧なfield照合を拒否するcommit)。
encoding/jsonの内部実装を従来のものへ戻すビルド用の指定は、GOEXPERIMENT=nojsonv2です。 一方、v2の実装でnilの出力だけをnullにするには、FormatNilSliceAsNull(true)やFormatNilMapAsNull(true)を使います。 GOEXPERIMENTは旧APIの実装を選び、変換オプションはv2での出力規則を選びます(Go 1.27のJSON変更)。
goroutineleakの到達可能性による検出
runtime/pprofのgoroutineleak profileは、Go 1.27ではGOEXPERIMENT環境変数を設定せずに使えます。 HTTPでの取得先は/debug/pprof/goroutineleakです(runtime/pprof.Profile)。 GOEXPERIMENT=goroutineleakprofileによる有効化指定は削除されました(Go 1.27のruntime変更)。
GCは、GC rootを起点に参照をたどり、到達したheap objectをmarkして記録します。 通常はすべてのgoroutineがGC rootになるので、回復不能にblockしたgoroutineが保持するmemoryもmarkされます。 通常のmark結果には、リークしたgoroutineからしか参照できないmemoryも、到達可能なものとして残ります。
GC rootを絞るリーク検出方式を、Georgian-Vlad SaiocとMilind Chabbiが提案しました。 実行可能なgoroutineを最初のGC rootとし、blocked goroutineは起点から外します。 グローバル変数など、goroutine以外の通常のGC rootは残します(goroutineリーク検出のproposal)。
最初のmarkが終わったら、未markのblocked goroutineについて、channelやMutexなど、blockの原因になっている同期対象を調べます。 同期対象がmark済みなら、到達可能な側の処理によってgoroutineを再開できる可能性があるため、対象のgoroutineもGC rootへ加えます。 新しく加えたGC rootからさらにheapをmarkし、追加できるgoroutineがなくなるまで繰り返します。 反復が終わっても起点へ加えられなかったgoroutineが、リークとして記録されます。
例えば、実行可能なG0がc1を参照し、c1でblockしているG1がc2を参照し、G2がc2でblockしているとします。 初期のGC rootからc2への参照経路はないものとします。 図では、GC rootから到達できない別のchannelc3で、G3もblockしていると仮定しています。

最初のmarkで到達するのはc1までですが、G1をGC rootへ加えるとc2へ到達し、G2もGC rootへ加えられます。 初回のmarkだけでリークを判定すると、参照をたどる途中で見つかるgoroutineを誤ってリークに含めるため、反復が必要です(検出アルゴリズムのproposal)。 反復が終わってもc3は到達可能にならないため、G3がリークとして記録されます。
同期処理用object*sudogは、allgsやsemtableからも到達できます。 参照を無条件にたどると、再開可能な側から到達していないchannelまでmarkされてしまいます。 proposalでは、*sudogの.elemと.hchanを条件付きでたどり、通常のmarkから参照を一時的に隠します(GCと参照表現の変更)。
次のプログラムは、orphanedとreachableを、別のchannelの受信でblockさせます。 orphanedのchannelはblocked goroutineだけに残す一方、reachableのchannelはグローバル変数heldにも残します。
package main
import (
"bytes"
"fmt"
"runtime"
"runtime/pprof"
"strings"
)
var held chan int
func orphaned(ch <-chan int, ready chan<- struct{}) {
close(ready)
<-ch
}
func reachable(ch <-chan int, ready chan<- struct{}) {
close(ready)
<-ch
}
func startOrphan() {
ch := make(chan int)
ready := make(chan struct{})
go orphaned(ch, ready)
<-ready
runtime.Gosched()
}
func main() {
runtime.GOMAXPROCS(1)
startOrphan()
held = make(chan int)
ready := make(chan struct{})
go reachable(held, ready)
<-ready
runtime.Gosched()
var profile bytes.Buffer
if err := pprof.Lookup("goroutineleak").WriteTo(&profile, 1); err != nil {
panic(err)
}
fmt.Println("orphaned:", strings.Contains(profile.String(), "main.orphaned"))
fmt.Println("reachable:", strings.Contains(profile.String(), "main.reachable"))
}
Go 1.27.3のdarwin/arm64で得た結果は次のとおりです。
orphaned: true
reachable: false
reachableはheldを通じてGC rootから到達できるため、profileへ列挙されません。 ただし、mainはheldへの送信やcloseを行わないため、reachableの受信は終わりません。 到達できるchannelが実際に使われるかまでは判定しないため、回復しないblockでも参照が残る場合は検出から外れます。
profileを取得しても、リークしたgoroutineやheap objectは強制的に回収されません。 リークと判定したgoroutineからも最後にmarkしてから、通常のsweepへ進みます。 リーク側のmemoryも残るので、goroutineが保持するmemoryを回収せずに診断結果を得られます(検出範囲と資源回収の扱い)。
小さいheap割り当ての特殊化
Go 1.27のcompilerは、小さいheap割り当てについて、サイズ別に特殊化したruntime functionを呼びます。 16バイトの型をheapへ割り当てる例では、Go 1.27.3のdarwin/arm64で呼び出し先が変わります。
package allocation
type Value struct{ A, B int64 }
func New() *Value { return new(Value) }
既定ではsize class別の入口へ進み、GOEXPERIMENT=nosizespecializedmallocで無効にするとruntime.newobjectを呼びます。
go tool compile -S allocation.go \
| rg --only-matching 'runtime\.(mallocgcSmallNoScanSC2|newobject)' | sort -u
# => runtime.mallocgcSmallNoScanSC2
GOEXPERIMENT=nosizespecializedmalloc go tool compile -S allocation.go \
| rg --only-matching 'runtime\.(mallocgcSmallNoScanSC2|newobject)' | sort -u
# => runtime.newobject
Valueはpointerを持たない16バイトの型なので、compilerは、走査不要を意味するNoScanと、サイズクラス2のSC2に対応する入口を選べます。 runtime.mallocgcSmallNoScanSC2には、sizeclass = 2とelemsize = 16が定数として埋め込まれています。 runtimeは埋め込まれた分類を使えるため、割り当てのたびにサイズクラスを決める経路を通らずに済みます(Go 1.27.3のmalloc_generated.go)。
特殊化の対象は、一部の80バイト未満の割り当てです。 サイズ別のruntime functionを追加するため、バイナリサイズはワークロードに依存せず約60KB増えると公表されています。 GOEXPERIMENT=nosizespecializedmallocはGo 1.27のビルド時に使用できます(Faster memory allocation)。
SIMDの可変幅APIとarchitecture固有のAPI
Go 1.27のexperimental packagesimdは、vector幅を型名に固定しないAPIを提供します。 固定幅APIでは、128ビット、256ビット、512ビットの幅に応じてループの処理単位が変わります。 対して、simdの呼び出し側は、実際に読み込めた要素数でループを進めます(Portable SIMD proposal #78902)。
例えばInt32sは、int32型の値を複数まとめて保持し、加算などの演算を要素ごとに適用するSIMD vectorです。 vectorの幅は128ビット以上で、プログラムの実行中は全vectorが同じ幅を使うため、処理中に要素数の基準は変わりません。 対応するCPU命令がなければGoで演算を再現するので、命令がない環境でもポータブル版のAPIを使えます(simdの仕様)。
次のプログラムは、LoadInt32sPartで実際に読んだ要素数をnへ受け取り、nだけループを進めて6要素へ10を加算します。 残りがvectorを満たさない場合も、LoadInt32sPartは不足分をゼロで埋め、入力から読んだ数を返します。 出力先をn要素に絞ってStorePartへ渡せば、末尾の端数でも、入力に対応する要素だけを書き戻せます(部分ロードとストア)。
package main
import (
"fmt"
"simd"
)
func main() {
in := []int32{1, 2, 3, 4, 5, 6}
out := make([]int32, len(in))
for i := 0; i < len(in); {
v, n := simd.LoadInt32sPart(in[i:])
v.Add(simd.BroadcastInt32s(10)).StorePart(out[i : i+n])
i += n
}
fmt.Println(out)
}
Go 1.27.3、darwin/arm64でのcommandとstdoutは、次の組み合わせです。
GOEXPERIMENT=simd go run simd.go
[11 12 13 14 15 16]

CPU固有の型と演算を直接使う場合は、ポータブル版に代わってsimd/archsimdを選びます。 Go 1.27ではamd64向けAPIが改訂され、arm64のNeon 128ビットとWebAssemblyの128ビットSIMDが加わりました。 amd64では、CPUが対応していれば256ビット・512ビットも扱えます。 simd/archsimdの有効化にもGOEXPERIMENT=simdを使います(simd/archsimd、Go 1.27の変更範囲)。
ポータブル型からCPU固有の演算へ渡すには、ToArch()で値を取り出します。 戻り値はanyなので、対象アーキテクチャのビルド制約を付け、具体型へtype assertionして使います。 逆に、CPU固有の値をポータブル型へ戻す場合はInt32sFromArchなどを使い、対応しない型を渡すとpanicします(simdのアーキテクチャ間変換)。
struct literalとtype inferenceの変更
埋め込みfieldから昇格したfieldを、struct literalのキーとして指定できるようになりました(proposal #9859)。 generic functionのtype argumentも、対応するfunction typeへの代入・変換全般で推論できるようになりました(proposal #77245)。
package main
import "fmt"
type Inner struct{ Port int }
type Outer struct{ Inner }
func keep[T any](value T) T { return value }
func main() {
value := Outer{Port: 8080}
functions := []func(int) int{keep}
fmt.Println(value.Inner.Port, functions[0](7)) // => 8080 7
}
PortはOuter直下の宣言ではなく、Innerから昇格したfieldです。 スライスの要素では、期待されるfunc(int) intからkeepのtype argumentを推論します。 同じstruct literalで、昇格したfieldと、昇格元の埋め込みfieldの両方は指定できません(Go 1.27.3のstruct literal検査)。
goコマンド、診断ツール、compilerの変更
ビルドへの入力と互換設定
compile、link、asm、cgo、cover、packは、@fileによる引数ファイルを受け付けます。 形式はGCC互換で、引用符、エスケープ、バックスラッシュによる行継続を使えます。 モジュール取得ではbzrのサポートが削除され、bzrサーバーからの直接取得はできなくなりました。
削除済みのGODEBUG設定がgo.modのgodebugや//go:debugに残っていても、削除前の最終既定値を指定していれば受理されます。 以前の動作へ戻す値を要求している場合は、ビルドが拒否されます(Goコマンドとツールの変更)。
テストとコードの検査
go testはstdversion検査を既定で実行します。 参照元ファイルに適用されるGo版より新しい標準ライブラリのシンボルを使っていれば、検査結果に現れます。 go test -jsonでは、outputイベントのOutputTypeにerror、error-continue、frameが加わりました。
vetのprintf検査は、値型E自身がerrorを実装する場合に、fmt.Errorfの%wへ*Eを渡す呼び出しを報告します。 期待するerrorの型と、包んだ値の型の不一致が対象です。 ドキュメントの表示には、go docでpackage@versionと-exを使えます。 -exは実行可能な例を列挙し、例の名前からコードとコメントも表示できます(テストと診断ツールの変更)。
コードと依存宣言の整理
go fixにはatomictypes、embedlit、slicesbackward、unsafefuncsが加わりました。 fmtappendfは削除され、waitgroupはwaitgroupgoへ改名されています。 go mod tidyは、go 1.27以降のモジュールでrequireブロックを直接依存と間接依存の最大2つに整理し、依存に付いたコメントを保ちます。
gofmtとgo/formatは、CPUアーキテクチャによって異なっていたカラム揃えの判定を修正しています。 整形結果が統一されるため、一部の既存ファイルには空白の差分が生じます(整形とコード更新の変更)。
runtimeの診断とタイマー
go tool traceの-http=:6060は、listen先がlocalhostに限定されました。 全アドレスでlistenするには、-http=0.0.0.0:6060を指定します。 tracebackは、go 1.27以降のモジュールでgoroutineのpprofラベルをヘッダーに含めます。 ラベルを出さない場合は、GODEBUG=tracebacklabels=0を指定できます。
タイマーではasynctimerchanが削除されました。 time packageが作るタイマーのchannelは、常に同期channelになります(Runtimeの変更)。
compilerとlinkerが扱う名前
//lineと/*line*/にある相対ファイル名は、ディレクティブを含むファイルのディレクトリから解決されます。 クロージャーは、インライン化によらず単純なシンボル名を使います。 同じfunctionリテラルのコードを共有する場合は、捕捉データが異なるクロージャーでも、コードポインターは一致し得ます。
macOS向けlinkerは、-macosと-macsdkでLC_BUILD_VERSIONのOS版とSDK版を指定できます。 既定値はmacOS 13.0.0とSDK 26.2.0です(CompilerとLinkerの変更)。
標準ライブラリの追加と動作変更
署名方式とTLS
crypto/mldsaにFIPS 204のML-DSA署名が加わり、x509でも鍵と署名を処理できます。 TLS 1.3ではMLDSA44、MLDSA65、MLDSA87を使えます(crypto/mldsa)。 cryptoには、External μ ML-DSA署名を識別するHash値MLDSAMuが加わりました(crypto.MLDSAMu)。
crypto/ecdsaでは、非nilのSignerOptsをPrivateKey.Signへ渡すと、ハッシュの長さが適切か検査されます(PrivateKey.Sign)。 QUICConfigにはClientHelloInfoConn、ConnectionStateにはLocalCertificateが加わりました。 CurvePreferencesにはMLKEM1024も指定できます(crypto/tls)。
TLSのConfig.Randは非推奨になりました。 決定的なテストにはtesting/cryptotest.SetGlobalRandomを使えます。 tlsmlkem=0やtlssecpmlkem=0を指定していても、CurvePreferencesへの明示指定は有効です(tls.Config)。 互換設定のtlsunsafeekm、tlsrsakex、tls3des、tls10server、x509keypairleafは削除されました。 tlskyberが1.24で削除済みだったことも補記されています(TLSの変更)。
証明書の表現と検証
crypto/x509の証明書、CSR、CRLにはRawSignatureAlgorithmが加わりました。 未知の署名方式でも、DERのAlgorithmIdentifierを取得できます。 pkix.Nameの未知の属性値も、asn1.RawValueとして保持されます(crypto/x509)。 crypto/x509/pkixのRDNSequence.Stringは、未知のOIDでも、文字列型の属性を文字列として表示します。 従来の表示はDERの16進表現でした(RDNSequence.String)。
WindowsとDarwinでは、SSL_CERT_FILEかSSL_CERT_DIRを指定すると、ファイルからルート証明書を読み込みます。 検証もOSのAPIからGoの検証器へ切り替わり、x509sslcertoverrideplatform=0で無効化できます(SystemCertPool)。
値の生成、分割、型変換
新しいuuid packageは、UUIDを16バイトの型として扱います。 New、NewV4、NewV7で生成し、Parseなどで読み込めます(uuid)。 bytesとstringsには、最後の区切り文字で前後を分けるCutLastが加わりました(bytes.CutLast、strings.CutLast)。
database/sqlのConvertAssignを使うと、Rows.Scanの型変換を利用できます(ConvertAssign)。 driver側にはRowsColumnScannerが加わり、利用者の格納先へ直接scanできます(RowsColumnScanner)。
math/bigのInt.Divideは、商と余りを求める演算です。 丸め方向にはTrunc、Floor、Round、Ceilを指定できます(Int.Divide)。 hash/maphashには、型ごとのハッシュと同値比較の契約を定めるHasherが加わりました。 ==で比較する場合はComparableHasherを使えます(hash/maphash)。
Hasherの同値比較とhashの入力を一致させる
Hasher[K]は、Hash(*Hash, K)とEqual(K, K) boolを持つinterfaceです。 Equalがtrueになる2値は、同じseedで同じhash値にならなければなりません。 値が異なってもhash値が衝突することは許されますが、同値な値のhash値が異なる実装は契約違反です。 hash値を使って探索先を選ぶcontainerでは、同値比較へ到達する前に別の場所を探してしまうためです(Hasherの契約)。
たとえば、小文字へ変換した結果で文字列を比較する実装は、hashにも同じ変換結果を渡します。 Equalだけで大文字と小文字を区別しなくなっても、元の文字列をhashしていれば、探索先を決める規則と同値比較が一致しません。 Lowercaseは、比較とhashの両方でstrings.ToLowerを使います。
type Lowercase struct{}
func (Lowercase) Hash(h *maphash.Hash, value string) {
h.WriteString(strings.ToLower(value))
}
func (Lowercase) Equal(left, right string) bool {
return strings.ToLower(left) == strings.ToLower(right)
}
同じseedを使えば、比較で等しい"Name"と"name"のhash値も一致します。
seed := maphash.MakeSeed()
var left, right maphash.Hash
left.SetSeed(seed)
right.SetSeed(seed)
Lowercase{}.Hash(&left, "Name")
Lowercase{}.Hash(&right, "name")
fmt.Println(Lowercase{}.Equal("Name", "name")) // => true
fmt.Println(left.Sum64() == right.Sum64()) // => true
Hasherはhash値を直接返すAPIではなく、呼び出し側から受け取ったHashへ値の表現を書き込みます。 seedを含むhash計算の状態と、型ごとの入力の作り方を分離した形です。 maphash.MakeSeedで作るseedは計算の入力なので、異なるseedで作ったhash値の一致は要求しません(Hash.SetSeed)。
ただし、ToLowerを使う比較は、strings.EqualFoldと同じ比較ではありません。 文字"ſ"と"s"は、比較方法によって同値性が変わります。
fmt.Println(strings.EqualFold("s", "ſ")) // => true
fmt.Println(Lowercase{}.Equal("s", "ſ")) // => false
Unicodeのsimple case foldingには、小文字への変換だけで同じ結果にならない文字の組み合わせがあります。 EqualFoldへ比較だけを置き換え、hash側へToLowerを残す変更は、同値な文字列のhash値を揃える契約を壊し得ます。 文字列を正規化する場合には、比較とhash入力で採用する文字の同一視まで揃える必要があります(strings.EqualFold)。
新しいinterfaceは、組み込みのmapへ独自の比較規則を渡す機能ではありません。 組み込みのmapは引き続きcomparableなkeyと==を使います。 Hasherを受け取る独自のcontainerなら、key型の比較方法とhash入力を共通のinterfaceで受け取れます。 したがって、map[string]Vの宣言を変えずに大文字と小文字を同一視する設定が増えたわけではなく、独自のcontainerが公開できる契約の追加です(mapのkeyに関する規則)。
Goコードの解析に使うAPI
go/constantのStringLenは、文字列定数の全体を構築せずに長さを返します(StringLen)。 go/scannerのScanner.Endはトークンの終端を返し、go/tokenのFile.StringはFileの短い説明を返します(Scanner.End、File.String)。
go/typesには、Identicalに対応するHasherと、IdenticalIgnoreTagsに対応するHasherIgnoreTagsが加わりました。 gotypesaliasも削除され、型エイリアスには常にAliasノードが生成されます(go/types)。
HTTPと接続の扱い
netのUnixConnは、読み取りがEOFに達すると、net.OpErrorで包まずにio.EOFを直接返します(UnixConn)。 HTTPでは、ConnectionState() tls.ConnectionStateを実装したnet.Connに対応しました。 TransportとServerは、実装から得たTLSの情報を使い、ALPNでプロトコルを選びます(HTTPの変更)。
HTTP/2は、RFC 9218のクライアント優先度を受け付けます。 Server.DisableClientPriority=trueを指定した場合は、優先度によらない従来のラウンドロビン処理を使います(http.Server)。 ヘッダー値の数はServer.MaxHeaderValueCountで制限でき、0ではDefaultMaxHeaderValueCountを使います(MaxHeaderValueCount)。
HTTP/1のResponse.BodyをCloseすると、未読本文の読み捨てを試みます。 閾値は256KiB、待機期限は50msで、EOFへ到達した場合に接続を再利用します(Go 1.27.3のtransport.go)。 net/urlには、深いコピーを作るURL.CloneとValues.Cloneが加わりました(URL.Clone、Values.Clone)。
テストと実行環境
net/http/httptestのNewTestServerは、synctestで使えるmemory上の仮想ネットワークを提供します(NewTestServer)。 testing/synctestのSleepは、time.Sleepとsynctest.Waitを結合した操作です(synctest.Sleep)。
runtime/secretでは、secretモード中に作ったgoroutineもsecretモードを継承します(runtime/secret)。 compress/flateではエンコーダーの変更に伴い、圧縮後のバイト列も変わる場合があります。 DEFLATEを使うzip、gzip、zlib、PNGの出力にも影響します(flateの変更)。
OSと文字データの更新
Plan 9のsyscallには、errorを実装するErrno型が加わりました。 Errnoを参照する共通コードをビルドできますが、システムコールの返り値は従来のErrorStringを保ちます(syscallの変更)。 unicodeの文字テーブルと関連処理は、Unicode 15から17へ更新されました(unicode.Version)。
macOSの最低要件は13 Venturaになりました。 Linuxのビッグエンディアンppc64はELFv2 ABIへ移行し、cgo、PIE、外部リンクをサポートします。 ELFv2対応は通常Linux 3.13以降で、RHEL 7には3.10へのバックポートがあります。 cgoや外部リンクを利用する場合は、リンク・ロードするlibcなどのライブラリにもELFv2対応が必要です(Portsの変更)。
1.27.2と1.27.3の修正で変わる処理
JSONの互換層の修正
1.27.2では、encoding/jsonで、名前付き文字列型をmapのキーにした場合の互換性を修正しています。 Go 1.26では名前付き文字列型のキーのMarshalTextを呼んでいませんでしたが、v2実装への切り替えで呼ぶようになっていました。 1.27.2の互換層はMarshalTextを呼ばず、Go 1.26と同じ結果を返します(JSONのmap key互換性を戻すCL 827864)。
1.27.2のセキュリティ修正
HTTP/2からHTTP/1へ転送するプロキシは、不正なフレーミング関連ヘッダーを削除するようになりました。 不正なヘッダーの転送によるresponse smugglingを防ぎます(不正なHTTP headerを除去するcommit)。 html/templateも、各JavaScript式の先頭の正規表現を正しくエスケープできるよう、式の文脈をリセットします(JavaScript式の文脈を初期化するcommit)。 キーワードとしてのyieldの後は正規表現と判定し、キーワードではない使い方は同じ扱いにしません(yield後の正規表現判定を直すcommit)。
モジュール取得では、golang.org/fips140向けの特例がなくなり、正規のgo.sumのh1ハッシュがfips140.sumへ加わりました(FIPSのh1 hashを追加するcommit)。 golang.org/toolchainは、sumdbから正規のチェックサムを取得して検証します。 local cacheのチェックサム記録が一致していても、sumdbとの照合を省略しません(toolchainのsumdb検証を必須にするcommit)。
TLSのECHは、不正なouter extensionの参照を拒否し、各拡張を高々一度だけコピーします(ECHの拡張参照を検証するcommit)。 MIMEヘッダーでは、memory上限から管理用の400バイトを引いた結果が負になると、先頭行だけ無制限として扱われていました。 1.27.2では、負の値でもmemory上限を迂回しないように修正しています(MIMEのmemory上限を適用するcommit)。
CONNECTの修正では、Transportが応答にかかわらずCONNECT後の接続を閉じ、ReverseProxyがCONNECTを405で拒否します。 旧動作では、サーバーがCONNECTを拒否すると、送信したRequest.Bodyが接続上の次のHTTP要求として解釈される可能性がありました。 接続をプールへ戻していたため、次に送った要求が先行する要求への応答を読む、という取り違えが起きていました(CONNECT後のconnection poolを保護するcommit)。 HTTP/1サーバーも、ハンドラーが2xxを返して接続をhijackせず戻った場合、CONNECTの接続を通常のHTTP要求へ再利用しません(CONNECT後の接続再利用を止めるcommit)。
Windowsのos.Rootは、Root.MkdirやRoot.MkdirAllの対象がリンク先の存在しないjunctionだった場合の処理を修正しています。 ルート外へのディレクトリ作成を防ぐ修正です(Windowsのjunction処理を直すcommit)。 ServeContentのRange処理も、大量の小さい範囲指定によるCPUとmemoryの消費を制限します。 既定の上限は200範囲で、httpservecontentmaxrangesで変更できます(Rangeの処理量を制限するcommit)。
HTTP/2の状態管理と処理量
HTTP/2の初期ウィンドウ変更は、SETTINGSの変更ごとに全ストリームを走査する処理から、O(1)の処理へ変わりました。 各ストリームは初期値を保存せず、WINDOW_UPDATEによる増分と送信済みバイト数の差を保持します。 現在のウィンドウは、増分と送信済みバイト数の差に、現在の初期値を足して求めます。 初期値をストリーム別に保存しないため、SETTINGSの受信時に全ストリームの値を書き換える必要がなくなります。 初期値変更時に全ストリームを検査する代わりに、個々の送信時やWINDOW_UPDATEの処理時に上限超過を検出します。
上限を管理する別案として、実装者のDamien Neilは、ストリームをheapで管理する方式も検討しています。 しかし、heapの管理で実装が複雑になり、処理量も増えるため採用していません。 最大値だけを追跡する方式も、ストリームの作成・更新・resetと初期値変更を組み合わせる攻撃を防げないとして退けています(HTTP/2の初期window更新を変えたcommitの設計理由)。
受信側では、ストリームのreset時と、reset後のハンドラーによる読み取り時に、同じデータのフロー制御枠を二重返却する問題を修正しています(受信枠の二重返却を防ぐcommit)。 HPACKも、応答ヘッダーのエンコードと、SETTINGSによるテーブルサイズの変更を並行して行わなくなりました(HPACKの並行更新を防ぐcommit)。 Trailerでは、宣言したfieldによるヘッダー数とサイズの上限の迂回を防ぎ、memory確保を制限します(Trailerにもheader上限を適用するcommit)。
Go 1.27.3で拒否する中間証明書
1.27.3の修正は、証明書のSubjectPublicKeyInfoをSHA-256でハッシュし、拒否対象の公開鍵ハッシュと照合します。 対象は8個のハッシュとして実装に保持されています。 拒否対象の公開鍵を持つ証明書がチェーンに含まれていれば、照合したチェーンを有効な候補から外します。 Go自身の検証器だけでなく、DarwinやWindowsのシステム検証器が返したチェーンにも適用します。 Windowsで有効なチェーンが残らなければ、検証は失敗します(中間証明書を拒否するcommit)。
脆弱性の登録はCVE-2026-111943、Go側のadvisoryはGO-2026-6809です。 Go 1.27系では、crypto/x509.Certificate.Verifyが対象となり、1.27.3で対処されています。 照合には、証明書全体のDERではなく、SPKIのDERをハッシュした値を使います。 したがって、subjectや有効期限が異なる証明書でも、SPKIのbyte列が一致すれば、同じ拒否対象のハッシュに一致します(crypto/x509の検証実装)。