Runes and UTF-8
A rune is an int32 holding one Unicode code point; ranging a string decodes UTF-8, while indexing it returns a single byte.
for i, r := range s { } — i is a byte position, r is a rune (int32)Unicode gives every character in every script a number, called a code point. The letter h is 104. The letter é is 233. Go's name for one of those numbers is rune. The language specification states that rune is an alias for int32, so a rune really is just a 32-bit whole number, and byte is likewise an alias for uint8. You write one with single quotes: 'é'. Double quotes make a string, single quotes make a rune, and the difference matters.
UTF-8 is how runes become bytes
A string stores bytes, not runes. UTF-8 is the rule for turning a code point into between one and four bytes. Code points below 128 take one byte, which is why plain English text has one byte per character. é takes two. Many emoji take four.
range decodes, indexing does not
A for loop with range over a string walks it one character at a time, decoding UTF-8 as it goes. It hands you two values: the byte position the character starts at, and the rune itself. Those positions are not 0, 1, 2 in a row; they jump by the width of each character. Indexing with s[1] skips all of that and returns the raw byte at position 1, which may be the middle of a character. utf8.RuneCountInString(s) counts characters, where len(s) counts bytes.
What trips people up
Slicing a string slices bytes, so s[:3] can split a character in two. Convert with []rune(s) when you need to index by character; that copies the text into one int32 per character.
Example
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "héllo"
// len counts bytes. RuneCountInString counts characters.
fmt.Println(len(s), utf8.RuneCountInString(s))
// A rune literal uses single quotes. A rune is a number: int32.
r := 'é'
fmt.Printf("%T %d %U\n", r, r, r)
// range decodes UTF-8. i is a byte position, c is the rune there.
for i, c := range s {
fmt.Printf("%d %c %d\n", i, c, c)
}
// Indexing does not decode. s[1] is the first byte of the two-byte é.
fmt.Println(s[1])
}
// Output:
// 6 5
// int32 233 U+00E9
// 0 h 104
// 1 é 233
// 3 l 108
// 4 l 108
// 5 o 111
// 195When to use it
- Count the characters a user typed, so a 20-character limit is not 20 bytes.
- Truncate a name for display without leaving half a character at the end.
- Reject text from an upload that is not valid UTF-8 before storing it.
- Check that every character of a username is a letter or a digit, in any script.
More examples
Indexing by character needs []rune
String slicing counts bytes, so taking the first three bytes of naive-with-a-diaeresis lands inside the i-with-two-dots and produces broken text. []rune(s) copies the string into one int32 per character, and then position 2 really is the third character. Converting a single rune back with string(r) gives its UTF-8 bytes. Reach for []rune when you truncate text for display or reverse it.
package main
import "fmt"
func main() {
s := "naïve"
// Slicing a string slices bytes. This cuts the ï in half.
fmt.Printf("%q\n", s[:3])
// []rune(s) copies the text into one int32 per character.
rs := []rune(s)
fmt.Printf("%q\n", string(rs[:3]))
// string(r) turns one rune back into its UTF-8 bytes.
fmt.Println(len(string(rs[2])))
}
// Output:
// "na\xc3"
// "naï"
// 2Invalid UTF-8 does not stop a range loop
Nothing stops a Go string holding bytes that are not valid UTF-8, because a string is just bytes. The specification says a range loop over such a string yields 0xFFFD, the replacement character, and moves on by one byte. So a loop never fails, it quietly substitutes. Check with utf8.ValidString before you trust text that arrived from a file, a form or a network.
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
// A string can hold bytes that are not valid UTF-8.
bad := "a\xffb"
fmt.Println(utf8.ValidString(bad))
// range does not fail. It yields U+FFFD and steps on one byte.
for i, c := range bad {
fmt.Printf("%d %U\n", i, c)
}
// RuneError is that replacement character, so you can test for it.
fmt.Printf("%U\n", utf8.RuneError)
}
// Output:
// false
// 0 U+0061
// 1 U+FFFD
// 2 U+0062
// U+FFFDAsking questions about one character
The unicode package takes a rune and answers one question about it, across every script rather than just A to Z. IsLetter, IsDigit and ToUpper all work on the Scandinavian o-with-a-stroke the same way they work on ASCII. This is how you validate a username or fold a search term, instead of comparing against 'a' and 'z' by hand.
package main
import (
"fmt"
"unicode"
)
func main() {
// unicode asks about one rune at a time, in any script.
for _, c := range "A1ø" {
fmt.Printf("%c letter=%v digit=%v upper=%c\n", c, unicode.IsLetter(c), unicode.IsDigit(c), unicode.ToUpper(c))
}
}
// Output:
// A letter=true digit=false upper=A
// 1 letter=false digit=true upper=1
// ø letter=true digit=false upper=Ø
Discussion