diff --git a/api/next/73450.txt b/api/next/73450.txt index c4b2dc2536..4f3b65d2da 100644 --- a/api/next/73450.txt +++ b/api/next/73450.txt @@ -1 +1,2 @@ pkg net/url, method (*URL) Clone() *URL #73450 +pkg net/url, method (Values) Clone() Values #73450 diff --git a/doc/next/6-stdlib/99-minor/net/url/73450.md b/doc/next/6-stdlib/99-minor/net/url/73450.md index 848dc90510..f1480bad26 100644 --- a/doc/next/6-stdlib/99-minor/net/url/73450.md +++ b/doc/next/6-stdlib/99-minor/net/url/73450.md @@ -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. 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) + } + }) + } +}