How to Replace Part of a String in Go

Replace part of a string in Go using the strings.Replace function with the original string, target, replacement, and count.

The immutable string swap

You're building a CLI tool that generates configuration files. The template contains a placeholder like __HOST__, and you need to swap it for the actual IP address before writing to disk. Or maybe you're sanitizing user input where every instance of @mention must become a hyperlink. You grab the string, find the target, and swap it out.

In Go, strings are immutable. You cannot mutate the bytes in place. Every modification produces a new string. The strings package provides the tools to perform these swaps efficiently while keeping the original data intact.

How replacement works

Strings in Go are value types consisting of a pointer to a byte array and a length. When you call strings.Replace, the function scans the original bytes, allocates a new byte array, copies the segments, inserts the replacements, and returns a new string header pointing to that array. The original string remains untouched.

This immutability makes strings safe to share across goroutines without locks. The trade-off is allocation. Every replacement creates a new string in memory. If you chain multiple replacements in a loop, you allocate a new string for every iteration. That burns CPU and memory. The standard library offers patterns to minimize this cost.

Minimal replacement

Here's the simplest replacement: swap one substring, keep the rest.

package main

import (
	"fmt"
	"strings"
)

func main() {
	// Original string stays unchanged; strings are immutable in Go.
	original := "Hello, World!"

	// Replace "World" with "Go" exactly once.
	// The fourth argument controls the number of replacements.
	result := strings.Replace(original, "World", "Go", 1)

	fmt.Println(result)
}

The function takes four arguments: the source string, the target substring, the replacement string, and an integer n controlling how many times to replace. If n is positive, the function replaces the first n occurrences. If n is -1, it replaces every occurrence. Passing -1 is the standard idiom for "replace all".

The function scans left to right. It stops after n matches. If the target isn't found, it returns a copy of the original string. If n is 0, the function returns the original string without allocating a new one. This optimization avoids wasted work when the count is zero.

Convention aside: The community often uses strings.ReplaceAll as a shorthand. It calls Replace with n = -1. Use ReplaceAll when you always want every occurrence swapped. It makes the intent clearer to readers and saves typing.

Strings are immutable. Reassign to update.

Bytes, runes, and UTF-8

Go strings are UTF-8 encoded. strings.Replace operates on bytes, not runes. This distinction matters when your target substring overlaps with multi-byte characters.

If you pass an empty string as the target, strings.Replace inserts the replacement between every byte. On a multi-byte character like é (bytes 0xC3 0xA9), this splits the rune and produces invalid UTF-8. The function does not validate the result. It performs byte-level substitution blindly.

Here's what happens when you replace with an empty target on a string containing multi-byte runes.

package main

import (
	"fmt"
	"strings"
	"unicode/utf8"
)

func main() {
	// Empty old string inserts new between every byte.
	// This splits multi-byte runes and creates invalid UTF-8.
	broken := strings.Replace("café", "", "X", -1)

	// broken is "XcXaXfX\xC3X\xA9X"
	// The rune "é" is now split across "X\xC3X\xA9X".
	fmt.Println(utf8.ValidString(broken))
}

The output prints false. The resulting string contains byte sequences that do not form valid UTF-8 runes. If you pass this string to a function that expects valid UTF-8, you may get panics or corrupted output. Always ensure your target substring is a valid, non-empty sequence if you care about UTF-8 integrity.

When working with runes, use []rune conversion or strings.Fields if you need to operate on character boundaries. strings.Replace is for literal byte sequences.

Empty strings match everywhere. Check your bounds before you replace.

Realistic replacement with Replacer

When you have a template with several placeholders, calling Replace repeatedly creates a new string for every swap. That burns CPU and memory. Use strings.Replacer to compile the rules once and apply them all at once.

strings.NewReplacer takes pairs of strings: old, new, old, new. It builds an internal trie structure that matches multiple targets efficiently. The replacer is safe for concurrent use. You can create it once and call Replace from multiple goroutines without locks.

Here's a sanitizer that strips directory traversal attempts and escapes HTML characters in a single pass.

package main

import (
	"fmt"
	"strings"
)

// SanitizePath replaces dangerous characters in a file path.
// It uses Replacer for efficiency when multiple substitutions are needed.
func SanitizePath(input string) string {
	// Compile the replacement rules once.
	// Replacer is safe for concurrent use across goroutines.
	replacer := strings.NewReplacer(
		"../", "",   // Remove directory traversal attempts.
		"..\\", "",  // Handle Windows-style traversal.
		"<", "&lt;", // Escape HTML angle brackets.
		">", "&gt;",
	)

	// Apply all rules in a single pass over the string.
	return replacer.Replace(input)
}

func main() {
	unsafe := "../secret/<config>"
	safe := SanitizePath(unsafe)
	fmt.Println(safe)
}

The replacer handles overlapping matches intelligently. If one replacement creates a new match for another rule, the replacer does not re-scan the inserted text. It processes the string in one forward pass. This prevents infinite loops and ensures linear performance.

strings.NewReplacer panics if you pass an odd number of arguments. The compiler cannot catch this because the function accepts variadic arguments. The panic message is strings.NewReplacer: odd number of arguments. Always pass pairs. If you need to replace a target with nothing, pass an empty string as the new value, not a missing argument.

Replace once, replace often. Compile the replacer, not the loop.

Pitfalls and errors

Strings are immutable, but variables holding strings are not. A common mistake is assuming that modifying a string variable updates the original source. It does not. You must reassign the result.

If you write strings.Replace(s, old, new, -1) without assigning the result, the new string is discarded. The variable s still points to the original bytes. The compiler does not warn you about unused return values. You get a silent bug.

Another trap involves the n parameter. Passing 0 returns the original string. Passing a negative number other than -1 is allowed but confusing. The documentation specifies -1 for "all". Using -2 also means "all", but -1 is the convention. Stick to -1 for clarity.

When using strings.Replacer, remember that it panics on odd arguments. The error is a runtime panic, not a compile error.

The runtime panics with strings.NewReplacer: odd number of arguments if you forget a replacement value.

If you try to assign to a string index, the compiler rejects the program. Strings are not mutable byte slices.

The compiler rejects s[0] = 'H' with cannot assign to s[0].

Use []byte(s) if you need mutable access, but remember that modifying the byte slice does not update the string. You must convert back to a string to see the changes.

Goroutines are cheap. Strings are not magic.

Decision matrix

Use strings.Replace when you need to swap a specific substring a fixed number of times.

Use strings.ReplaceAll when you want to replace every occurrence of a substring without typing -1.

Use strings.Replacer when you have multiple distinct replacements to apply to the same string.

Use regexp.ReplaceAllString when the target is a pattern, not a literal substring.

Use bytes.Replace when you are already working with []byte buffers and want to avoid converting to string and back.

Use manual slicing (s[:i] + new + s[j:]) only when you need custom logic around the replacement index that the standard library doesn't provide.

Where to go next