Strings, Bytes and Runes
A Go string is a read-only sequence of bytes, and by convention those bytes are UTF-8 text. Most of the time you don't need to think about that — but as soon as text contains accents, other alphabets or emoji, the difference between a byte and a character matters. This lesson explains bytes, runes and UTF-8, and how to index, slice, iterate and compare strings correctly.
Strings are bytes
len returns the number of bytes, and indexing returns a single byte (a uint8). For plain ASCII text — English letters, digits, punctuation — each character is one byte, so everything lines up:
package main import "fmt" func main() { s := "Gopher" fmt.Println(len(s), s[0], s[1]) fmt.Println(string(s[0]), s[1:4]) fmt.Printf("%T\n", s[0]) }
6 71 111 G oph uint8
s[0] is 71, the byte value of "G". Slicing with s[1:4] gives bytes 1 up to (not including) 4, as a new string.
UTF-8 and multi-byte characters
UTF-8 stores each Unicode character in one to four bytes. Characters beyond ASCII take two or more, so byte counts and indexes no longer match character counts:
package main import ( "fmt" "unicode/utf8" ) func main() { s := "Café ☕" fmt.Println(len(s), utf8.RuneCountInString(s)) fmt.Println(s[3], s[3:5]) fmt.Printf("% x\n", s) }
9 6 195 é 43 61 66 c3 a9 20 e2 98 95
The string has 6 characters but 9 bytes: é is two bytes (c3 a9) and ☕ is three. s[3] is just the first half of é. Use utf8.RuneCountInString to count characters, and be careful when slicing text that might not be ASCII — cutting through the middle of a character produces invalid UTF-8.
Runes
A rune is an alias for int32 that holds one Unicode code point. Rune literals use single quotes. Ranging over a string decodes it rune by rune; converting a string to []rune decodes all of it at once:
package main import "fmt" func main() { letter := 'é' fmt.Println(letter, string(letter)) fmt.Printf("%c %U %T\n", letter, letter, letter) word := "naïve" for i, r := range word { fmt.Printf("%d:%c ", i, r) } fmt.Println() runes := []rune(word) fmt.Println(len(runes), string(runes[2])) fmt.Println(string(runes[:3])) // the first three characters, safely }
233 é é U+00E9 int32 0:n 1:a 2:ï 4:v 5:e 5 ï naï
Converting to []rune is the simple way to index characters by position or take the first n characters. It copies the text, so for long strings that you only scan once, prefer range.
Strings are immutable
You can't change a string in place — s[0] = 'J' doesn't compile. Build a new string instead, or convert to a byte or rune slice, modify that, and convert back:
package main func main() { s := "hello" s[0] = 'J' }
./main.go:5:2: cannot assign to s[0] (neither addressable nor a map index expression)
package main import "fmt" func main() { s := "hello" b := []byte(s) b[0] = 'J' fmt.Println(string(b), s) t := "J" + s[1:] fmt.Println(t) }
Jello hello Jello
Because strings are immutable, they're cheap to pass around and slicing a string doesn't copy anything — both strings share the same bytes.
Escape sequences
| Escape | Meaning |
|---|---|
\n \t \r | Newline, tab, carriage return |
\\ \" \' | Backslash, double quote (in strings), single quote (in runes) |
\x41 | A byte given in hex |
\u00e9 \U0001F600 | A Unicode code point (4 or 8 hex digits) |
package main import "fmt" func main() { fmt.Println("Tab:\t|", "Quote: \"", "Hex: \x47\x6f", "Unicode: \u00e9 \U0001F642") fmt.Println(`Raw: \n stays as backslash-n`) }
Tab: | Quote: " Hex: Go Unicode: é 🙂 Raw: \n stays as backslash-n
Comparing and joining
== compares strings exactly, byte for byte, and </> compare byte by byte, which for ASCII is alphabetical order with capitals first. For case-insensitive comparison use strings.EqualFold. The + operator joins strings:
package main import ( "fmt" "strings" ) func main() { fmt.Println("go" == "Go", strings.EqualFold("go", "GO")) fmt.Println("apple" < "banana", "Banana" < "apple") greeting := "Hello" greeting += ", " + "world" fmt.Println(greeting) }
false true true true Hello, world
Joining with + in a big loop creates a new string every time; lesson 22 shows the efficient way.
Invalid UTF-8
A string can hold any bytes, not only valid UTF-8 — for example, data read from a binary file. Ranging over invalid bytes produces the replacement character U+FFFD. You can check with utf8.ValidString, or clean a string with strings.ToValidUTF8:
package main import ( "fmt" "strings" "unicode/utf8" ) func main() { bad := "ok\xffok" fmt.Println(utf8.ValidString(bad), utf8.ValidString("Café")) for _, r := range bad { fmt.Printf("%U ", r) } fmt.Println() fmt.Println(strings.ToValidUTF8(bad, "?")) }
false true U+006F U+006B U+FFFD U+006F U+006B ok?ok
Try it yourself
Reverse a string character by character. Try it with plain ASCII, then with accents and emoji — reversing the bytes would break those, but reversing runes works.
desserts 🎉 !élO¡
strings package.