From 09c3cfbc208fbd3ee8a06885e7efa53cdd56be7a Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Wed, 18 Feb 2026 17:03:37 -0500 Subject: [PATCH] net/url: add (*URL).Clone This change adds URL.Clone which creates a deep copy of the URL's fields including the .User and tests these. In a separate CL I shall send Values.Clone too. Updates #73450 Change-Id: Ifea4bfc4ddd0640247544ec111ec83bd9bbe9104 Reviewed-on: https://go-review.googlesource.com/c/go/+/746800 Reviewed-by: Nicholas Husin Auto-Submit: Emmanuel Odeke Reviewed-by: Damien Neil Reviewed-by: Nicholas Husin LUCI-TryBot-Result: Go LUCI Auto-Submit: Damien Neil --- api/next/73450.txt | 1 + doc/next/6-stdlib/99-minor/net/url/73450.md | 1 + src/net/http/clone.go | 11 +-- src/net/url/url.go | 13 ++++ src/net/url/url_test.go | 76 +++++++++++++++++++++ 5 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 api/next/73450.txt create mode 100644 doc/next/6-stdlib/99-minor/net/url/73450.md diff --git a/api/next/73450.txt b/api/next/73450.txt new file mode 100644 index 0000000000..c4b2dc2536 --- /dev/null +++ b/api/next/73450.txt @@ -0,0 +1 @@ +pkg net/url, method (*URL) Clone() *URL #73450 diff --git a/doc/next/6-stdlib/99-minor/net/url/73450.md b/doc/next/6-stdlib/99-minor/net/url/73450.md new file mode 100644 index 0000000000..848dc90510 --- /dev/null +++ b/doc/next/6-stdlib/99-minor/net/url/73450.md @@ -0,0 +1 @@ +The new [URL.Clone] method creates a deep copy of a URL. diff --git a/src/net/http/clone.go b/src/net/http/clone.go index 0c2daf8552..7ea353a42f 100644 --- a/src/net/http/clone.go +++ b/src/net/http/clone.go @@ -39,16 +39,7 @@ func cloneURLValues(v url.Values) url.Values { // //go:linkname cloneURL func cloneURL(u *url.URL) *url.URL { - if u == nil { - return nil - } - u2 := new(url.URL) - *u2 = *u - if u.User != nil { - u2.User = new(url.Userinfo) - *u2.User = *u.User - } - return u2 + return u.Clone() } // cloneMultipartForm should be an internal detail, diff --git a/src/net/url/url.go b/src/net/url/url.go index 202957a3a2..6f0e1efca1 100644 --- a/src/net/url/url.go +++ b/src/net/url/url.go @@ -1322,3 +1322,16 @@ func JoinPath(base string, elem ...string) (result string, err error) { } return res.String(), nil } + +// Clone creates a deep copy of the fields of the subject [URL]. +func (u *URL) Clone() *URL { + if u == nil { + return nil + } + + uc := new(*u) + if u.User != nil { + uc.User = new(*u.User) + } + return uc +} diff --git a/src/net/url/url_test.go b/src/net/url/url_test.go index 9ba2a1231d..1645c9a882 100644 --- a/src/net/url/url_test.go +++ b/src/net/url/url_test.go @@ -10,6 +10,7 @@ import ( "encoding/gob" "encoding/json" "fmt" + "internal/diff" "io" "net" "reflect" @@ -2361,3 +2362,78 @@ func TestParseStrictIpv6(t *testing.T) { } } + +func TestURLClone(t *testing.T) { + tests := []struct { + name string + in *URL + }{ + {"nil", nil}, + {"zero value", &URL{}}, + { + "Populated but nil .User", + &URL{ + User: nil, + Host: "foo", + Path: "/path", + RawQuery: "a=b", + }, + }, + { + "non-nil .User", + &URL{ + User: User("user"), + Host: "foo", + Path: "/path", + RawQuery: "a=b", + }, + }, + { + "non-nil .User: user and password set", + &URL{ + User: UserPassword("user", "password"), + Host: "foo", + Path: "/path", + RawQuery: "a=b", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 1. The cloned URL must always deep equal the input, but never the same pointer. + cloned := tt.in.Clone() + if !reflect.DeepEqual(tt.in, cloned) { + t.Fatalf("Differing values\n%s", + diff.Diff("original", []byte(tt.in.String()), "cloned", []byte(cloned.String()))) + } + if tt.in == nil { + return + } + + // Ensure that their pointer values are not the same. + if tt.in == cloned { + t.Fatalf("URL: same pointer returned: %p", cloned) + } + + // 2. Test out malleability of URL fields. + cloned.Scheme = "https" + if cloned.Scheme == tt.in.Scheme { + t.Error("Inconsistent state: cloned.scheme changed and reflected in the input's scheme") + } + if reflect.DeepEqual(tt.in, cloned) { + t.Fatal("Inconsistent state: cloned and input are somehow the same") + } + + // 3. Ensure that the .User object deep equals but not the same pointer. + if !reflect.DeepEqual(tt.in.User, cloned.User) { + t.Fatalf("Differing .User\n%s", + diff.Diff("original", []byte(tt.in.String()), "cloned", []byte(cloned.String()))) + } + bothNil := tt.in.User == nil && cloned.User == nil + if !bothNil && tt.in.User == cloned.User { + t.Fatalf(".User: same pointer returned: %p", cloned.User) + } + }) + } +}