How to Reverse a String in Go (Unicode-Safe)

To reverse a string in Go while preserving Unicode characters, you must iterate over `rune` values instead of `byte` values, as Go strings are UTF-8 encoded and a single character can span multiple bytes.

The byte trap

You write a function to reverse a string. It works perfectly for "Hello". You test it with "Hello δΈ–η•Œ" and the output looks like οΏ½η•ŒδΈ– olleH. The bytes got scrambled. The terminal displays replacement characters because the UTF-8 sequences were torn apart.

Go strings are UTF-8 encoded. A single character can span one to four bytes. If you reverse the byte slice directly, you reverse the internal structure of multi-byte characters. The decoder sees garbage and gives up.

Bytes versus runes

Go distinguishes between raw data and characters. A byte is an 8-bit unsigned integer. It holds a single byte of data. A rune is an alias for int32. It holds a Unicode code point, which represents one logical character.

Think of bytes like individual tiles in a mosaic. Some pictures need one tile. Others need four tiles arranged in a specific pattern to form the image. If you reverse the order of the tiles, you destroy the pictures. Runes are the pictures themselves. Reversing the runes keeps the pictures intact while changing their order.

The conversion []rune(s) decodes the UTF-8 string into a slice of runes. Each rune in the slice corresponds to one code point. This handles the variable-length encoding automatically. You don't need to track byte boundaries or worry about continuation bytes.

Bytes are data. Runes are characters. Reverse the right thing.

The standard pattern

Here's the simplest correct implementation. Convert the string to a rune slice, reverse the slice in place, and convert back to a string.

package main

import (
	"fmt"
)

// ReverseString returns the input string with runes reversed.
// It handles multi-byte UTF-8 characters correctly.
func ReverseString(s string) string {
	// allocate a slice of runes; each element holds one Unicode code point
	runes := []rune(s)

	// swap elements from both ends moving toward the center
	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
		runes[i], runes[j] = runes[j], runes[i]
	}

	// encode the rune slice back to a UTF-8 string
	return string(runes)
}

func main() {
	input := "Hello δΈ–η•Œ 🌍"
	fmt.Println("Original:", input)
	fmt.Println("Reversed:", ReverseString(input))
}
# output:
Original: Hello δΈ–η•Œ 🌍
Reversed: 🌍 η•ŒδΈ– olleH

What happens under the hood

When you call []rune(s), the runtime allocates a new slice on the heap. The length of the slice matches the number of runes in the string, not the number of bytes. The runtime walks through the string, decodes each UTF-8 sequence, and stores the resulting code point in the slice.

The loop swaps runes in place. This part is fast. It touches each rune exactly once. No extra allocation happens during the swap.

The call string(runes) allocates a new string. The runtime encodes each rune back to UTF-8 bytes. If the input contained ASCII characters, the output uses one byte per character. If the input contained emojis, the output uses four bytes per character. The encoding matches the original character set.

Two allocations occur: one for the rune slice and one for the result string. For short strings, this cost is negligible. The garbage collector handles small allocations efficiently. For extremely large strings, the memory spike might matter. In those cases, you need a streaming approach, but that adds complexity. The slice conversion is the right trade-off for correctness and readability in almost every scenario.

Strings are already cheap to pass by value. Passing a pointer to a string adds indirection without saving memory. The string header is just two words: a pointer and a length. Copying the header is faster than dereferencing a pointer.

Real-world usage

In a production codebase, you'd likely wrap this logic in a package function. Here's how that looks with proper documentation and a receiver method style, which is common for utility types.

package textutil

// Reverse returns the string with runes reversed.
// It preserves multi-byte UTF-8 characters.
func Reverse(s string) string {
	// convert to runes to isolate code points from UTF-8 encoding
	runes := []rune(s)

	// reverse the slice in place using two pointers
	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
		runes[i], runes[j] = runes[j], runes[i]
	}

	// re-encode runes to UTF-8 bytes
	return string(runes)
}

Public names start with a capital letter. Private names start with a lowercase letter. Go has no public or private keywords. Visibility is controlled entirely by the first letter of the identifier. The function Reverse is exported. A helper function inside the package would be lowercase.

The receiver name is usually one or two letters matching the type. If this were a method on a struct, you'd see (t *Text) Reverse(). You wouldn't use this or self. The community convention keeps receiver names short and consistent.

The grapheme cluster gotcha

Rune reversal handles code points correctly. It does not handle grapheme clusters. A grapheme cluster is what a user perceives as a single character. Some grapheme clusters consist of multiple runes.

Combining marks are the most common case. The character Γ© can be represented as a single rune U+00E9, or as two runes: e followed by a combining acute accent U+0301. Both render identically in most fonts. Rune reversal treats them differently.

Here's the difference in action. The first string uses a precomposed character. The second uses a base character plus a combining mark.

package main

import (
	"fmt"
)

func main() {
	// precomposed Γ©: single rune
	s1 := "cafΓ©"
	// decomposed Γ©: e plus combining accent U+0301
	s2 := "cafe\u0301"

	fmt.Printf("s1 runes: %v\n", []rune(s1))
	fmt.Printf("s2 runes: %v\n", []rune(s2))

	// reverse both using the rune slice method
	r1 := Reverse([]rune(s1))
	r2 := Reverse([]rune(s2))

	fmt.Println("Reversed s1:", string(r1))
	fmt.Println("Reversed s2:", string(r2))
}

// Reverse helper for this example
func Reverse(runes []rune) []rune {
	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
		runes[i], runes[j] = runes[j], runes[i]
	}
	return runes
}
# output:
s1 runes: [99 97 102 233]
s2 runes: [99 97 102 101 769]
Reversed s1: Γ©fac
Reversed s2: 101fac

The output for s2 looks broken. The combining accent got separated from the e. The terminal tries to attach the accent to the previous character, which creates visual garbage. The compiler won't warn you about this. The code runs fine. The result is just wrong for human readers.

If your application processes user input that might contain combining marks, emoji modifiers, or complex scripts, simple rune reversal is insufficient. You need grapheme cluster segmentation. The golang.org/x/text/unicode/norm package can normalize text to a consistent form, or you can use a grapheme segmentation library to split the string into clusters before reversing. For standard ASCII and precomposed Unicode text, the rune slice approach is sufficient.

The compiler won't save you from logical errors. Test with emojis and accented characters.

Pitfalls and errors

Reversing bytes directly produces invalid UTF-8. The compiler allows you to reverse a byte slice. The damage happens at runtime.

// This is wrong. Do not use this.
func ReverseBytes(s string) string {
	b := []byte(s)
	for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
		b[i], b[j] = b[j], b[i]
	}
	return string(b)
}

If you pass "δΈ–η•Œ" to this function, the bytes get scrambled. The result contains byte sequences that violate UTF-8 rules. When you print the result, the terminal displays replacement characters. The compiler rejects the program with loop variable i captured by func literal if you try to use loop variables in closures incorrectly, but it won't stop you from corrupting UTF-8 data. The error is semantic, not syntactic.

Memory allocation is another consideration. Converting to []rune allocates a slice. If the string is 1 MB of ASCII text, the rune slice uses 4 MB of memory. Each ASCII character expands from one byte to four bytes. If memory is tight, this expansion can trigger garbage collection pressure. For hot paths processing massive ASCII data, you might need a byte-aware reversal that respects UTF-8 boundaries. That implementation is complex and error-prone. The rune slice is the safe default.

Don't fight the type system. Wrap the value or change the design.

Decision matrix

Use []rune(s) conversion when you need correct Unicode handling and the string fits in memory. This is the standard approach for 99% of applications.

Use byte-level reversal only when you have verified ASCII-only input and you are optimizing a tight loop where allocation is forbidden. Verify the input first.

Use the golang.org/x/text/unicode/norm package when your text contains combining marks, accents, or complex scripts where visual grapheme clusters must stay intact.

Use a streaming decoder when processing massive files that cannot fit in memory. Decode runes one by one, buffer them, and emit in reverse order.

Pick the tool that matches your data. Don't over-engineer for edge cases you don't have.

Where to go next