Guides

How to Reverse a String in Go: Bytes, Runes, and Unicode

Choose bytes, runes, or grapheme clusters for Go string reversal, with UTF-8 validation, runnable examples, and self-contained tests.

By Text Reverser TeamOctober 31, 20267 min read

If you need to reverse a string in Go, first decide what should move as one unit. An ASCII-only identifier can be reversed byte by byte. A Unicode code-point exercise needs rune reversal. Text displayed to people may require grapheme-cluster reversal so that combining marks and joined emoji stay together. These are different operations, not interchangeable implementations of one universal character reverser.

The distinction matters because a Go string is an immutable sequence of bytes, not a mutable array of visible characters. The Go strings, bytes, runes and characters guide also explains that string values may contain arbitrary bytes; they are not required to contain valid UTF-8. The examples below make that input policy explicit instead of silently changing malformed data.

Understand bytes, runes, and string indexes

For s := "café", len(s) is 5 because the precomposed é uses two UTF-8 bytes. utf8.RuneCountInString(s) is 4 because the text contains four Unicode code points. A rune is Go’s name for a Unicode code point, not necessarily a visible glyph. The family emoji 👨‍👩‍👧‍👦 contains several runes joined by zero-width joiners, although it is normally displayed as one user-perceived character.

String indexing uses byte offsets. s[0] returns a byte, not a rune. Slicing is also byte-based: with this café example, s[4:] starts at the second byte of é and produces invalid UTF-8. Being within the string’s byte bounds does not establish a valid character boundary.

A for range loop decodes runes, but its index still reports each rune’s starting byte offset. The Go specification’s range rules define this behavior. Ranging over "日本語" gives offsets 0, 3, and 6, not rune ordinals 0, 1, and 2. The second rune begins at byte 3. Mixing these coordinate systems leads to incorrect slices and misplaced edits.

Method 1: reverse bytes for ASCII or intentional binary data

Use byte reversal when bytes are genuinely the units you want. It works for ASCII because each ASCII character occupies one byte. It can also serve a binary format that explicitly requires its bytes reversed. It is not a general Unicode text algorithm.

func reverseBytes(s string) string {
    b := []byte(s)
    for left, right := 0, len(b)-1; left < right; left, right = left+1, right-1 {
        b[left], b[right] = b[right], b[left]
    }
    return string(b)
}

The conversion gives you a writable byte slice without changing the original string. For reverse, the result is esrever. For café, the UTF-8 bytes belonging to é are c3 a9; reversing puts them in the invalid order a9 c3. A readable result on one sample is not proof that a byte algorithm handles international text.

This function intentionally performs no UTF-8 validation because it is a byte operation. If callers require ASCII-only input, validate that restriction separately. Keep that contract visible rather than letting a text API quietly select byte reversal whenever it seems convenient.

Method 2: reverse runes with the standard library

For valid UTF-8 where code points are the intended units, convert to []rune, swap opposite elements, and convert back. A rune slice stores decoded code points, rather than the individual bytes of their UTF-8 encodings.

Save this complete program as main.go in a fresh directory:

package main

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

func reverseRunes(s string) (string, error) {
    if !utf8.ValidString(s) {
        return "", errors.New("input is not valid UTF-8")
    }

    runes := []rune(s)
    for left, right := 0, len(runes)-1; left < right; left, right = left+1, right-1 {
        runes[left], runes[right] = runes[right], runes[left]
    }
    return string(runes), nil
}

func main() {
    input := "café"
    output, err := reverseRunes(input)
    if err != nil {
        panic(err)
    }
    fmt.Printf("input: %s\n", input)
    fmt.Printf("reversed: %s\n", output)
    fmt.Printf("bytes: %d, runes: %d\n", len(input), utf8.RuneCountInString(input))
}

Run go run main.go. The output is:

input: café
reversed: éfac
bytes: 5, runes: 4

The empty string works: len(runes)-1 is -1, so the loop condition is false. These indexes are signed Go integers, unlike an unsigned size type that can wrap below zero. Odd and even lengths use the same loop. The original string stays unchanged; the function returns a new value.

The demonstration panics only if its fixed example unexpectedly fails validation. An application should handle the returned error according to its own requirements instead of crashing on untrusted user input.

Why validate UTF-8 before converting?

Converting invalid input directly with []rune(s) does not preserve its malformed bytes. Decoding substitutes utf8.RuneError, the replacement code point U+FFFD, for invalid encodings. A range loop also decodes malformed UTF-8 this way, advancing one byte for each invalid encoding it encounters. That may fit tolerant display or log processing, but it changes the data.

The unicode/utf8 package documentation provides ValidString for string validation and Valid for byte slices. The helper rejects malformed text before conversion so the caller receives an explicit error rather than a surprisingly altered result.

Keep separate policies for separate APIs. A function accepting arbitrary binary bytes should preserve byte semantics, not replace unknown sequences merely to satisfy a text helper. A user-facing text function should document whether invalid UTF-8 is rejected or repaired. Validation is not normalization: valid precomposed é and valid decomposed e plus an accent can still have different byte sequences.

Rune reversal is linear in the input size and needs linear working storage. The rune slice and returned string both contribute to memory use. Repeatedly prepending to an immutable string can repeatedly copy accumulated data; a mutable slice makes this algorithm much easier to inspect.

Method 3: preserve grapheme clusters for displayed text

Runes are not always what a reader perceives as characters. In decomposed text, e\u0301 consists of an e and a combining acute accent. Reversing those code points moves the mark away from its original base. Joined emoji sequences have the same problem: every rune can remain valid while the intended visual unit breaks apart.

For this requirement, use a Unicode grapheme segmentation implementation. The uniseg package documentation describes its extended grapheme clusters and NewGraphemes iterator. It is a third-party dependency, not part of Go’s standard library. Review its behavior and version like any other dependency.

Use a different directory from the earlier package main example. In that fresh directory, initialize a module and add the version tested here:

go mod init example.com/reverse-text
go get github.com/rivo/uniseg@v0.4.7

Save this helper as reverse.go:

package reverse

import (
    "errors"
    "strings"
    "unicode/utf8"

    "github.com/rivo/uniseg"
)

func ReverseGraphemes(s string) (string, error) {
    if !utf8.ValidString(s) {
        return "", errors.New("input is not valid UTF-8")
    }

    clusters := make([]string, 0)
    g := uniseg.NewGraphemes(s)
    for g.Next() {
        clusters = append(clusters, g.Str())
    }
    for left, right := 0, len(clusters)-1; left < right; left, right = left+1, right-1 {
        clusters[left], clusters[right] = clusters[right], clusters[left]
    }
    return strings.Join(clusters, ""), nil
}

ReverseGraphemes("👨‍👩‍👧‍👦A") returns "A👨‍👩‍👧‍👦", moving the family as one cluster. It also turns "e\u0301x" into "xe\u0301", keeping the accent with e. This helper allocates a slice of cluster strings and constructs a result; it is not an allocation-free, in-place modification of the input.

Grapheme boundaries are not a universal visual guarantee. They model Unicode text segmentation, not bidirectional layout, font shaping, or mirrored glyphs. Segmentation rules and dependency versions matter. Pinning v0.4.7 makes this example reproducible; it is not a claim that this is the latest release or that its Unicode data fits every project.

Add tests you can run yourself

For the standard-library main.go example, initialize a module once in its own directory with go mod init example.com/reverse-demo. Add this main_test.go beside it:

package main

import "testing"

func TestReverseRunes(t *testing.T) {
    cases := []struct {
        input string
        want  string
    }{
        {"", ""},
        {"a", "a"},
        {"abcd", "dcba"},
        {"red blue", "eulb der"},
        {"café", "éfac"},
        {"中文", "文中"},
        {"e\u0301x", "x\u0301e"},
    }
    for _, tc := range cases {
        got, err := reverseRunes(tc.input)
        if err != nil || got != tc.want {
            t.Fatalf("reverseRunes(%q) = %q, %v; want %q", tc.input, got, err, tc.want)
        }
    }
    if _, err := reverseRunes(string([]byte{0xff})); err == nil {
        t.Fatal("expected invalid UTF-8 to be rejected")
    }
}

Run go test ./... in that directory. The decomposed-accent case deliberately checks code-point order, not attractive rendering: the result places the accent before its former base letter. A test should describe the chosen contract, not silently assume all Unicode operations mean the same thing.

In the separate grapheme module, add reverse_test.go:

package reverse

import "testing"

func TestReverseGraphemes(t *testing.T) {
    cases := map[string]string{
        "":               "",
        "👨‍👩‍👧‍👦A":       "A👨‍👩‍👧‍👦",
        "e\u0301x":      "xe\u0301",
    }
    for input, want := range cases {
        got, err := ReverseGraphemes(input)
        if err != nil || got != want {
            t.Fatalf("got %q, %v; want %q", got, err, want)
        }
    }
    if _, err := ReverseGraphemes(string([]byte{0xff})); err == nil {
        t.Fatal("expected invalid UTF-8 to be rejected")
    }
}

Run go test ./... there as well. The implementations and cluster behavior were checked with Go 1.26.8 and uniseg v0.4.7. Extend the cases with your real punctuation, scripts, line endings, and input limits rather than treating a few successful examples as complete coverage.

Troubleshoot Go-specific mistakes

If indexing returns an unexpected number, remember that s[i] is a byte. If range positions appear to skip numbers, they are byte offsets, not character counters. For example, the second rune in 日本語 starts at byte 3; no character has been skipped.

If invalid bytes turn into replacement characters, look for a conversion to runes before validation. If accented text looks broken despite valid UTF-8 output, check whether combining sequences needed grapheme treatment. If an emoji family splits into components, a rune loop alone does not satisfy the visual requirement.

If Go reports mixed package names, do not put the package reverse helper in the same directory as the earlier package main program. If it cannot find the uniseg module, run the pinned go get command in the grapheme module’s directory. These are project-layout and dependency issues, not errors in the swapping loop.

An exported API should communicate its unit and error policy. Names such as ReverseBytes, ReverseRunes, and ReverseGraphemes give callers different expectations. Check validation errors at the boundary, and budget for the working slice plus returned string when processing large values.

Choose the operation before the algorithm

For ASCII or intentional binary data, reverse bytes. For valid UTF-8 code points, reverse runes. For displayed text whose emoji and combining marks must stay grouped, segment grapheme clusters and reverse those clusters. Do not select a method merely because its code is shortest.

Word-order-only reversal is different: these examples turn red blue into eulb der, not blue red. A multiline string is processed as a whole, so line separators also participate in reversal. For a reverse-each-line feature, decide how to preserve separators and transform each content segment separately. Grapheme segmentation can group a CRLF pair, while rune reversal handles its two code points individually; line-ending requirements deserve explicit tests.

Reversing logical text is not encryption, normalization, transliteration, or mirroring a rendered image. For a related browser-language implementation, see JavaScript reversal without breaking emoji. For the contrast with byte-oriented storage, see reversing a string in C.

If you only need a quick sample rather than application code, try the Text Reverser tool. In Go code, the reliable approach starts with the same question: exactly which units should remain intact when the sequence is reversed?

Try the Text Reverser tool

Reverse text, words, or letters instantly — free and entirely in your browser.

Open the tool

Permalink: https://www.textreverser.me/blog/reverse-string-in-go