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

分享
cover-medium-net-url-clone
creator generate

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.

From internal cloneURL in 2019, to a rejected proposal in 2020, to data-backed evidence in 2025, to Go 1.27.0 shipping it in 2026 — the same code took six years.
From internal cloneURL in 2019, to a rejected proposal in 2020, to data-backed evidence in 2025, to Go 1.27.0 shipping it in 2026 — the same code took six years.

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 hits
  • url2, _ = 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 *Userinfo methods which mutate the value (and Userinfo is documented as immutable), but a user could modify a URL's User field with something like *u2.User = *url.User("username"). We generally recommend not worrying about this possibility (nobody should assign to a *Userinfo in this fashion) and recommend deep-copying a URL with 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 = *u

Now 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
Same machine, same run: URL.Clone is 7x faster than Parse(String()), Values.Clone is 3.2x faster than Encode()+ParseQuery().
Same machine, same run: URL.Clone is 7x faster than Parse(String()), Values.Clone is 3.2x faster than Encode()+ParseQuery().

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.