Strings are bytes, characters are runes
A Go string is an immutable byte slice, usually UTF-8. There is no char, no String object, no encoding
attached. len(s) is bytes. s[i] is a byte. A character is a rune (int32, a code point).
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "Grüße, 世界"
fmt.Println("bytes:", len(s), "runes:", utf8.RuneCountInString(s))
fmt.Println("byte 0:", s[0], string(s[0:2]))
for i, r := range s {
fmt.Printf("%d:%c ", i, r) // i is a BYTE offset, r is a rune
}
fmt.Println()
fmt.Println([]rune(s)[1]) // 252 = 'ü'
}
String s = "Grüße, 世界";
s.length(); // UTF-16 code units, not bytes, not code points
s.codePointCount(0, s.length());
s.charAt(0); // a UTF-16 unit: still not a character
for (int cp : s.codePoints().toArray()) { }
const s = "Grüße, 世界";
s.length; // UTF-16 units
[...s].length; // code points — closest to Go runes
for (const ch of s) { } // iterates code points
Iterating a string with range decodes UTF-8 for you (and skips invalid bytes as U+FFFD); iterating with an index
for i := 0; i < len(s); i++ walks bytes and will cut multibyte characters in half.
Three lengths, three answers: bytes (len), code points (utf8.RuneCountInString), and grapheme clusters (emoji
families, flags — you need a library, and so does Java).
Building strings
strings.Builder is StringBuilder: amortised append, no intermediate allocations. s += x in a loop is quadratic
because strings are immutable — same reason as Java, except Go has no compiler rewriting it to a builder.
// Go
var b strings.Builder
b.Grow(64) // optional, avoids regrowth
for _, id := range ids {
fmt.Fprintf(&b, "%s,", id)
}
out := strings.TrimSuffix(b.String(), ",")
var b = new StringBuilder(64);
for (String id : ids) b.append(id).append(',');
String out = b.toString().replaceAll(",$", "");
const out = ids.join(","); // no builder needed
[]byte ↔ string conversions copy. strings.Builder and bytes.Buffer avoid that when you are writing to a
network or a file — which is most of what a service does.
The standard library you will actually use
| Task | Go |
|---|---|
| split / join | strings.Split, strings.Join |
| prefix / suffix / contains | strings.HasPrefix/HasSuffix/Contains |
| trim whitespace | strings.TrimSpace |
| case-insensitive compare | strings.EqualFold |
| replace all | strings.ReplaceAll |
| number ↔ string | strconv.Itoa, strconv.Atoi, strconv.FormatFloat |
| parse JSON-ish numbers | strconv.ParseInt(s, 10, 64) |
| regexp | regexp.MustCompile (package-level, compiler it once) |
Trap:
strings.ToLowerfor email addresses or identifiers is locale-blind and does not normalise Unicode. Compare withEqualFoldinstead of lowercasing both sides.
Proof checkpoint
- Print the byte length and rune count of a string with an emoji in it. Explain the difference out loud.
- Convert a
[]bytetostringinside a hot loop, then refactor to avoid the allocation. - Predict the output of
for i, r := range "héllo"before running it.