> ## Content Index
> Fetch the complete content index at: https://huizhou92.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Why Go Took 15 Months to Add 8 Lines of Code
- URL: https://huizhou92.com/why-go-took-15-months-to-add-8-lines-of-code/
- Published: 2026-04-18T02:56:56.000Z
- Updated: 2026-09-08T02:14:31.000Z
- Description: The strings.CutLast Proposal Reveals how Go Thinks about Stdlib API Design — Permanent Decisions, Family Consistency, and why Real-world…
- Author: huizhou92
- Tags: #Migrated-1788833207488, #Import 2026-09-08 02:07

#### GO PROPOSAL!

### Why Go Took 15 Months to Add 6 Lines of Code

#### **The `strings.CutLast` Proposal Reveals how Go Thinks about Stdlib API Design — Permanent Decisions, Family Consistency, and why Real-world Evidence Beats Elegant Intuition.**

Here’s a function you’ve probably written yourself:

```go
func cutLast(s, sep string) (before, after string, found bool) { 
    if i := strings.LastIndex(s, sep); i >= 0 { 
        return s[:i], s[i+len(sep):], true 
    } 
    return "", s, false 
}
```

Straightforward. Eight lines. Roger Peppe found five independent copies scattered across his own codebase, then discovered every single one was subtly wrong.

He said so in a GitHub comment: *“I was actually using it wrong in all these cases!”* That admission ended a 15-month debate about how `strings.CutLast` should behave when the separator isn't found.

So why does 8 lines of code take 15 months in Go? What’s actually going on in those discussions?

#### **The Proposal: a Function Everyone Already Wrote**

In January 2025, @chipaca opened [issue #71151](https://github.com/golang/go/issues/71151?ref=huizhou92.com) proposing `strings.CutLast` a complement to `strings.Cut` those searches from the right instead of the left.

`strings.Cut` was a 2021 addition, championed by Russ Cox as a single function to replace the overwhelming majority of `Index`, `IndexByte`, `IndexRune`, and `SplitN` usage ([#46336](https://github.com/golang/go/issues/46336?ref=huizhou92.com)):

```go
// Instead of: 
i := strings.Index(s, ":") 
if i >= 0 { 
    user, pass = s[:i], s[i+1:] 
} 
  
// Just: 
user, pass, _ := strings.Cut(s, ":")
```

`CutLast` extends this to the right-to-left case: parse a URL slug, extract a file extension, split a package URL at its last separator. The kind of operation you reach for every few months and always end up hand-rolling.

The CL that eventually landed — [CL 764601](https://go-review.googlesource.com/c/go/+/764601?ref=huizhou92.com), merged April 14, 2026 — is nearly identical to chipaca’s original proposal:

```go
// CutLast slices s around the last instance of sep, 
// returning the text before and after sep. 
// The found result reports whether sep appears in s. 
// If sep does not appear in s, CutLast returns s, "", false. 
func CutLast(s, sep string) (before, after string, found bool) { 
    if i := LastIndex(s, sep); i >= 0 { 
        return s[:i], s[i+len(sep):], true 
    } 
    return s, "", false 
}
```

Eight lines. Fifteen months.

#### **The First Debate: What Should Failure Return?**

When the separator isn’t found, should `CutLast` return `s, "", false` or `"", s, false`?

It sounds like a minor detail. It turned into months of back-and-forth.

@earthboundkid argued for `"", s, false` with a geometric case: as the separator moves leftward through the string, `after` grows. When there's no separator, `after` should get everything. There's logic there — `Cut` puts the original string in `before`, so `CutLast` should mirror it in `after`. A left-right symmetry.

@mateusz834 pushed back: file extensions. `CutLast("test", ".")` with no dot puts `"test"` in `ext`. That's almost never what you want.

Neither side gave ground. Then @rogpeppe ran an audit.

![Failure semantics: intuitive vs Go's choice, with rogpeppe's 5 wrong implementations](https://huizhou92.com/content/images/2026/09/1-7bbbcja0gflxipgwlon2vw-png.jpg)

He searched his own codebase for every call to `cutLast` that used the returned values without checking `found` first. He found cases where he'd relied on the `"", s, false` semantic. Everyone was broken — code that would quietly misbehave when the separator was absent, hidden by the "elegant" intuition.

> “I looked at my uses of cutLast where it was using the values regardless of the value of found and found that clearly my intuition was wrong: I was actually using it wrong in all these cases!” — @rogpeppe

Eight thumbs-up. The debate was over.

@adonovan put it precisely: `Cut` can't simultaneously guarantee "original string is always in the first position" and "Cut + CutLast are left-right inverses." One had to give. Real-world usage, not theoretical elegance, decided which one.

#### **The Second Debate: CutLast or LastCut?**

Names in a standard library are permanent. You don’t get to change them.

`LastCut` aligns with `LastIndex` — Cut and Index are paired throughout `strings`, so following that pattern seemed reasonable. @Merovius wrote a systematic counterargument by mapping the entire package into six function families:

- `ContainsX` — Contains, ContainsAny, ContainsRune, ContainsFunc
- `HasX` — HasPrefix, HasSuffix
- `CutX` — Cut, CutPrefix, CutSuffix
- `IndexX` — Index, IndexByte, IndexAny, IndexRune, IndexFunc
- `LastIndexX` — LastIndex, LastIndexByte, LastIndexAny, LastIndexFunc
- `TrimX` — Trim, TrimLeft, TrimRight, TrimSpace, ...

![strings package family matrix: where CutLast belongs](https://huizhou92.com/content/images/2026/09/1-xov_v8-efz7bs62va4qwtq-png.jpg)

The `Cut ↔ Index` pairing is an outlier, he argued. `CutLast` belongs in the `CutX` family alongside `CutPrefix` and `CutSuffix` ([#42537](https://github.com/golang/go/issues/42537?ref=huizhou92.com)) — and naming it that way leaves room for `CutAny`, `CutByte`, `CutLastAny`, whatever comes next.

> “I would argue CutLast is *actually more consistent*, as it emphasizes membership of the CutX family. LastCut seems consistent, because Cut and Index are in correspondence, but that correspondence is really an outlier.” — @Merovius

@aclements agreed. `CutLast` it is.

#### **Three Languages, Three Answers**

The same operation exists in other languages, and they all handle the not-found case differently.

**Rust’s `str::rsplit_once`:**

```rust
let s = "foo:bar:baz"; 
assert_eq!(s.rsplit_once(':'), Some(("foo:bar", "baz"))); 
assert_eq!(s.rsplit_once("::"), None);
```

Returns `Option<(&str, &str)>`. Not found means `None` — no default value, no original string, and the compiler won't let you ignore it.

**Python’s `str.rpartition`:**

```python
>>> 'Monty Python'.rpartition(' ') 
('Monty', ' ', 'Python') 
>>> 'Monty Python'.rpartition('-') 
('', '', 'Monty Python')
```

Not found: `('', '', original_string)`. The original string mirrors the search direction — `partition` puts it first, `rpartition` puts it last.

**Go’s `strings.CutLast`:** not found means `s, "", false`. The original is always in `before`, search direction be damned.

![Table Image](https://huizhou92.com/content/images/2026/09/1-vnvo55gmzai0ikd40styvq.png)

![Three languages, three answers to "not found": Go / Rust / Python](https://huizhou92.com/content/images/2026/09/1-ayjwynmgwn6zeesette6ng-png.jpg)

Python’s design is probably the most intuitive on first read. It’s also the design rogpeppe had in his head when he wrote five wrong implementations.

#### **The Accidental Early Merge**

In April 2026, the proposal still hadn’t been formally accepted when @griesemer accidentally marked the CL as auto-submittable. It merged on April 14.

@earthboundkid noticed. Griesemer apologized and made a call: leave it in. He figured the proposal would pass the next day. If it didn’t, they’d revert.

It passed on April 16\. No revert.

The code review itself was almost uneventful. The only substantive Gerrit comment was about semicolons vs. commas in test output — a formatting nitpick, for a change that took 15 months. All the real work had already happened in the issue tracker.

#### **Why 8 Lines Takes 15 Months**

Standard library APIs are permanent in a way that few things in software are. Not “permanent until someone files a deprecation PR” permanent — actually permanent. If `CutLast` had shipped with the `"", s, false` semantic, that's what it would return in Go 1.30, 1.40, and 1.50\. The rogpeppe bug pattern baked in. Billions of programs depending on it.

That’s why you pay the cost upfront. A month of argument in a GitHub issue is cheap compared to carrying a design mistake for twenty years.

The naming debate works the same way. `LastCut` and `CutLast` are both defensible in isolation. But the choice isn't just about this one function — it's about what the next person adding `CutAny` or `CutLastByte` will reach for. Get the family wrong, and the inconsistency compounds. Fix it later, and you're explaining the exception to developers for a decade.

I keep coming back to rogpeppe’s audit. He had more experience with this function than almost anyone, and he still got it wrong five times. The “intuitive” semantics were the ones hiding his bugs. Fifteen months to avoid that outcome seems like a reasonable trade.

`strings.CutLast` comes in Go 1.27\. By the time you use it, the version of the function in your head will probably be correct. That's not an accident.

---

### *References:*

- [golang/go#71151](https://github.com/golang/go/issues/71151?ref=huizhou92.com) — the proposal
- [CL 764601](https://go-review.googlesource.com/c/go/+/764601?ref=huizhou92.com) — the implementation
- [#46336](https://github.com/golang/go/issues/46336?ref=huizhou92.com) — the original Cut proposal
- [#42537](https://github.com/golang/go/issues/42537?ref=huizhou92.com) — CutPrefix and CutSuffix