Strings, Bytes and Runes in Go: Handling Unicode Correctly
Why len("সাব্বির") is 21: bytes vs runes, UTF-8, ranging over strings and building strings efficiently.
On this page
Why does len("সাব্বির") return 21 when the name has 7 characters? Because a Go string is a sequence of bytes, not characters. Understanding bytes, runes and UTF-8 is essential the moment your application handles names, Bangla text, emoji or anything beyond plain English.
Strings are read-only byte slices
A Go string is an immutable sequence of bytes. String literals in Go source files are UTF-8 encoded. ASCII characters take 1 byte, but most other characters take 2 to 4 bytes.
s := "Go"
fmt.Println(len(s)) // 2
fmt.Println(s[0]) // 71 (a byte, the ASCII code of 'G')
fmt.Println(string(s[0])) // G
fmt.Println([]byte(s)) // [71 111]Indexing a string gives you a byte (uint8), not a character. That is fine for ASCII and wrong for almost everything else.
Runes: Unicode code points
A rune is an alias for int32 and represents one Unicode code point. Converting a string to []rune decodes the UTF-8 bytes into code points.
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
name := "সাব্বির"
r := []rune(name)
fmt.Println(len(name)) // 21 bytes
fmt.Println(len(r)) // 7 runes
fmt.Println(utf8.RuneCountInString(name)) // 7, without allocating a slice
}Each Bangla letter here is 3 bytes in UTF-8, so 7 runes need 21 bytes.
Ranging over a string decodes runes
for i, v := range str walks the string rune by rune. The index i is the byte offset where each rune starts, which is why it jumps by 3:
for i, v := range "সাব্বির" {
fmt.Printf("%d:%c(%U) ", i, v, v)
}0:স(U+09B8) 3:া(U+09BE) 6:ব(U+09AC) 9:্(U+09CD) 12:ব(U+09AC) 15:ি(U+09BF) 18:র(U+09B0)Compare that with indexing bytes directly: name[0] is 224, the first byte of স, and name[0:3] happens to be the full letter only because it is exactly 3 bytes.
name[0:2] cuts স in half and produces invalid UTF-8 that prints as �.Changing characters
Strings are immutable, so you convert to a mutable []rune, change it, and convert back. This is the example from the repository:
name := "সাব্বির"
r := []rune(name)
r[0] = 'ছ'
fmt.Println(string(r)) // ছাব্বিরEach conversion allocates and copies, which is fine for occasional edits but expensive in a hot loop.
Code points vs what users see
One "visible character" can be several runes. In সাব্বির, the conjunct ব্ব is built from ব, the hasanta ্, and another ব. Emoji with skin tones, and flags, are also multiple code points. These user-perceived characters are called grapheme clusters. The Go standard library counts runes; for true grapheme handling (for example, "truncate to 10 visible characters") use a package such as github.com/rivo/uniseg.
Building strings efficiently
Because strings are immutable, s += x in a loop allocates a new string every time. Use strings.Builder instead:
var b strings.Builder
for i := range 3 {
fmt.Fprintf(&b, "line %d\n", i)
}
result := b.String()The strings package has everything else you need: strings.Split, strings.Join, strings.Contains, strings.ToUpper, strings.Fields and more. They are all UTF-8 aware.
Key takeaways
len returns bytes, not characters.rune is an int32 Unicode code point. Use []rune(s) or utf8.RuneCountInString to count characters.range over a string yields runes and their byte offsets.strings.Builder to build strings in loops.Last updated on