Question

Understanding rune in Go: Characters, Unicode, and Case Swapping

goterminologyrune

Question

In Go, I often see the statement that rune is just an alias for int32, but I do not understand what that means in practice.

For example, in this case-swapping function:

func SwapRune(r rune) rune {
    switch {
    case 'a' <= r && r <= 'z':
        return r - 'a' + 'A'
    case 'A' <= r && r <= 'Z':
        return r - 'A' + 'a'
    default:
        return r
    }
}

I do not understand:

  • Why an integer-like type is used for characters
  • What expressions like 'a' <= r && r <= 'z' mean
  • Why subtraction and addition are used, such as r - 'a' + 'A'
  • Why switch has no expression after it

And in this function:

func SwapCase(str string) string {
    return strings.Map(SwapRune, str)
}

I understand that it maps over the string and returns a new string, but I do not clearly understand how rune and byte work here, or why rune is the right type for this kind of character processing in Go.

Short Answer

By the end of this page, you will understand what a rune is in Go, why it is used for Unicode characters, how it differs from byte, and how character comparisons and arithmetic work in code like a case-swapping function. You will also see how strings.Map processes a string rune by rune.

Concept

In Go, a rune represents a Unicode code point. It is defined as an alias for int32.

That does not mean Go is treating characters as random numbers. It means that each character is stored using its numeric Unicode value.

For example:

fmt.Println('A') // 65
fmt.Println('a') // 97

Characters have numeric codes behind the scenes. This is true in many programming languages.

Why rune exists

Go strings are stored as UTF-8 encoded bytes. UTF-8 is efficient, but some characters use more than one byte.

Examples:

  • 'A' uses 1 byte in UTF-8
  • 'é' uses 2 bytes in UTF-8
  • '世' uses 3 bytes in UTF-8

A byte is just one 8-bit value. It is useful when working with raw binary data or the raw UTF-8 encoding of a string.

A rune represents the actual character code point, which is what you usually want when doing character-based logic like:

  • checking if a character is a letter
  • converting case
  • filtering characters
  • counting human-readable characters more accurately than bytes

Why characters can be compared like numbers

Because a rune is a numeric Unicode value, comparisons such as this are valid:

'a' <= r && r <= 'z'

This means:

  • r is greater than or equal to 'a'
  • and r is less than or equal to 'z'

So it checks whether r is a lowercase ASCII letter.

Why subtraction and addition work

In Unicode and ASCII, uppercase and lowercase English letters are arranged in order.

  • 'a' is 97
  • 'b' is 98
  • ...
  • 'z' is 122

And:

  • 'A' is 65
  • 'B' is 66
  • ...
  • 'Z' is 90

So this expression:

r - 'a' + 'A'

means:

  1. Find how far r is from lowercase 'a'
  2. Apply that same offset to uppercase 'A'

If r is 'c':

  • 'c' - 'a' is 2
  • 2 + 'A' gives 'C'

This works for basic English letters because their code points are in a continuous range.

Why switch has no expression

Go allows a switch without an expression:

switch {
case condition1:
    // ...
case condition2:
    // ...
}

This is like writing:

switch true {
case condition1:
    // ...
case condition2:
    // ...
}

Each case is a boolean expression. The first one that evaluates to true runs.

That is why this works:

switch {
case 'a' <= r && r <= 'z':
    return r - 'a' + 'A'
case 'A' <= r && r <= 'Z':
    return r - 'A' + 'a'
default:
    return r
}

Why rune is used instead of byte

If you loop through a string as bytes, you are reading the raw UTF-8 encoding, not necessarily whole characters.

For example:

s := "é"
fmt.Println(len(s)) // 2 bytes

But it is one character.

If you process the string as runes, Go decodes UTF-8 and gives you actual Unicode code points.

That is why strings.Map uses a function of type:

func(rune) rune

It applies the function to each rune in the string, then builds a new string from the returned runes.

Important limitation of this specific example

The SwapRune function only handles ASCII letters A-Z and a-z. It does not correctly swap case for all Unicode letters.

For full Unicode-aware case conversion, Go provides the unicode package, such as:

unicode.ToUpper(r)
unicode.ToLower(r)

That matters in real programs when text may contain non-English characters.

Mental Model

Think of a string in Go as a box of encoded bytes.

A byte is one small piece from that box. A rune is the decoded character value you get after interpreting those bytes as UTF-8.

So:

  • byte = raw storage unit
  • rune = actual character code point

For the case-swapping example, imagine a row of lowercase letters and a row of uppercase letters:

Lowercase: a b c d e ... z
Uppercase: A B C D E ... Z

If you know a letter is 2 places after a, then its uppercase version is 2 places after A.

That is exactly what this does:

r - 'a' + 'A'

It says: "find the position of this lowercase letter, then move to the same position in the uppercase row."

Take Quiz

Syntax and Examples

Basic rune syntax

var r rune = 'A'
fmt.Println(r)        // 65
fmt.Printf("%c\n", r) // A

Single quotes in Go are used for rune literals.

  • 'A' is a rune literal
  • "A" is a string literal

Comparing runes

func isLowercaseASCII(r rune) bool {
    return 'a' <= r && r <= 'z'
}

This checks whether r is between 'a' and 'z' inclusive.

Simple case conversion with rune arithmetic

func toUpperASCII(r rune) rune {
    if 'a' <= r && r <= 'z' {
        return r - 'a' + 'A'
    }
    return r
}

If r is 'd':

  • 'd' - 'a' = 3
  • 3 + 'A' = 'D'

Using strings.Map

package main

import (
    "fmt"
    "strings"
)

func SwapRune(r rune) rune {
    switch {
    case 'a' <= r && r <= 'z':
        return r - 'a' + 'A'
    case 'A' <= r && r <= 'Z':
        return r - 'A' + 'a'
    default:
        return r
    }
}

func main() {
    s := "Hello, Go!"
    fmt.Println(strings.Map(SwapRune, s)) // hELLO, gO!
}

strings.Map reads the string rune by rune, calls SwapRune for each rune, and builds a new string.

byte versus rune

s := "é"

fmt.Println(len(s)) // 2

for i := 0; i < len(s); i++ {
    fmt.Println(s[i])
}

This prints individual bytes, not the whole character.

Compare that with:

for _, r := range s {
    fmt.Printf("%c %d\n", r, r)
}

This prints the decoded rune.

Step by Step Execution

Consider this example:

func SwapRune(r rune) rune {
    switch {
    case 'a' <= r && r <= 'z':
        return r - 'a' + 'A'
    case 'A' <= r && r <= 'Z':
        return r - 'A' + 'a'
    default:
        return r
    }
}

Now trace it with:

result := SwapRune('c')

Step 1: Call the function

r receives the rune 'c'.

Its numeric value is 99.

Step 2: Evaluate the switch

This is a condition-based switch:

switch {

Go checks each case from top to bottom.

Step 3: First case

'a' <= r && r <= 'z'

Substitute values:

  • 'a' is 97
  • r is 99
  • 'z' is 122

So Go checks:

97 <= 99 && 99 <= 122

This is true.

Step 4: Return converted rune

return r - 'a' + 'A'

Substitute values:

  • r is 99
  • 'a' is 97
  • 'A' is 65

So:

99 - 97 + 65 = 67

67 is the code point for 'C'.

So the function returns 'C'.

Another trace with a non-letter

result := SwapRune('!')
  • First case: 'a' <= '!' && '!' <= 'z' is false
  • Second case: 'A' <= '!' && '!' <= 'Z' is false
  • default runs and returns '!'

What happens in strings.Map

If the input is:

"Ab!"

Then strings.Map(SwapRune, str) effectively does this:

  1. Read 'A' → returns 'a'
  2. Read 'b' → returns 'B'
  3. Read '!' → returns '!'
  4. Build the final string: "aB!"

Real World Use Cases

When runes are useful

Text processing

When you need to inspect or transform characters in a string:

  • convert case
  • remove unwanted characters
  • validate input
  • normalize text for search

Parsers and tokenizers

Compilers, interpreters, and config parsers often examine input one character at a time. Using runes is safer than bytes when Unicode input is possible.

User input validation

Examples:

  • allow only letters and digits
  • reject control characters
  • detect spaces or punctuation

Search and indexing tools

Programs may lowercase or clean text before storing or comparing it.

APIs and web applications

If your app receives names, messages, or comments from users, rune-aware code helps avoid breaking non-ASCII text.

When bytes are more appropriate

Use byte when working with:

  • binary file formats
  • network protocols
  • image or audio data
  • manual UTF-8 encoding details
  • high-performance operations on raw data
Take Quiz

Real Codebase Usage

In real Go codebases, developers rarely write manual ASCII case logic unless they specifically want ASCII-only behavior.

Common patterns

Use range to iterate runes

for _, r := range s {
    fmt.Printf("%c\n", r)
}

This decodes UTF-8 automatically.

Use the unicode package for character categories

import "unicode"

func isLetter(r rune) bool {
    return unicode.IsLetter(r)
}

This works beyond ASCII.

Use Unicode-aware case conversion

import "unicode"

func swapRuneUnicode(r rune) rune {
    if unicode.IsLower(r) {
        return unicode.ToUpper(r)
    }
    if unicode.IsUpper(r) {
        return unicode.ToLower(r)
    }
    return r
}

Guard clauses for clarity

func normalizeLetter(r rune) rune {
    if !unicode.IsLetter(r) {
        return r
    }
    return unicode.ToLower(r)
}

This style is common because it keeps special cases simple.

Validation pipelines

Developers often combine rune iteration with validation:

func onlyASCII(s string) bool {
    for _, r := range s {
        if r > 127 {
            return false
        }
    }
    return true
}

Mapping functions for transformation

cleaned := strings.Map(func(r rune) rune {
    if r == ' ' {
        return '-'
    }
    return r
}, input)

This pattern is common in slug generation, sanitization, and formatting.

Common Mistakes

1. Confusing rune literals and strings

Broken:

var r rune = "A"

Why it fails:

  • "A" is a string
  • 'A' is a rune

Correct:

var r rune = 'A'

2. Treating bytes as characters

Broken assumption:

s := "é"
fmt.Println(len(s)) // beginner may expect 1

Actually, this prints 2 because the UTF-8 encoding uses 2 bytes.

If you want characters, iterate over runes:

for _, r := range s {
    fmt.Printf("%c\n", r)
}

3. Assuming ASCII logic works for all languages

Broken idea:

func toUpper(r rune) rune {
    if 'a' <= r && r <= 'z' {
        return r - 'a' + 'A'
    }
    return r
}

This works only for ASCII English letters.

Use unicode.ToUpper if you need wider Unicode support.

4. Indexing a string and expecting a rune

Broken expectation:

s := "世界"
fmt.Println(s[0])

s[0] is a byte, not the full character.

Safer approach:

for _, r := range s {
    fmt.Printf("%c\n", r)
}

5. Misreading switch without an expression

Some beginners think this is invalid:

switch {
case x > 10:
    fmt.Println("big")
}

It is valid in Go. Each case is just a boolean condition.

6. Forgetting that && means both sides must be true

'a' <= r && r <= 'z'

This means r must satisfy both comparisons.

If either side is false, the whole condition is false.

Comparisons

rune vs byte

Conceptrunebyte
Underlying typeint32uint8
RepresentsUnicode code pointRaw 8-bit value
Good forCharacters and text logicRaw bytes and binary data
String indexing resultNot directlyYes, s[i] gives a byte
Unicode-safe character handlingYesNo

Rune literal vs string literal

SyntaxMeaningExample
'A'A single runecode point 65
"A"A stringsequence of bytes representing A

switch with expression vs switch with conditions

FormExampleUse case
Value switchswitch r { case 'a': ... }Match exact values
Condition switchswitch { case 'a' <= r && r <= 'z': ... }Test ranges or boolean conditions

Manual ASCII case conversion vs unicode package

ApproachGood forLimitation
r - 'a' + 'A'Simple ASCII-only logicDoes not handle full Unicode
unicode.ToUpper(r)Real text processingSlightly more general, but usually preferred
Take Quiz

Cheat Sheet

Quick reference

What is a rune?

type rune = int32

A rune represents a Unicode code point.

What is a byte?

type byte = uint8

A byte is an 8-bit raw value.

Rune literal vs string literal

'A'   // rune
"A"   // string

Check ASCII lowercase

'a' <= r && r <= 'z'

Check ASCII uppercase

'A' <= r && r <= 'Z'

Convert lowercase ASCII to uppercase

r - 'a' + 'A'

Convert uppercase ASCII to lowercase

r - 'A' + 'a'

switch without expression

switch {
case condition1:
    // ...
case condition2:
    // ...
}

Equivalent idea: switch true

Iterate over bytes

for i := 0; i < len(s); i++ {
    b := s[i]
    _ = b
}

Iterate over runes

for _, r := range s {
    _ = r
}

Transform every rune in a string

strings.Map(func(r rune) rune {
    return r
}, s)

Important edge case

len(s) returns bytes, not the number of runes or user-visible characters.

Unicode-aware tools

unicode.IsLetter(r)
unicode.IsLower(r)
unicode.IsUpper(r)
unicode.ToLower(r)
unicode.ToUpper(r)

FAQ

What is a rune in Go in simple terms?

A rune is a value that represents one Unicode character code point. Go uses rune when you want to work with characters instead of raw bytes.

Why is rune an alias for int32?

Because Unicode code points are numbers, and int32 is large enough to store them. The alias makes the purpose clearer when reading code.

What is the difference between byte and rune in Go?

A byte is raw 8-bit data. A rune is a decoded Unicode character value. Strings are stored as bytes, but you often process text as runes.

Why does 'a' <= r && r <= 'z' work?

Because rune literals like 'a' and 'z' have numeric code point values. The expression checks whether r falls in that range.

Why does r - 'a' + 'A' convert lowercase to uppercase?

It keeps the same alphabet position. For example, 'c' is 2 positions after 'a', so the result is 2 positions after 'A', which is 'C'.

Why does switch have no value in this example?

In Go, switch can be used without an expression. Each case is then a boolean condition, and the first true one runs.

Does this case-swapping code work for all Unicode letters?

No. It only works for ASCII letters A-Z and a-z. For broader Unicode support, use the unicode package.

Does len(string) count characters in Go?

No. len(s) counts bytes. Some characters use more than one byte in UTF-8.

Take Quiz

Mini Project

Description

Build a small text transformer that swaps the case of ASCII letters in a string. This project helps you practice working with rune, using strings.Map, and understanding the difference between text processing by bytes and by Unicode code points.

Goal

Create a Go program that converts uppercase ASCII letters to lowercase, lowercase ASCII letters to uppercase, and leaves all other characters unchanged.

Requirements

Requirement 1

Take Quiz

Keep learning

Related questions

View more