#81698likely_accept
maps: add Grow
新規提案
要約
AIによる要約であり、誤りを含む場合があります。
概要
既存のマップに対して「この後 n 個のエントリを追加する予定がある」と事前に伝え、容量をまとめて確保できるようにする maps.Grow 関数を maps パッケージに追加する提案です。あわせて、maps.Copy もコピー先を自動で事前拡張するように変更されます。
ステータス変更
(新規) → likely_accept
2026年10月14日の Proposal Review Meeting で @aclements が議論の結果を報告しました。委員会の判断は次の3点です。
- 前回「Go のコードでは書けない機能」を追加したのは
clearで、このときは組み込み関数になりました。ただしclearはマップとスライスの両方を対象にしていたため、置き場所になるライブラリパッケージがありませんでした。今回はマップ専用なので、多少「マジック」な関数でもmapsパッケージに置くことに反対する委員はいませんでした。 - 引数 n はあくまでヒントです。それ以上拡張されないことを保証するものではない旨をドキュメントに明記します。
maps.Copyが内部で自動的に事前拡張するのは理にかなっています。
この方針に基づいて likely accept と判断されました。
技術的背景
現状の問題点
マップの容量を事前に指定できるのは、make(M, hint) で新しく作る場合だけです。maps.Clone も元のマップと同じサイズで複製するだけです。すでに存在するマップに大量のエントリを追加すると、要素数が増えるたびに段階的なリハッシュ(内部テーブルの作り直し)が何度も起き、その分の時間とメモリ確保が無駄になります。
コピー先のマップを自分が所有していない場合(たとえば http.ResponseWriter.Header() が返すマップ)は、make で新しいマップを作って差し替えることもできません。標準ライブラリでは次の箇所がこれに当たります。
net/httpのTimeoutHandlernet/http/httputilのReverseProxy(copyHeader)net/http/cgi
外部ライブラリの例として libhoney-go があります。ここでは「新しいマップを大きめに作り、既存のエントリをすべて移し替えてから差し替える」という回避策を取っており、それでも本番環境で約5%の CPU 削減になりました。ただし差し替えによって、呼び出し側が持っているマップへの参照が切れるという副作用も生じています。
提案された解決策
// Grow は m の容量を必要に応じて増やし、典型的なケースでは
// あと n 個のエントリを再拡張なしで追加できるようにする
// (make(M, len(m)+n) で作成した場合と同等)。縮小はしない。
// ベストエフォートであり、追加の拡張が起きないことは保証しない。
// n が負の場合、または m が nil で n > 0 の場合は panic する。
func Grow[M ~map[K]V, K comparable, V any](m M, n int)
slices.Grow と違って戻り値はありません。マップをその場で更新するためで、そのぶん nil マップから新しいマップを作ることはできません。また maps.Copy(dst, src) は、dst を len(src) まで事前拡張するようになります。
これによって何ができるようになるか
- HTTP ヘッダーのマージ: リバースプロキシやミドルウェアで、上流レスポンスのヘッダーを
rw.Header()にまとめて追加する前に容量を確保できます。 - スライスやストリームからの一括投入: map 以外のデータ源から、既存のマップへまとめてエントリを追加する場合に使えます。例として、CORS ミドルウェアの初期化で使う配列ベースのアキュムレータがあります。
- 参照を維持したままの高速マージ: libhoney-go のようにマップの差し替えをしなくてよくなるため、呼び出し側が持つ参照が正しく保たれます。
さらにmaps.Copyは自動的に高速になります。#56182 と CL 839065 のベンチマークでは、空または小さなマップへのコピーで実行時間が約43〜57%短縮し、メモリ確保もおよそ半分になりました。
コード例
// Before: 自分が所有していないマップへの追加。段階的なリハッシュが何度も起きる
func copyHeaders(dst http.Header, src []kv) {
for _, p := range src {
dst[p.key] = append(dst[p.key], p.val)
}
}
// After: 事前に容量を確保してから追加する
func copyHeaders(dst http.Header, src []kv) {
maps.Grow(dst, len(src))
for _, p := range src {
dst[p.key] = append(dst[p.key], p.val)
}
}
// map 同士のコピーなら、maps.Copy が内部で同様の事前拡張を行う
maps.Copy(w.Header(), upstream.Header)
議論のハイライト
- ライブラリ関数による「言語変更」ではないか: @magical は、
clearが組み込み関数になった経緯を踏まえて懸念を示しました。これに対してslices.Growという前例があることが指摘されました。委員会も、マップ専用であることからmapsに置くことを容認しました。 - API を増やすほどの効果があるか: @randall77 は、PGO などによる自動最適化の方が望ましいとしつつ、
makeのサイズヒントと同じ考え方なので許容できるとし、弱い賛成の立場を取りました。提案者の @lizthegrey は、改善幅は「数%」ではなく、ケースによっては約半分になるというベンチマーク結果を示しました。 Copyの内部最適化だけで十分ではないか: まず API を増やさずにmaps.Copyを内部で最適化する CL 839065 が出されました。拡張量をlen(src)に限定しているため、キーが重複していても過剰確保にならず、#56182 で @CAFxX が挙げた懸念は当てはまりません。一方で @jub0bs は、スライスやストリームから既存マップへ追加するケースはCopyでは扱えないと主張し、Growを公開する必要性を示しました。- 最終的な形:
Growの公開とCopyの自動事前拡張の両方を採用することになりました。ドキュメントには「ベストエフォート」であることが追記されます。 - #52157 との関係: map の
capを取得する提案は却下されています。Growは容量を外部に見せず、サイズヒントを渡すだけなので、その方針と矛盾しないと説明されています。