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 <husin@google.com>
Auto-Submit: Emmanuel Odeke <emmanuel@orijtech.com>
Reviewed-by: Damien Neil <dneil@google.com>
Reviewed-by: Nicholas Husin <nsh@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Damien Neil <dneil@google.com>
This commit is contained in:
Emmanuel T Odeke
2026-02-18 17:03:37 -05:00
committed by Gopher Robot
parent 839cd82fa5
commit 09c3cfbc20
5 changed files with 92 additions and 10 deletions

1
api/next/73450.txt Normal file
View File

@@ -0,0 +1 @@
pkg net/url, method (*URL) Clone() *URL #73450

View File

@@ -0,0 +1 @@
The new [URL.Clone] method creates a deep copy of a URL.

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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)
}
})
}
}