メインコンテンツへスキップ

Go Proposal Weekly Digest

Go言語のproposal更新を毎週お届け

新機能

net/urlパッケージにURLとValuesを安全にディープコピーするClone()メソッドを追加

net/url

この項目の注釈は AI により生成されており、誤りを含む場合があります。
使用例のコンパイル検証: 検証済み

概要

net/urlパッケージにURL.CloneメソッドとValues.Cloneメソッドが追加された。URL.CloneはURLのフィールドをディープコピーし、Userフィールド(*Userinfo)が存在する場合はそれも新たにアロケートしてコピーする。Values.Clonemap[string][]stringという入れ子構造を持つValuesを、内部のスライスも含めてディープコピーする。

導入経緯

これまでnet/url.URLを安全にコピーする方法は明確ではなかった。u2 := *u1という単純な構造体コピーは安全だが、URLUser *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 746800CL 746801)が投稿された。

議論のハイライト

  • Userフィールド(*Userinfo)をディープコピーすべきかが焦点の一つとなった。Userinfoはドキュメント上immutableとされているが、*u2.User = *someNewUserinfoのように書き換えることも技術的には可能であるため、確実性を優先してURL.CloneではUserが存在する場合にそれも新たにアロケートしてコピーする実装が採用された。
  • http.Headernet/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.Cloneu == nilのときはnilを返し、それ以外はnew(*u)でフィールドをシャローコピーした後、u.User != nilであればnew(*u.User)Userinfoも新たにアロケートしてコピーする。new(expr)という式構文(Go 1.24以降のnew拡張)を利用しており、u2 := new(url.URL); *u2 = *uと等価な処理を簡潔に書いている。

Values.Clonevs == nilのときはnilを返し、それ以外はmake(Values, len(vs))で新しいマップを確保した上で、各キーに対応するスライスをslices.Cloneでコピーする。マップのキーとスライスの双方を新たに確保するため、クローン後にどちらのValuesを変更してももう一方に影響しない。

関連リンク