strings.ToLower liefert eine Kopie der Eingabe, in der alle Buchstaben auf ihr Kleinbuchstaben-Äquivalent abgebildet werden. Die Funktion arbeitet Unicode-aware: sie kennt nicht nur A–Z, sondern auch Akzentbuchstaben, kyrillische Großbuchstaben, griechische Versalien und tausende weitere Codepoints, deren Default-Casing in den Unicode Character Database-Tabellen hinterlegt ist.
Trotz ihrer scheinbaren Trivialität ist ToLower an zwei Stellen subtil: sie folgt Unicode-Default-Casing und ignoriert damit sprachspezifische Regeln (das berühmte türkische Punkt-I), und sie alloziert nur dann eine neue Zeichenkette, wenn tatsächlich ein Codepoint geändert werden muss. Wer diese beiden Punkte verinnerlicht, vermeidet Bugs in Authentifizierung, Suche und Datennormalisierung.
Die Signatur ist denkbar schlicht: func ToLower(s string) string. Es gibt keinen Fehler-Rückgabewert, keinen error, keine bool-Statusflagge — die Operation kann auf jeder gültigen UTF-8-Zeichenkette ausgeführt werden. Strings sind in Go unveränderlich, deshalb gibt ToLower immer eine neue (oder die identische) Zeichenkette zurück und mutiert das Argument nicht.
Wichtig zu wissen: ToLower interpretiert die Eingabe als UTF-8. Bytes, die keine gültige UTF-8-Sequenz bilden, werden als Replacement-Runes (U+FFFD) behandelt — die Funktion crasht also nicht, „repariert" aber auch nichts. Wer mit potenziell ungültigem UTF-8 arbeitet, sollte vorher mit utf8.ValidString prüfen oder durch strings.ToValidUTF8 säubern.
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ToLower("Hello, WORLD!"))
fmt.Println(strings.ToLower("ÄÖÜ ß"))
fmt.Println(strings.ToLower("ΓΕΙΆ ΣΟΥ"))
fmt.Println(strings.ToLower("ПРИВЕТ"))
}hello, world!
äöü ß
γειά σου
приветAuffällig: das ß bleibt ß. Es existiert zwar inzwischen ein Großbuchstabe ẞ (U+1E9E), aber ß selbst hat keine kleinere Form — es ist bereits ein Kleinbuchstabe.
Intern besitzt ToLower einen Fast-Path für reine ASCII-Strings. Das Paket prüft beim Iterieren, ob alle Bytes unterhalb von 0x80 liegen und ob mindestens ein Großbuchstabe (A–Z) auftritt. Ist die Eingabe bereits klein und rein ASCII, gibt die Funktion denselben String zurück — ohne Allokation.
Sobald ein Multi-Byte-Codepoint auftritt, schaltet die Implementation auf den allgemeinen Pfad um: sie dekodiert Rune für Rune mit utf8.DecodeRuneInString, ruft unicode.ToLower auf der einzelnen Rune auf und schreibt das Ergebnis in einen strings.Builder. Das ist messbar teurer, aber für Internationalisierung notwendig.
package main
import (
"fmt"
"strings"
)
func main() {
a := "ALREADY lower-mixed"
b := strings.ToLower(a)
fmt.Println(b)
// Reine Kleinschrift -> identische Rückgabe
c := "alles klein"
d := strings.ToLower(c)
fmt.Println(c == d)
}already lower-mixed
trueDie Praxis-Konsequenz: Aufrufe auf bereits normalisierten Daten kosten quasi nichts, ein blindes ToLower in einem Hot Path ist also selten ein Performance-Problem — solange die Eingabe ASCII ist.
Im Türkischen sind i und I nicht ein Paar. Stattdessen gilt: kleines i → großes İ (mit Punkt), kleines ı (ohne Punkt) → großes I. Das Default-Casing von Unicode kennt diese Regel nicht — es bildet I schlicht auf i ab. Genau das tut auch strings.ToLower.
Wer korrektes türkisches Casing braucht, nutzt strings.ToLowerSpecial(unicode.TurkishCase, s). Diese Variante akzeptiert eine unicode.SpecialCase-Tabelle und wendet sprachspezifische Mappings an.
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
s := "İSTANBUL"
fmt.Println(strings.ToLower(s))
fmt.Println(strings.ToLowerSpecial(unicode.TurkishCase, s))
}i̇stanbul
istanbulToLower zerlegt İ in i plus Combining-Dot — sichtbar bleibt der zusätzliche Punkt. ToLowerSpecial mit TurkishCase liefert das erwartete saubere istanbul. Für ein deutsches Backend ohne Locale-Anspruch ist ToLower ausreichend, für Lokalisierung sollte die Sprache explizit gewählt werden.
ToLower alloziert konservativ. Wenn keine Rune verändert werden muss, gibt die Funktion das Original zurück — Header-Zeiger und Länge sind identisch, kein Byte wird kopiert. Das ist über die strings-Bibliothek hinweg eine durchgängige Konvention: Operationen, die nichts ändern, kosten auch nichts.
Sobald ein einziger Buchstabe konvertiert werden muss, wird ein neuer Buffer mit etwa der Länge des Originals allokiert. Bei Strings mit Sonderzeichen, deren Klein- und Großform unterschiedliche Byte-Längen haben, wächst der Buffer dynamisch — relevant z. B. für İ → i + Combining-Dot.
Für Hochlast-Pfade gilt: wenn beide Seiten eines Vergleichs ohnehin ToLower-normalisiert würden, ist strings.EqualFold schneller und allokationsfrei.
strings.ToLower arbeitet auf einer ganzen Zeichenkette, unicode.ToLower auf einer einzelnen Rune. Beide nutzen dieselben Casing-Tabellen, aber sie sind nicht austauschbar: wer in einer Schleife über Bytes iteriert und unicode.ToLower auf jedes Byte anwendet, zerstört Multi-Byte-Codepoints. Korrekte Rune-für-Rune-Verarbeitung braucht for _, r := range s — und in diesem Pfad ist unicode.ToLower(r) das richtige Werkzeug.
package main
import (
"fmt"
"strings"
"unicode"
)
func main() {
var b strings.Builder
for _, r := range "ÄPFEL" {
b.WriteRune(unicode.ToLower(r))
}
fmt.Println(b.String())
// Identisches Ergebnis, aber als One-Shot
fmt.Println(strings.ToLower("ÄPFEL"))
}äpfel
äpfelFaustregel: wenn ohnehin Rune-für-Rune verarbeitet wird (z. B. Filtern, Mappen, Validieren), nimm unicode.ToLower. Wenn die gesamte Zeichenkette in einem Rutsch konvertiert werden soll, nimm strings.ToLower.
Ein klassisches Antipattern ist strings.ToLower(a) == strings.ToLower(b). Das funktioniert, ist aber doppelt teuer: zwei Allokationen plus ein Byte-Vergleich. Go bietet dafür strings.EqualFold(a, b), das beide Strings Rune-für-Rune mit Unicode-Case-Folding vergleicht — ohne Zwischenkopien.
package main
import (
"fmt"
"strings"
)
func main() {
a, b := "Müller", "MÜLLER"
// Antipattern
fmt.Println(strings.ToLower(a) == strings.ToLower(b))
// Idiomatisch
fmt.Println(strings.EqualFold(a, b))
}true
trueEqualFold ist nicht nur schneller, sondern semantisch präziser: es nutzt Simple Case Folding, das z. B. ß und ss korrekt als gleichwertig erkennt — eine Eigenschaft, die ToLower allein nicht bietet.
HTTP-Header sind laut RFC 7230 case-insensitive. Wer eigene Header-Maps baut (z. B. in einem Reverse-Proxy oder einem Mock-Server), normalisiert die Keys typischerweise via ToLower, um sie deterministisch nachzuschlagen.
package main
import (
"fmt"
"strings"
)
type HeaderBag map[string]string
func (h HeaderBag) Set(key, val string) {
h[strings.ToLower(key)] = val
}
func (h HeaderBag) Get(key string) string {
return h[strings.ToLower(key)]
}
func main() {
h := HeaderBag{}
h.Set("Content-Type", "application/json")
h.Set("AUTHORIZATION", "Bearer abc")
fmt.Println(h.Get("content-type"))
fmt.Println(h.Get("Authorization"))
}application/json
Bearer abcDie Standardbibliothek selbst nutzt für net/http allerdings textproto.CanonicalMIMEHeaderKey, das Content-Type statt content-type produziert. Für eigene Strukturen ist ToLower aber die einfachste und robusteste Wahl.
Bei einer einfachen Substring-Suche über kleine Datenmengen ist es bequem, sowohl Haystack als auch Needle einmalig in Kleinschrift zu überführen und dann strings.Contains zu nutzen. Für große Datenmengen lohnt sich der Wechsel auf Indexstrukturen — aber für ein CLI-Tool, das Logs durchsucht, reicht das Muster vollkommen.
package main
import (
"fmt"
"strings"
)
func containsCI(haystack, needle string) bool {
return strings.Contains(
strings.ToLower(haystack),
strings.ToLower(needle),
)
}
func main() {
log := "2026-05-25 ERROR: Disk full on /var/log"
fmt.Println(containsCI(log, "error"))
fmt.Println(containsCI(log, "DISK"))
fmt.Println(containsCI(log, "warning"))
}true
true
falseWer denselben Haystack mehrfach durchsucht, normalisiert ihn einmal und cached die Kleinschrift-Variante. Wer den Needle mehrfach gegen viele Haystacks prüft, sollte stattdessen einen strings.NewReplacer oder eine regex-basierte Lösung evaluieren.
Default-Casing
strings.ToLower nutzt Unicode-Default-Casing — sprachunabhängig, deterministisch, aber blind für türkisches İ/ı.
No-Op = Original-String
Ist die Eingabe bereits klein, gibt ToLower denselben Backing-Pointer zurück — keine Allokation.
ToLowerSpecial für Locales
Für sprachspezifisches Casing nimmt man strings.ToLowerSpecial(unicode.TurkishCase, s) oder eine eigene SpecialCase-Tabelle.
unicode.ToLower auf Runes
Bei Rune-für-Rune-Verarbeitung ist unicode.ToLower(r) das passende Werkzeug — nicht strings.ToLower auf Einzelbytes.
EqualFold schlägt ToLower-Vergleich
Für case-insensitive Gleichheit ist strings.EqualFold(a, b) schneller und allokationsfrei.
UTF-8-Annahme
Ungültiges UTF-8 wird als U+FFFD behandelt — ToLower validiert nicht, also vorher utf8.ValidString aufrufen, wenn die Quelle unsicher ist.
ß bleibt ß
Das ß hat keine kleinere Form; ToLower lässt es unverändert. Großschreibung via ToUpper kann je nach Go-Version ẞ oder SS liefern — getrennt prüfen.
ASCII-Fast-Path
Reine ASCII-Eingaben durchlaufen einen Byte-für-Byte-Pfad ohne UTF-8-Dekodierung — der typische Hot-Path ist günstig.
Weiterführende Ressourcen
Externe Quellen
strings.ToLowerstrings.ToLowerSpecialstrings.EqualFoldunicode.ToLower- Unicode Technical Standard #21 — Case Mappings