Wer mehrere Substitutionen auf einen String anwenden will, greift reflexartig zu einer Kette aus strings.ReplaceAll-Aufrufen. Das ist verführerisch lesbar, hat aber zwei harte Nachteile: jeder Aufruf alloziert einen neuen String, und spätere Schritte können auf den Ergebnissen früherer Schritte „kreuz-feuern". Aus a → b gefolgt von b → c wird so unbemerkt ein zweischrittiges a → c.
Der Typ strings.Replacer löst beides. Er kompiliert die Liste aller Ersetzungen einmal vor — intern in eine Trie-/Automaten-Struktur — und läuft danach in einem einzigen Pass über den Input. Es gibt kein Zwischenergebnis, also auch kein Kreuz-Replacement; Allokationen entstehen nur einmal pro Replace-Aufruf, nicht pro Regel. Das Resultat ist threadsafe nutzbar, ideal als Paket-Variable für HTML-Escape, Slug-Erzeugung mit deutschen Umlauten oder leichtgewichtige Template-Substitution.
NewReplacer ist variadisch und erwartet die Ersetzungen als flache Paare aus altem und neuem String. Die Argumentliste muss eine gerade Länge haben — andernfalls löst die Funktion eine Panic aus, weil das letzte „alte" Token kein Gegenstück hätte. Diese Wahl ist bewusst hart: ein versehentlich vergessenes Argument wäre ein lautloser semantischer Fehler, der oft erst in Produktion auffiele.
package main
import (
"fmt"
"strings"
)
func main() {
r := strings.NewReplacer(
"Mo", "Montag",
"Di", "Dienstag",
"Mi", "Mittwoch",
)
fmt.Println(r.Replace("Mo, Di und Mi"))
}Montag, Dienstag und MittwochIm Beispiel werden drei Wochentag-Kürzel expandiert. Wichtig: der Replacer wird einmal gebaut und kann beliebig oft wiederverwendet werden — typischerweise als Paket-Variable, nicht in einer Hot-Loop neu konstruiert.
Replacer arbeitet von links nach rechts über den Input und versucht an jeder Position das längstmögliche passende Pattern aus seiner Regelliste. Bei mehreren gleich langen Treffern gewinnt die Regel, die im Konstruktor zuerst genannt wurde. Diese Regel ist deterministisch und macht den Output unabhängig von Map-Iterationsreihenfolge oder Hash-Zufall.
package main
import (
"fmt"
"strings"
)
func main() {
// Reihenfolge entscheidet bei Überlappung:
rA := strings.NewReplacer("ab", "X", "abc", "Y")
rB := strings.NewReplacer("abc", "Y", "ab", "X")
fmt.Println(rA.Replace("abcd")) // erste passende Regel zuerst
fmt.Println(rB.Replace("abcd"))
}Xcd
YdBei rA matcht ab zuerst, weil es als erste Regel deklariert wurde — c bleibt übrig. Bei rB greift erst das längere abc. Wer maximales Matchen will, sortiert die Regeln lang vor kurz.
Der Typ hat genau zwei öffentliche Methoden. Replace(s string) string gibt den vollständig substituierten String zurück und ist die typische Wahl, wenn das Resultat noch weiterverarbeitet wird. WriteString(w io.Writer, s string) (int, error) schreibt direkt in einen Writer und vermeidet die Zwischen-Allokation eines kompletten Result-Strings — relevant für HTTP-Handler, Log-Sinks oder beim Streamen in eine Datei.
package main
import (
"fmt"
"os"
"strings"
)
func main() {
r := strings.NewReplacer("Go", "Golang", "ist", "rockt")
// Variante 1: Replace gibt einen String zurück
out := r.Replace("Go ist gut")
fmt.Println(out)
// Variante 2: WriteString schreibt direkt in einen Writer
r.WriteString(os.Stdout, "Go ist schnell\n")
}Golang rockt gut
Golang rockt schnellReplace ist der bequeme Default. WriteString kommt dann ins Spiel, wenn das Ziel ohnehin ein io.Writer ist — der direkte Pfad spart eine String-Kopie und ggf. eine Heap-Allokation.
Eine naive Mehrfach-Ersetzung schreibt sich gerne als Kette: strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(s, ...), ...), ...). Jeder Aufruf läuft einmal vollständig über den Input und alloziert einen neuen String. Bei k Regeln und Input-Länge n entstehen so k Voll-Scans und k Allokationen — der Replacer schafft dasselbe in einem Scan und einer Allokation.
package main
import (
"fmt"
"strings"
)
func mitKette(s string) string {
s = strings.ReplaceAll(s, "Mo", "Montag")
s = strings.ReplaceAll(s, "Di", "Dienstag")
s = strings.ReplaceAll(s, "Mi", "Mittwoch")
return s
}
var tageReplacer = strings.NewReplacer(
"Mo", "Montag",
"Di", "Dienstag",
"Mi", "Mittwoch",
)
func mitReplacer(s string) string {
return tageReplacer.Replace(s)
}
func main() {
in := "Mo, Di und Mi"
fmt.Println(mitKette(in))
fmt.Println(mitReplacer(in))
}Montag, Dienstag und Mittwoch
Montag, Dienstag und MittwochFunktional liefern beide Varianten dasselbe — aber der Replacer ist sowohl schneller als auch semantisch sauberer, sobald die Regelmenge wächst oder Input und Ersatz sich potenziell überlappen können.
Das eigentliche Argument gegen ReplaceAll-Ketten ist nicht Performance, sondern Korrektheit. Sobald ein neuer Wert wieder einem alten Pattern entspricht, läuft die nächste Stufe ungewollt darüber. Ein klassischer Fall ist eine Rotations-Chiffre oder ein Tausch zweier Symbole.
package main
import (
"fmt"
"strings"
)
func main() {
// Ziel: a und b vertauschen.
in := "abab"
// BUG: Kette macht a -> b -> c und löscht b komplett.
out1 := strings.ReplaceAll(in, "a", "b")
out1 = strings.ReplaceAll(out1, "b", "a")
fmt.Println("Kette :", out1)
// Replacer: single-pass, kein Kreuz-Replacement.
r := strings.NewReplacer("a", "b", "b", "a")
fmt.Println("Replacer:", r.Replace(in))
}Kette : aaaa
Replacer: babaDie Kette verwandelt zuerst alle a in b, danach alle (jetzt vorhandenen) b zurück in a — das Ergebnis ist unbrauchbar. Der Replacer entscheidet pro Position einmal und bewegt sich dann weiter; vertauschen ist damit trivial korrekt.
Ein *strings.Replacer ist immutable und nach dem Bau threadsafe. Das macht ihn zum idealen Kandidaten für eine Paket-Variable, die einmalig beim Programmstart aufgebaut und danach von beliebig vielen Goroutinen ohne Lock verwendet wird. Die teure Kompilierung der internen Match-Struktur fällt nur einmal an, alle weiteren Replace-Calls profitieren davon.
package main
import (
"fmt"
"strings"
"sync"
)
// Einmal kompiliert, beliebig oft genutzt — auch nebenläufig.
var htmlEscape = strings.NewReplacer(
"&", "&",
"<", "<",
">", ">",
`"`, """,
"'", "'",
)
func main() {
var wg sync.WaitGroup
for i, in := range []string{`<b>x</b>`, `a & b`, `"zitat"`} {
wg.Add(1)
go func(idx int, s string) {
defer wg.Done()
fmt.Printf("[%d] %s\n", idx, htmlEscape.Replace(s))
}(i, in)
}
wg.Wait()
}[0] <b>x</b>
[1] a & b
[2] "zitat"Kein sync.Mutex, kein sync.Once — der Replacer trägt seine Thread-Sicherheit als Eigenschaft des Typs. Die Reihenfolge der Goroutine-Outputs kann variieren, das hat aber nichts mit dem Replacer zu tun.
WriteString ist die Methode für Pipelines, in denen das Ergebnis ohnehin in einen Stream wandert. Statt w.Write([]byte(r.Replace(s))) mit zwei Allokationen schreibt der Replacer Stück für Stück direkt in den Writer. Bei großen Templates oder hohem Durchsatz spart das messbar GC-Druck.
package main
import (
"bytes"
"fmt"
"strings"
)
func main() {
r := strings.NewReplacer("{{user}}", "Anna", "{{role}}", "Admin")
var buf bytes.Buffer
n, err := r.WriteString(&buf, "Hallo {{user}}, Rolle: {{role}}.")
if err != nil {
fmt.Println("Fehler:", err)
return
}
fmt.Printf("%d Bytes geschrieben\n", n)
fmt.Println(buf.String())
}30 Bytes geschrieben
Hallo Anna, Rolle: Admin.Der Rückgabewert n ist die Zahl der geschriebenen Bytes, nicht die Länge des Inputs — was bei Substitution unterschiedlich sein kann. In einem HTTP-Handler würde statt &buf direkt der http.ResponseWriter übergeben.
Der archetypische Anwendungsfall: User-Input für HTML-Kontext escapen. Die Standardbibliothek bietet html.EscapeString, aber wer aus Konfiguration oder mit eigenen Sonderregeln arbeitet, baut sich einen eigenen Replacer — als Paket-Variable, damit die Kompilierung exakt einmal stattfindet.
package main
import (
"fmt"
"strings"
)
// Vorkompiliert, threadsafe, ohne Lock nutzbar.
var htmlEscape = strings.NewReplacer(
"&", "&",
"<", "<",
">", ">",
`"`, """,
"'", "'",
)
func renderKommentar(name, text string) string {
return fmt.Sprintf(
"<article><h3>%s</h3><p>%s</p></article>",
htmlEscape.Replace(name),
htmlEscape.Replace(text),
)
}
func main() {
fmt.Println(renderKommentar(
`<script>alert("x")</script>`,
`Tom & Jerry sagen "Hallo".`,
))
}<article><h3><script>alert("x")</script></h3><p>Tom & Jerry sagen "Hallo".</p></article>Wichtig ist, dass & als erste Regel steht — sonst würde es die bereits eingeführten & und Co. wieder anfassen. Bei einem Replacer ist das eigentlich egal (single-pass), aber die Reihenfolge dokumentiert die Intention klar.
Bei der Erzeugung von URL-Slugs aus deutschen Titeln müssen Umlaute und Eszett in ASCII-Äquivalente überführt werden. Eine ReplaceAll-Kette ist hier besonders fehleranfällig, weil Groß- und Kleinschreibung jeweils eigene Regeln brauchen — und weil die Reihenfolge bei ß → ss und nachfolgenden s-Regeln Kopfschmerzen bereiten könnte.
package main
import (
"fmt"
"strings"
)
var umlautMap = strings.NewReplacer(
"ä", "ae", "ö", "oe", "ü", "ue", "ß", "ss",
"Ä", "Ae", "Ö", "Oe", "Ü", "Ue",
)
func slug(s string) string {
s = umlautMap.Replace(s)
s = strings.ToLower(s)
s = strings.ReplaceAll(s, " ", "-")
return s
}
func main() {
titel := []string{
"Über große Flüsse",
"Straße der Möglichkeiten",
"Ärger mit Ölheizung",
}
for _, t := range titel {
fmt.Println(slug(t))
}
}ueber-grosse-fluesse
strasse-der-moeglichkeiten
aerger-mit-oelheizungDer Replacer behandelt alle sieben Umlaut-Regeln in einem Pass; danach reicht ein ToLower und ein simples Leerzeichen-Replace, um daraus einen URL-tauglichen Slug zu machen. Im Produktionscode käme noch ein Regex-Filter für übrige Sonderzeichen dazu, das Prinzip bleibt aber dasselbe.
Für simple String-Templates mit {{token}}-Platzhaltern wirkt text/template schnell überdimensioniert: Parser-Cache, Reflection, Fehlerpfade. Wer nur eine Handvoll fester Tokens ersetzen will, baut den Replacer pro Request — oder noch besser einmal pro Template — und ist mit zwei Zeilen fertig.
package main
import (
"fmt"
"strings"
)
func renderMail(empfaenger, produkt, betrag string) string {
r := strings.NewReplacer(
"{{empfaenger}}", empfaenger,
"{{produkt}}", produkt,
"{{betrag}}", betrag,
)
template := "Hallo {{empfaenger}},\n" +
"vielen Dank für den Kauf von {{produkt}}.\n" +
"Wir haben {{betrag}} EUR abgebucht."
return r.Replace(template)
}
func main() {
fmt.Println(renderMail("Anna Schmidt", "Lizenz Pro", "49,00"))
}Hallo Anna Schmidt,
vielen Dank für den Kauf von Lizenz Pro.
Wir haben 49,00 EUR abgebucht.Diese Lösung hat klare Grenzen — keine Bedingungen, keine Schleifen, kein Kontext-Escape. Genau diese Schlichtheit ist aber oft erwünscht, etwa bei Transaktions-Mails, Slack-Notifications oder System-Log-Mustern, wo jedes zusätzliche Template-Feature ein potenzielles Sicherheitsrisiko wäre.
Single-Pass schützt vor Kreuz-Replacements
Replacer entscheidet pro Input-Position einmal und springt weiter — was er schreibt, sieht er nicht mehr. Ketten aus ReplaceAll haben diese Garantie nicht.
Als Paket-Variable vorkompilieren
Die teure Trie-Konstruktion fällt nur einmal an, wenn der Replacer auf Paket-Ebene lebt. In Hot-Paths niemals frisch via NewReplacer erzeugen.
Threadsafe ohne Lock
*strings.Replacer ist nach Konstruktion immutable und damit von beliebig vielen Goroutinen parallel nutzbar — kein Mutex, kein sync.Once nötig.
Reihenfolge entscheidet bei Konflikten
Bei gleich langen Treffern gewinnt die zuerst genannte Regel. Wer maximales Matching will, sortiert lang vor kurz beim NewReplacer-Aufruf.
WriteString spart Zwischen-String
Geht das Ergebnis ohnehin in einen io.Writer, vermeidet WriteString die Allokation des kompletten Result-Strings und reduziert GC-Druck spürbar.
Panic bei ungerader Argumentzahl
NewReplacer verlangt strikt Paare aus alt/neu. Ein vergessenes Argument führt zur Panic — bewusst hart, damit semantische Fehler nicht lautlos durchgehen.
Schneller als ReplaceAll-Kette
k Ersetzungen kosten k Voll-Scans und k Allokationen bei ReplaceAll — Replacer macht dasselbe in einem Scan und einer Allokation.
Sweet Spot: HTML-Escape, Umlaute, Template-Tokens
Überall wo eine feste, kleine Menge an Substitutionen sehr oft auf wechselnden Input angewendet wird, ist Replacer die natürliche Wahl.