diff options
| author | Emmanuel T Odeke <emmanuel@orijtech.com> | 2026-02-18 18:12:01 -0500 |
|---|---|---|
| committer | Gopher Robot <gobot@golang.org> | 2026-03-06 10:48:32 -0800 |
| commit | 0359353574980629e42c73f7ed54397f7fdff321 (patch) | |
| tree | 8ad00198df175f4bdbf74a60f5398a0cceefc96f /src | |
| parent | 6c083034f82ddb2a91d3fbe0f96e39f1ecd194d8 (diff) | |
| download | go-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.go | 13 | ||||
| -rw-r--r-- | src/net/url/url_test.go | 92 |
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) + } + }) + } +} |
