net/url: add Values.Clone

This change implements a method Clone on Values
that creates a deep copy of all of the subject's
consistent values.

CL 746800 added URL.Clone and this one therefore closes
out the feature.

Fixes #73450

Change-Id: I6fb95091c856e43063ab641c03034e1faaff8ed6
Reviewed-on: https://go-review.googlesource.com/c/go/+/746801
Reviewed-by: Nicholas Husin <husin@google.com>
Reviewed-by: Sean Liao <sean@liao.dev>
Auto-Submit: Emmanuel Odeke <emmanuel@orijtech.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
Reviewed-by: Nicholas Husin <nsh@golang.org>
This commit is contained in:
Emmanuel T Odeke
2026-02-18 18:12:01 -05:00
committed by Gopher Robot
parent 6c083034f8
commit 0359353574
4 changed files with 107 additions and 0 deletions

View File

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

View File

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

View File

@@ -915,6 +915,19 @@ func (v Values) Has(key string) bool {
return ok
}
// Clone creates a deep copy of the subject [Values].
func (vs Values) Clone() Values {
if vs == nil {
return nil
}
newVals := make(Values, len(vs))
for k, v := range vs {
newVals[k] = slices.Clone(v)
}
return newVals
}
// ParseQuery parses the URL-encoded query string and returns
// a map listing the values specified for each key.
// ParseQuery always returns a non-nil map containing all the

View File

@@ -12,8 +12,10 @@ import (
"fmt"
"internal/diff"
"io"
"maps"
"net"
"reflect"
"slices"
"strconv"
"strings"
"testing"
@@ -2443,3 +2445,93 @@ func TestURLClone(t *testing.T) {
})
}
}
func TestValuesClone(t *testing.T) {
tests := []struct {
name string
in Values
}{
{"nil", nil},
{"empty", Values{}},
{"1 key, nil values", Values{"1": nil}},
{"1 key, no values", Values{"1": {}}},
{"1 key, some values", Values{"1": {"a", "b"}}},
{"multiple keys, diverse values", Values{"1": {"a", "b"}, "X": nil, "B": {"abcdefghi"}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// The cloned map must always deep equal the input.
cloned1 := tt.in.Clone()
if !reflect.DeepEqual(tt.in, cloned1) {
t.Fatal("reflect.DeepEqual failed")
}
if cloned1 == nil && tt.in == nil {
return
}
if len(cloned1) == 0 && len(tt.in) == 0 && (cloned1 == nil || tt.in == nil) {
t.Fatalf("Inconsistency: both have len=0, yet not both nil\nCloned: %#v\nOriginal: %#v\n", cloned1, tt.in)
}
// Test out malleability of values.
cloned1["XXXXXXXXXXX"] = []string{"a", "b"}
if reflect.DeepEqual(tt.in, cloned1) {
t.Fatal("Inconsistent state: cloned and input are somehow the same")
}
// Ensure that we can correctly invoke some methods like .Add
cloned2 := tt.in.Clone()
if !reflect.DeepEqual(tt.in, cloned2) {
t.Fatal("reflect.DeepEqual failed")
}
cloned2.Add("a", "A")
if !cloned2.Has("a") {
t.Error("Cloned doesn't have the desired key: a")
}
if !cloned2.Has("a") {
t.Error("Cloned doesn't have the desired key: a")
}
// Assert that any changes to the clone did not change the original.
if reflect.DeepEqual(tt.in, cloned2) {
t.Fatal("reflect.DeepEqual unexpectedly passed after modify cloned")
}
cloned2.Del("a")
// Assert that reverting the clone's changes bring it back to original state.
if !reflect.DeepEqual(tt.in, cloned2) {
t.Fatal("reflect.DeepEqual failed")
}
cloned3 := tt.in.Clone()
clonedKeys := slices.Collect(maps.Keys(cloned3))
if len(clonedKeys) == 0 {
return
}
key0 := clonedKeys[0]
// Test modifying the actual slice.
if len(cloned3[key0]) == 0 {
cloned3[key0] = append(cloned3[key0], "golang")
} else {
cloned3[key0][0] = "directly modified"
if got, want := cloned3.Get(key0), "directly modified"; got != want {
t.Errorf("Get failed:\n\tGot: %q\n\tWant: %q", got, want)
}
}
if reflect.DeepEqual(tt.in, cloned3) {
t.Fatal("reflect.DeepEqual unexpectedly passed after modify cloned")
}
// Try out also with .Set.
cloned4 := tt.in.Clone()
if !reflect.DeepEqual(tt.in, cloned4) {
t.Fatal("reflect.DeepEqual failed")
}
cloned4.Set(key0, "good night")
if reflect.DeepEqual(tt.in, cloned4) {
t.Fatal("reflect.DeepEqual unexpectedly passed after modify cloned")
}
if got, want := cloned4.Get(key0), "good night"; got != want {
t.Errorf("Get failed:\n\tGot: %q\n\tWant: %q", got, want)
}
})
}
}