A Go string is a sequence of bytes, and Go source and string literals are UTF-8. A byte is one of those bytes. A rune is one Unicode character (a code point), which UTF-8 stores in 1 to 4 bytes. Most string confusion in Go comes from mixing the two up:
語 is three bytes in UTF-8, so "語Go" is 5 bytes but 3 runes. The index loop sees five bytes, and bytes 0, 1 and 2 are meaningless on their own. The range loop decodes UTF-8 and yields three runes at indexes 0, 3 and 4: i is always a byte offset, not a character count.
byte and rune Are Aliases
| Name | Same type as | Literal | Meaning |
|---|---|---|---|
byte | uint8 | byte('A'), 0x41 | one byte of data |
rune | int32 | 'A', 'é', '世' | one Unicode code point |
Because they are aliases, a byte is a uint8 and a rune is an int32; no conversion is needed between the alias and its underlying type. Single quotes make a rune, double quotes a string. 'ab' is a compile error, since a rune literal holds exactly one character.
Runes are numbers, so you can do arithmetic on them:
'7' - '0' is the classic way to turn a digit character into its value. The unicode package classifies runes (IsLetter, IsDigit, IsSpace, IsUpper) and changes case correctly for non-ASCII letters. %c prints a rune as a character, %U in U+4E16 form, %q as a quoted rune literal.
How UTF-8 Stores Characters
| Code points | Bytes | Examples |
|---|---|---|
| U+0000 to U+007F | 1 | ASCII: a, 7, { |
| U+0080 to U+07FF | 2 | é, ß, ж, ع |
| U+0800 to U+FFFF | 3 | 語, €, 한 |
| U+10000 and above | 4 | emoji, rare CJK |
ASCII text is identical in UTF-8, which is why byte-based code often works in testing and breaks on the first accented name. The unicode/utf8 package works with the encoding directly:
% x (with a space) prints bytes in hex separated by spaces. utf8.ValidString reports whether a string is valid UTF-8. Go strings may hold any bytes, not only valid UTF-8; when range meets an invalid byte it yields utf8.RuneError (U+FFFD, the replacement character) and advances one byte.
Converting Between string, []byte and []rune
| Conversion | Result | Cost |
|---|---|---|
[]byte(s) | the UTF-8 bytes | copies |
[]rune(s) | decoded code points | decodes and copies (4 bytes per rune) |
string(b) | a string from bytes | copies |
string(r) where r is []rune | encodes to UTF-8 | copies |
string(r) where r is a rune | a one-character string | small |
Reversing by bytes would scramble 世界 into invalid UTF-8, which is why reverse works on runes. Even runes are not the full story: a character like é can also be written as e plus a combining accent, which is two runes. For user-visible characters (grapheme clusters), use a library such as github.com/rivo/uniseg.
Each conversion copies, because strings are immutable and slices are not. In hot code, avoid converting back and forth; the compiler optimizes some cases (such as string(b) used as a map key or in a comparison) to skip the copy.
Indexing Gives a Byte
s[i] returns a byte, and s[i:j] slices by byte offsets:
s := "café"
fmt.Println(s[3]) // 195: the first byte of é
fmt.Println(string(s[3])) // "Ã": that byte read as a code point
fmt.Println(s[:3]) // "caf"
fmt.Println(s[:4]) // "caf\xc3": half of é, invalid UTF-8
string(s[3]) is a trap: converting a single byte to string treats it as the code point 195, which is Ã, not the byte itself. To find character boundaries, use range, the strings.Index family (which return byte offsets that are always on boundaries), or utf8.DecodeRuneInString.
The bytes Package
bytes mirrors strings for []byte: bytes.Contains, bytes.Split, bytes.Fields, bytes.TrimSpace, bytes.Equal, bytes.ToUpper and so on. Use it when data arrives as bytes (file contents, HTTP bodies, network buffers), so you do not convert to a string and back:
bytes.Buffer is a growable byte buffer that implements io.Reader and io.Writer. It is the right tool when you both write and read bytes; for building a string, strings.Builder is slightly cheaper. Compare byte slices with bytes.Equal, since == does not work on slices.
Choosing Between Them
- Text you display, compare or search: keep it a
string, and userangewhen you walk it character by character. - Character positions, reversing, truncating to n characters: convert to
[]rune. - I/O, hashing, encoding, binary protocols: use
[]byteand thebytespackage. - A single character: a
rune. A single raw byte: abyte.
Frequently Asked Questions
What is a rune in Go?
rune is an alias for int32 and represents one Unicode code point. A rune literal is written in single quotes: 'a' is 97, 'é' is 233, '世' is 19990. Ranging over a string with for i, r := range s gives you runes, decoded from the string's UTF-8 bytes.
What is the difference between a rune and a byte in Go?
A byte (alias for uint8) is 8 bits of raw data. A rune (alias for int32) is a whole Unicode character. In a UTF-8 string, ASCII characters take one byte each, while other characters take two to four bytes but are still one rune. len(s) counts bytes; utf8.RuneCountInString(s) counts runes.
How do I loop over the characters of a string in Go?
Use for i, r := range s. Each iteration gives the byte offset i and the rune r, so multi-byte characters are handled correctly. A plain index loop for i := 0; i < len(s); i++ visits bytes, which splits non-ASCII characters into pieces.
How do I convert a string to a byte slice in Go?
b := []byte(s) copies the string's bytes into a new slice. string(b) converts back, again with a copy. Use []rune(s) instead when you need to work with characters, for example to reverse a string or take its first n characters.