skip to content

In Go, why prefer strings.EqualFold over comparing strings.ToLower of both sides?

level: middleimportance: should knowfreq 45%

answer

  1. one of them allocates two strings
  2. folding is a different operation from lowercasing
  3. some runes fold but do not lowercase
  4. think about where the comparison can stop early
  5. the long s folds to a plain s

basics

~20 s

strings.EqualFold compares under Unicode simple case folding, allocates nothing and stops at the first difference. Lowercasing both sides builds two throwaway strings and still misses folds such as the long s, which lowercases to itself but folds to a plain s.

solid answer

~40 s

`strings.EqualFold(s, t) bool` walks both strings rune by rune and compares them under **simple Unicode case folding**, which is more general than lowercasing: it treats U+017F, the long s, as equal to `"s"`, whereas `strings.ToLower("\u017f")` returns the long s unchanged so the lowercased comparison says they differ. It also allocates nothing and returns as soon as it finds a mismatch, while the `ToLower` form builds two complete new strings before comparing a single byte. What `EqualFold` is *not* is a general identity check: it does no Unicode normalisation, so a precomposed and a decomposed accented letter still compare unequal, and it applies no locale rules. Use it for one-off case-insensitive comparisons; when you need a stable key to store or index, you still lowercase once and keep that key.

code

go · 5 lines
go
fmt.Println(strings.EqualFold("\u017f", "s"))
// true

fmt.Println(strings.ToLower("\u017f") == "s")
// false

go deeper

for a junior

Know that ToUpper and ToLower return new strings and never change the original, and that EqualFold exists as the ready-made case-insensitive comparison rather than lowercasing both sides yourself.

for a middle

Explain that folding is a distinct Unicode operation with its own orbits, name a rune where it and lowercasing disagree, and point out the two throwaway allocations the ToLower comparison makes.

for a senior

Draw the line between a comparison and a canonical key: EqualFold for values you already hold, a lowercased and normalised form for anything you store or index. Say clearly what EqualFold does not cover.

for a principal

Own the canonicalisation policy across services — which transformation happens where, whether it is locale-independent by rule, and how a change to it is rolled out when existing stored keys were produced by the old rule.

## Casing in the strings package Go's case functions are `strings.ToUpper(s)`, `strings.ToLower(s)`, `strings.ToTitle(s)`, their `*Special` variants, and `strings.EqualFold(s, t)`. `ToUpper` and `ToLower` apply Unicode's simple case mappings to every rune, with no locale awareness whatsoever. That is a deliberate choice: the result is deterministic everywhere the program runs, which is what you want for a search key or a protocol token, and wrong for presenting text to a Turkish reader, where a dotless `ı` and a dotted `İ` are separate letters. For that case the package offers `strings.ToUpperSpecial(unicode.TurkishCase, s)` and `strings.ToLowerSpecial`, taking a `unicode.SpecialCase` — `unicode.TurkishCase` and `unicode.AzeriCase` are provided. `strings.ToTitle` is a common trap: it maps **every** rune to its title case, so it behaves like an uppercaser on most alphabets rather than capitalising word initials. ## Folding is not lowercasing Case folding is a separate Unicode operation whose purpose is caseless *matching*, not producing readable text. `strings.EqualFold` implements **simple** folding: for each pair of runes it walks the fold orbit (the same orbit exposed by `unicode.SimpleFold`) to see whether the two runes can reach each other. Those orbits contain members that a lowercase mapping does not produce. The clearest is the long s: `'\u017F'` folds together with `'S'` and `'s'`, so `strings.EqualFold("\u017f", "s")` is true — but `unicode.ToLower('\u017F')` is the long s itself, so `strings.ToLower("\u017f") == "s"` is false. Historic text, OCR output and pasted PDFs contain exactly these characters, so on a real corpus the two approaches disagree. Folding is also *simple*, meaning one rune maps to one rune. Full folding, which can expand one character into several, is not what `EqualFold` does, so ligatures and the sharp s do not compare equal to their multi-letter spellings. ## Why the ToLower comparison is also wasteful `strings.ToLower(a) == strings.ToLower(b)` allocates up to two fresh strings, transforms every rune of both — including the entire tails of two strings that differ in the first character — and only then compares. `EqualFold` streams: it decodes runes from both inputs in lockstep and returns false at the first position where the folds differ. For a hot path comparing a header value or a field name against a small fixed set, that difference is measurable, and it is free to get right. ## What EqualFold will not do for you Case-insensitivity is only one axis of "the same text". `EqualFold` does no **normalisation**, so a precomposed `é` (U+00E9) and an `e` followed by a combining acute (U+0065 U+0301) are different strings and stay different — normalising is a separate step, taken before any comparison. It knows nothing about locale, about visually confusable characters from different scripts, or about trailing whitespace. So it is a comparison for protocol tokens, header names, enum-like values and user-facing search matching — not by itself an authorisation or identity decision. ## Canonical keys versus one-off comparisons There is a practical split worth stating explicitly. If you are **comparing two values you already hold**, `EqualFold` is the right call. If you are producing a value to **store, index or hash** — a search key, a map key, a deduplication token — you must materialise a canonical form, and that means lowercasing once with `ToLower` (after normalising and trimming) and keeping the result. Case folding gives you an equivalence relation, not a representative element, so you cannot build a map key out of it directly. ## strings.Title and its deprecation `strings.Title` capitalises the first letter of each word, and it has been deprecated since Go 1.18. Its word-boundary rule treats any non-letter as a boundary, so `"don't"` becomes `"Don'T"`, and it mishandles Unicode punctuation more generally. The deprecation notice points at `golang.org/x/text/cases`, whose title caser takes a language tag and therefore knows the rules of the language it is applied to. `strings.ToTitle` is *not* the replacement — it title-cases every rune. If a linter flags a `strings.Title` call, the fix is a language-aware caser, or, quite often, dropping the transformation entirely and letting the presentation layer style the text.

  • What replaced strings.Title, and why is strings.ToTitle not the answer?
    `strings.Title` was deprecated in Go 1.18 because its word-boundary rule mishandles apostrophes and Unicode punctuation — `"don't"` comes out as `"Don'T"`. The documented replacement is the title caser in `golang.org/x/text/cases`, which takes a language tag. `strings.ToTitle` is a different function entirely: it maps every rune to title case, so it behaves like an uppercaser.
  • Is strings.EqualFold enough to decide that two account names refer to the same account?
    No. It handles case only. It applies no Unicode normalisation, so precomposed and decomposed accents still differ; it applies no locale rules; and it does nothing about visually confusable characters from other scripts. An identity decision needs an explicit canonicalisation policy, with EqualFold at most one step inside it.
  • How would you case-fold a Turkish dotted capital I correctly?
    Not with `strings.ToLower`, which is deliberately locale-independent. Use `strings.ToLowerSpecial(unicode.TurkishCase, s)` or `strings.ToUpperSpecial(unicode.TurkishCase, s)`, which apply that language's mappings. Reserve those for text being shown to a reader; keys and protocol tokens should stay on the locale-independent functions so they mean the same thing on every machine.

saying these in an interview costs you the question

  • Thinks EqualFold is just ToLower on both sides
  • Believes strings.ToLower applies the user's locale rules
  • Uses strings.ToTitle expecting word-initial capitals
  • Calls strings.Title in new code without noticing the deprecation
  • Treats a case-insensitive match as proof two identities are the same
  • Builds a map key out of a fold rather than a lowercased form