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
switchhas 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:
ris greater than or equal to'a'- and
ris 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:
- Find how far
ris from lowercase'a' - Apply that same offset to uppercase
'A'
If r is 'c':
'c' - 'a'is22 + '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 unitrune= 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."
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'= 33 + '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'is97ris99'z'is122
So Go checks:
97 <= 99 && 99 <= 122
This is true.
Step 4: Return converted rune
return r - 'a' + 'A'
Substitute values:
ris99'a'is97'A'is65
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 defaultruns and returns'!'
What happens in strings.Map
If the input is:
"Ab!"
Then strings.Map(SwapRune, str) effectively does this:
- Read
'A'→ returns'a' - Read
'b'→ returns'B' - Read
'!'→ returns'!' - 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
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
| Concept | rune | byte |
|---|---|---|
| Underlying type | int32 | uint8 |
| Represents | Unicode code point | Raw 8-bit value |
| Good for | Characters and text logic | Raw bytes and binary data |
| String indexing result | Not directly | Yes, s[i] gives a byte |
| Unicode-safe character handling | Yes | No |
Rune literal vs string literal
| Syntax | Meaning | Example |
|---|---|---|
'A' | A single rune | code point 65 |
"A" | A string | sequence of bytes representing A |
switch with expression vs switch with conditions
| Form | Example | Use case |
|---|---|---|
| Value switch | switch r { case 'a': ... } | Match exact values |
| Condition switch | switch { case 'a' <= r && r <= 'z': ... } | Test ranges or boolean conditions |
Manual ASCII case conversion vs unicode package
| Approach | Good for | Limitation |
|---|---|---|
r - 'a' + 'A' | Simple ASCII-only logic | Does not handle full Unicode |
unicode.ToUpper(r) | Real text processing | Slightly more general, but usually preferred |
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.
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
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.