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 AZ, 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.

Go signatur.go
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("ПРИВЕТ"))
}
Output
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 (AZ) 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.

Go ascii_vs_unicode.go
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)
}
Output
already lower-mixed
true

Die 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.

Go turkish.go
package main

import (
	"fmt"
	"strings"
	"unicode"
)

func main() {
	s := "İSTANBUL"

	fmt.Println(strings.ToLower(s))
	fmt.Println(strings.ToLowerSpecial(unicode.TurkishCase, s))
}
Output
i̇stanbul
istanbul

ToLower 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.

Go rune_level.go
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"))
}
Output
äpfel
äpfel

Faustregel: 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.

Go equalfold.go
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))
}
Output
true
true

EqualFold 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.

Go header_norm.go
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"))
}
Output
application/json
Bearer abc

Die 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.

Go search.go
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"))
}
Output
true
true
false

Wer 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

/ Weiter

Zurück zu Das strings-Paket — String-Manipulation

Zur Übersicht