From 0359353574980629e42c73f7ed54397f7fdff321 Mon Sep 17 00:00:00 2001 From: Emmanuel T Odeke Date: Wed, 18 Feb 2026 18:12:01 -0500 Subject: 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 Reviewed-by: Sean Liao Auto-Submit: Emmanuel Odeke LUCI-TryBot-Result: Go LUCI Reviewed-by: Cherry Mui Reviewed-by: Nicholas Husin --- src/net/url/url.go | 13 +++++++ src/net/url/url_test.go | 92 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) (limited to 'src') 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) + } + }) + } +} -- cgit v1.3-6-g1900