Go Rejected This Method for Five Years — Its Own Team Had Been Using It for Six
A method Russ Cox rejected in 2020 -- but running inside Go's own standard library since 2019
Golang
Go Rejected This Method for Five Years — Its Own Team Had Been Using It for Six
A method Russ Cox rejected in 2020 — but running inside Go’s own standard library since 2019
Go 1.27.0 shipped yesterday, August 19, 2026. Buried in the release notes is one line most people will skim past: net/url gained URL.Clone and Values.Clone. I benchmarked it before writing anything else:
BenchmarkURL_ShallowCopy-10 18.4 ns/op 144 B/op 1 allocs/op
BenchmarkURL_ParseString-10 190.2 ns/op 288 B/op 4 allocs/op
BenchmarkURL_Clone-10 26.6 ns/op 192 B/op 2 allocs/op(go1.27rc2 darwin/arm64, Apple M5, -benchmem -count 5, variance under 5%.)
Clone is 7x faster than the round-trip most people reach for, Parse(u.String()). That part isn't surprising — a copy operation shouldn't have to go through an encoder and a parser.
What’s surprising is Go’s team sat on this for five years before shipping something this obvious.
A proposal rejected on the spot
In October 2020, someone proposed a Clone method for URL in golang/go#41733. The reasoning was simple: URL carries a *Userinfo pointer field, and a plain u2 := *u shallow copy leaves nobody quite sure whether Userinfo ends up shared between the two.
Russ Cox pushed back at once:
I’m confused about this. This operation would apply to many structs where you want to make a change to one field to produce a new copy. This is a common idiom in Go. Why is URL special? Why does it merit a special method? … I have not seen it come up often at all, which would warrant not adding a helper.
The proposer had no usage data, only his own coding habits as evidence. Less than two weeks later, he closed the issue himself and settled for something more modest: just document u2 := *u is safe (#38351).
A normal story would end there. Proposal rejected, everyone moves on.
Six years of quiet internal use
net/http has a file called clone.go that dates back to 2019, a year before the proposal existed. It holds an unexported function, cloneURL, used internally by Request.Clone, Transport.Clone, and a handful of other call sites:
func cloneURL(u *url.URL) *url.URL {
if u == nil {
return nil
}
u2 := new(url.URL)
*u2 = *u
if u.User != nil {
u2.User = new(url.Userinfo)
*u2.User = *u.User
}
return u2
}Same logic as the URL.Clone that shipped five years later: shallow-copy the struct, deep-copy User on its own.
The Go team needed this more than anyone; they just never planned to expose it. The comment above the function is almost embarrassing to read:
// cloneURL should be an internal detail,
// but widely used packages access it using linkname.
// Notable members of the hall of shame include:
// - github.com/searKing/golang
//
// Do not remove or change the type signature.
// See go.dev/issue/67401.Translation: this was supposed to stay internal, but third-party packages were already reaching in with //go:linkname to steal it — github.com/searKing/golang gets named on the "hall of shame" for doing just that. When the public API never comes, people find a way around it.
A method the maintainers used every day, that outsiders were already prying open with black magic, and the one thing missing was a front door. That’s the part of this release note actually worth stopping on.

Data flips the verdict
In April 2025, Sean Liao reopened the proposal as golang/go#73450. This time he skipped the argument and went straight to a GitHub code search:
url2 := *url1-style shallow copies: 21,200 hitsurl2, _ = url.Parse(url1.String())-style round trips: 3,300 hits
One in eight users were paying for an encode-and-parse round trip just to copy a value, on top of handling an error return that should never fire. earthboundkid added the closer: http.Header already has .Clone(), and url.Values is the exact same underlying type — there was never a reason it shouldn't.
neild later laid out the sharper edge of the argument:
There are no*Userinfomethods which mutate the value (andUserinfois documented as immutable), but a user could modify a URL'sUserfield with something like*u2.User = *url.User("username"). We generally recommend not worrying about this possibility (nobody should assign to a*Userinfoin this fashion) and recommend deep-copying aURLwith a simple struct copy.
Nothing about *Userinfo can mutate it from the outside, and the docs say as much. But the language itself can't stop someone from writing *u2.User = *url.User("username") anyway. Deep-copying Userinfo on Clone is cheap insurance against a case nobody expects to hit.
On February 9, 2026, the proposal moved to likely accept. On February 18, it was formally accepted, with two CLs (746800, 746801) landing the same day. Six months later, Go 1.27.0 shipped and the method went public.
“I think it’s common” didn’t move the review group. “I searched, and it’s 21,200 hits” did. That’s arguably the more valuable outcome of this whole reversal — not Clone itself, but the evidence bar it took to get there.
Inside the implementation: a new(expr) cameo
The final URL.Clone reads like this:
func (u *URL) Clone() *URL {
if u == nil {
return nil
}
uc := new(*u)
if u.User != nil {
uc.User = new(*u.User)
}
return uc
}new(*u) isn't a typo. It's new(expr), the syntax Go 1.26 shipped — new used to accept only a type; now it can take an expression directly and return a pointer to a copy of it. The old way needed two lines:
uc := new(URL)
*uc = *uNow it’s one. I wrote about new(expr) after Go 1.26 shipped it. This is the first time I've watched it show up in the standard library doing actual work.
Values.Clone cuts an even bigger corner:
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
}net/http's internal version doesn't even bother rewriting this much. It just type-converts and reuses http.Header.Clone:
func cloneURLValues(v url.Values) url.Values {
return url.Values(Header(v).Clone())
}url.Values and http.Header are both map[string][]string under the hood. Between identically-shaped types, one conversion is enough to borrow an implementation that already exists.
Three old workarounds, ranked
Back to the question that matters: before Clone, what did people do?
Shallow copy, u2 := *u. Technically safe: every field on URL except User is a value type. Nobody was ever quite sure, though. Userinfo being a pointer is enough to make people second-guess whether it's shared.
Round trip, url.Parse(u.String()). Safe by construction, at the cost of an encode and a parse, plus an error return that almost never fires.
Encode round trip (the Values-only version), url.ParseQuery(v.Encode()). Same round-trip cost, and Values is a map[string][]string — a plain assignment only copies the map reference, leaving the underlying slices shared. That bug is quieter than the URL one, and easier to miss.
Benchmarked on the same machine:
BenchmarkValues_ManualDeepCopy-10 129.5 ns/op 512 B/op 6 allocs/op
BenchmarkValues_EncodeParseQuery-10 475.0 ns/op 808 B/op 16 allocs/op
BenchmarkValues_Clone-10 146.9 ns/op 512 B/op 6 allocs/op
Values.Clone adds only 13% over a correctly-written manual deep copy, mostly the method call and nil check, and it's 3.2x faster than the encode round trip.
URL.Clone is the better deal of the two. It costs a few nanoseconds more than the shallow copy nobody trusted, the price of one extra allocation for Userinfo, and in exchange you stop having to reason about whether the copy is safe.
When Clone earns its keep
Keep a base URL in config and clone it per request to change the Path. Write a middleware or SDK that hands out a *url.URL without wanting callers to mutate the copy you're still holding. Those are the cases where Clone is worth reaching for.
Mutating a URL field on a single goroutine was already safe. That's Go's general rule: if you own the value, you can change it, and URL was never a special case. That's the point Russ Cox pushed five years ago. What Clone fixes is unclear semantics and round-trip cost, not "can I mutate this?"
This reversal happened because someone was willing to spend an afternoon running a GitHub search, not because Clone is a particularly clever method.
“I think it’s common” doesn’t convince the Go team. “I searched, and here’s the number” does.