How does a rune differ from a byte?
Brief Answer
byte | rune | |
|---|---|---|
| Alias for | uint8 | int32 |
| Size | 1 byte (8 bits) | 4 bytes (32 bits) |
| Represents | one byte (a number from 0 to 255) | one Unicode code point |
| When to use | raw data, ASCII text, binary formats | working with characters in any language |
byte is about bytes in memory, while rune is about characters.
Why Do We Need Different Types?
To understand the difference, you need to understand how text data is stored.
The Problem with Regular Bytes
If every character always occupied exactly one byte, there would be no need for rune. However, one byte can store only 256 different values, while there are more than 150,000 characters in the world: Latin and Cyrillic letters, Chinese characters, emojis, Arabic script, and many others.
The Solution: Unicode
Unicode is a table in which every character is assigned a number called a code point:
| Character | Unicode code point |
|---|---|
A | 65 |
я | 1103 |
中 | 20013 |
🎉 | 127881 |
Numbers this large require more than one byte. The way these numbers are stored in memory is a separate concern handled by different encodings.
UTF-8 Encoding
Go commonly uses UTF-8, an encoding in which one Unicode code point occupies from 1 to 4 bytes, depending on its value:
| Character | Code point | Bytes in UTF-8 |
|---|---|---|
A | 65 | 1 byte |
é | 233 | 2 bytes |
я | 1103 | 2 bytes |
中 | 20013 | 3 bytes |
🎉 | 127881 | 4 bytes |
Therefore, a string in Go is not a sequence of characters but a sequence of bytes, where different characters may occupy different numbers of bytes.
byte — A Unit of Memory
byte is simply another name for uint8, an integer from 0 to 255.
var b byte = 65
fmt.Println(b) // 65
fmt.Printf("%c\n", b) // A — the character with code 65 in ASCII
// These two declarations are completely equivalent
var b1 byte = 65
var b2 uint8 = 65It is used when working with raw bytes, such as when reading files, transferring data over a network, or processing binary formats.
data := []byte{0x48, 0x65, 0x6C, 0x6C, 0x6F} // bytes
fmt.Println(string(data)) // "Hello"rune — One Unicode Code Point
rune is an alias for int32, an integer type that can store values up to approximately two billion. One rune stores the code point of one Unicode character, and a single rune value is sufficient for any valid Unicode code point.
var r rune = 'я'
fmt.Println(r) // 1103 — code point
fmt.Printf("%c\n", r) // я
// These two declarations are equivalent
var r1 rune = 1103
var r2 int32 = 1103Notice that a character is written using single quotes, such as 'я', while a string is written using double quotes, such as "я". These are different values:
var r rune = 'я' // one rune: the number 1103
var s string = "я" // a string: 2 bytes in UTF-8The Main Difference in Practice
Consider a string containing Russian text:
s := "Привет"Looking Inside the String
Method 1: Bytes (`byte`)
If we iterate over the string as a sequence of bytes, we see the raw UTF-8 bytes, not the characters:
s := "Привет"
fmt.Println(len(s)) // 12 — length in BYTES
// each Russian letter occupies 2 bytes
for i := 0; i < len(s); i++ {
fmt.Printf("%d ", s[i])
}
// 208 159 209 128 208 184 208 178 208 181 209 130
// These are not letters. They are the bytes that encode the letters in UTF-8.s[0] is the first byte, 208, not the first letter, П. You cannot retrieve the letter using s[0] because that letter occupies two bytes.
Method 2: Runes (`rune`)
When you use a range loop, Go automatically decodes UTF-8 and returns the individual Unicode code points:
s := "Привет"
for i, r := range s {
fmt.Printf("position %d: rune %d, character %c\n", i, r, r)
}
// position 0: rune 1055, character П
// position 2: rune 1088, character р
// position 4: rune 1080, character и
// ...Notice that i increases by two instead of one because each Russian letter occupies two bytes in the string.
Counting Characters
This is the key practical difference:
s := "Привет"
fmt.Println(len(s)) // 12 — BYTES
fmt.Println(utf8.RuneCountInString(s)) // 6 — RUNES
fmt.Println(len([]rune(s))) // 6 — RUNESlen(s) always returns the number of bytes. To count Unicode code points, use utf8.RuneCountInString or convert the string to a rune slice.
Conversions
s := "Привет"
// String → bytes
b := []byte(s) // [208 159 209 128 208 184 208 178 208 181 209 130]
fmt.Println(len(b)) // 12
// String → runes
// This requires UTF-8 decoding
r := []rune(s) // [1055 1088 1080 1074 1077 1090]
fmt.Println(len(r)) // 6
// Access by index
fmt.Println(b[0]) // 208 — the first byte
fmt.Println(r[0]) // 1055 — the first rune, the letter П
fmt.Printf("%c", r[0]) // ПCommon Mistakes
Mistake 1: Getting the First Character with s[0]
s := "Привет"
firstChar := s[0] // 208 — this is not the letter
fmt.Printf("%c\n", firstChar) // incorrect outputThe value 208 is only the first byte of the UTF-8 representation of П.
Correct:
runes := []rune(s)
firstChar := runes[0]
fmt.Printf("%c\n", firstChar) // ПMistake 2: Cutting a String in the Middle of a Character
s := "Привет"
trimmed := s[:3] // take the first 3 bytes
fmt.Println(trimmed) // "П�"The result contains the complete two-byte encoding of П and only the first byte of р.
Correct:
runes := []rune(s)
trimmed := string(runes[:2])
fmt.Println(trimmed) // "Пр"Mistake 3: Counting String Length for a User
Suppose a username is limited to five characters:
nickname := "Иван"
if len(nickname) > 5 { // 8 > 5, so a valid name is rejected
return errors.New("name is too long")
}Correct:
import "unicode/utf8"
if utf8.RuneCountInString(nickname) > 5 {
return errors.New("name is too long")
}When to Use Each Type
Use byte or []byte When:
1. You are working with raw data
// Reading a file
data, _ := os.ReadFile("photo.jpg") // returns []byte
// Network protocol
buf := make([]byte, 1024)
conn.Read(buf)2. You know that the text contains only ASCII characters
// HTTP headers, Base64, and hexadecimal strings use ASCII
auth := "Bearer abc123"
for i := 0; i < len(auth); i++ {
b := auth[i] // safe: every character occupies exactly one byte
}3. Performance is critical and you do not need to interpret characters
// Count spaces in a large file
count := 0
for _, b := range []byte(text) {
if b == ' ' {
count++
}
}4. You are working with packages or APIs that use `[]byte`
Many standard library APIs accept or return []byte, including those related to io.Reader, crypto/sha256, and encoding/json.
Use rune or []rune When:
1. You need to work with Unicode code points instead of bytes
// Count runes
count := utf8.RuneCountInString(s)
// Iterate over runes
for _, r := range s {
fmt.Printf("%c ", r)
}2. You need to index a string by Unicode code points
runes := []rune(s)
fmt.Println(runes[5]) // the sixth rune, regardless of its byte size3. You need to check character properties
import "unicode"
for _, r := range s {
if unicode.IsLetter(r) {
// ...
}
if unicode.IsDigit(r) {
// ...
}
if unicode.IsUpper(r) {
// ...
}
}4. You need to reverse a string
This is a common example where reversing bytes breaks non-ASCII text:
// Incorrect for non-ASCII strings
func reverseBad(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)
}
// Correctly reverses Unicode code points
func reverseGood(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
fmt.Println(reverseBad("Привет")) // broken UTF-8
fmt.Println(reverseGood("Привет")) // "тевирП"Summary Table
byte | rune | |
|---|---|---|
| Alias | uint8 | int32 |
| Size | 1 byte | 4 bytes |
| Literal in code | 'A' for ASCII or 0x41 | 'я', '中', '🎉' |
| Stores | one byte from 0 to 255 | one Unicode code point |
| Used for | raw data and ASCII | text in any language |
| String access | s[i] returns the i-th byte | []rune(s)[i] returns the i-th rune |
| String length | len(s) returns the number of bytes | utf8.RuneCountInString(s) counts runes |
| Loop | for i := 0; i < len(s); i++ | for _, r := range s |
Main Rule for Junior Developers
> When working with text, use `rune` if you are not certain that the text contains only ASCII characters. When working with binary data, use `byte`.
Remember that len(string) returns the number of bytes, not the number of characters. This is one of the most common mistakes beginners make in Go.