aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorEmmanuel T Odeke <emmanuel@orijtech.com>2026-02-18 18:12:01 -0500
committerGopher Robot <gobot@golang.org>2026-03-06 10:48:32 -0800
commit0359353574980629e42c73f7ed54397f7fdff321 (patch)
tree8ad00198df175f4bdbf74a60f5398a0cceefc96f /src
parent6c083034f82ddb2a91d3fbe0f96e39f1ecd194d8 (diff)
downloadgo-0359353574980629e42c73f7ed54397f7fdff321.tar.xz
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>
Diffstat (limited to 'src')
-rw-r--r--src/net/url/url.go13
-rw-r--r--src/net/url/url_test.go92
2 files changed, 105 insertions, 0 deletions
diff --git a/src/net/url/url.go b/src/net/url/url.go
index a350686341..3c49f0527d 100644
--- a/src/net/url/url.go
+++ b/src/net/url/url.go
@@ -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
diff --git a/src/net/url/url_test.go b/src/net/url/url_test.go
index f58538bd0e..b048989b6c 100644
--- a/src/net/url/url_test.go
+++ b/src/net/url/url_test.go
@@ -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)
+ }
+ })
+ }
+}