Strings Under the Hood
Strings Under the Hood
A Go string is a chunk of bytes. For ASCII text you never notice, but non-English characters and emoji force the distinction into the open.
Strings are bytes
s := "世界"
fmt.Println(len(s)) // 6, not 2
len() counts bytes. UTF-8 encodes 世 as 3 bytes and 界 as 3 bytes, so len("世界") is 6.
Indexing has the same problem:
s := "世界"
fmt.Println(s[0]) // 228
s[0] returns a single byte. For "Hello" that happens to match the character. For "世界" it's a meaningless fragment of a multi-byte rune.
Range decodes runes
range over a string yields one rune per iteration, regardless of how many bytes that rune occupies:
for i, ch := range "世界" {
fmt.Printf("%d: %c\n", i, ch)
}
// 0: 世
// 3: 界
The index jumps from 0 to 3 because range skipped the remaining bytes of 世. You get characters, not byte fragments.
Runes are integers
A rune is just an int32, so you can compare runes with integer operators:
for _, ch := range s {
if ch >= 'a' && ch <= 'z' {
// lowercase letter
}
}Character literals like 'a' and 'z' are integer constants, so the comparison is a plain numeric range check. The same pattern works for uppercase, digits, or any contiguous span of code points.
Helpers in unicode/utf8
When you don't need a full loop, unicode/utf8 has drop-in helpers.
Counting characters:
import "unicode/utf8"
utf8.RuneCountInString("世界") // 2
RuneCountInString returns how many runes the string contains, which is what len() can't tell you.
Decoding the first rune:
r, size := utf8.DecodeRuneInString("世界")
fmt.Println(r, size) // 19990 3
fmt.Println(string(r), size) // 世 3
DecodeRuneInString returns two values:
r: the first rune, as anint32code pointsize: how many bytes that rune took in the original string
Since r is an integer, printing it directly gives you the code point 19990. Use string(r) to convert it back into a readable character.
One trap: on an empty string, DecodeRuneInString returns the Unicode replacement character U+FFFD (�), not zero and not an error. Always guard empty input:
func firstRuneSize(s string) int {
if s == "" {
return 0
}
_, size := utf8.DecodeRuneInString(s)
return size
}Use these helpers when you want characters. Reach for len() and s[i] only when you truly want bytes.
Exercise: First Character
Write firstChar that returns the first character of a string as a string. It must work for any input, including emoji and non-English text.
Examples:
firstChar("Hello")returns"H"firstChar("世界")returns"世"firstChar("🚀 launch")returns"🚀"firstChar("")returns""
Steps:
- If the string is empty, return
""before doing anything else. Decoding an empty string yields the replacement character�, not"". - Decode the first rune with a helper from
unicode/utf8(remember to import the package). - Convert that rune to a string before returning it.
Watch out for two byte traps:
s[0]is a single byte, not a character, so it can't be returned directly as astring(byte and string are different types).string(s[0])does compile and passes"Hello", but it converts only that one byte, so it mangles any multi-byte character like世or🚀. Decode a rune instead.