net/urlパッケージにURLとValuesを安全にディープコピーするClone()メソッドを追加
net/url
概要
net/urlパッケージにURL.CloneメソッドとValues.Cloneメソッドが追加された。URL.CloneはURLのフィールドをディープコピーし、Userフィールド(*Userinfo)が存在する場合はそれも新たにアロケートしてコピーする。Values.Cloneはmap[string][]stringという入れ子構造を持つValuesを、内部のスライスも含めてディープコピーする。
導入経緯
これまでnet/url.URLを安全にコピーする方法は明確ではなかった。u2 := *u1という単純な構造体コピーは安全だが、URLがUser *Userinfoというポインタフィールドを持つため、そのことに確信を持てない開発者が多く、代わりにurl.Parse(u1.String())という非効率な再パースによるコピーが広く使われていた。提案者はGitHub上のコード検索により、単純なシャローコピーが約21,200件、再パースによるコピーが約3,300件見つかったとするデータを示し、後者が前者の約1/8に相当する規模でシリアライズ/デシリアライズの余分なコストを支払っていることを根拠とした。またnet/httpパッケージは内部に非公開のcloneURL関数を持ち、Request.Cloneなどで利用しているが、これが公開APIとしては提供されていない点も動機として挙げられた。
同様の提案は2020年にも#41733として出されていたが、当時は「Cloneが必要な型はURLに限らず多いため、URL固有の対応は不要」という理由で却下されていた。今回の提案(#73450)は、実際の使用状況を示すコード検索データを提示した点が過去の提案との違いとなった。
議論タイムライン
- 2026-01-28: proposal review groupにより
activeステータスに移行。 - 2026-02-09:
likely acceptと判定。 - 2026-02-18: 異論が出ず
acceptedとして正式に承認。同日、実装CL(CL 746800、CL 746801)が投稿された。
議論のハイライト
Userフィールド(*Userinfo)をディープコピーすべきかが焦点の一つとなった。Userinfoはドキュメント上immutableとされているが、*u2.User = *someNewUserinfoのように書き換えることも技術的には可能であるため、確実性を優先してURL.CloneではUserが存在する場合にそれも新たにアロケートしてコピーする実装が採用された。http.Header(net/url.Valuesと同じmap[string][]stringという型表現)には既にClone()メソッドが存在しており、url.ValuesにだけClone()がないのは一貫性を欠くという指摘があり、Values.Cloneの追加も支持された。- 過去の却下理由であった「Cloneの需要が不明」という点について、GitHubコード検索による実データの提示が今回の採否判断を後押しした。
使用例
Before
package main
import (
"fmt"
"net/url"
)
func main() {
base, _ := url.Parse("https://api.example.com/v1")
// 再パースによるコピー(シリアライズ/デシリアライズのコストがかかる)
u1, _ := url.Parse(base.String())
u1.Path = u1.Path + "/users"
// シャローコピー(安全だが Userinfo を共有しており分かりにくい)
u2 := *base
u2.Path = u2.Path + "/items"
fmt.Println(u1.String(), u2.String())
}
After
package main
import (
"fmt"
"net/url"
)
func main() {
base, _ := url.Parse("https://api.example.com/v1")
u1 := base.Clone()
u1.Path = u1.Path + "/users"
u2 := base.Clone()
u2.Path = u2.Path + "/items"
fmt.Println(u1.String(), u2.String())
q := url.Values{"page": {"1"}}
qCopy := q.Clone()
qCopy.Set("page", "2")
fmt.Println(q.Encode(), qCopy.Encode())
}
実装解説
URL.Cloneはu == nilのときはnilを返し、それ以外はnew(*u)でフィールドをシャローコピーした後、u.User != nilであればnew(*u.User)でUserinfoも新たにアロケートしてコピーする。new(expr)という式構文(Go 1.24以降のnew拡張)を利用しており、u2 := new(url.URL); *u2 = *uと等価な処理を簡潔に書いている。
Values.Cloneはvs == nilのときはnilを返し、それ以外はmake(Values, len(vs))で新しいマップを確保した上で、各キーに対応するスライスをslices.Cloneでコピーする。マップのキーとスライスの双方を新たに確保するため、クローン後にどちらのValuesを変更してももう一方に影響しない。