gopherpath
0%

Strings are bytes, characters are runes

30 min · lesson 2 of 3 · #strings · #unicode

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).

Go
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 = 'ü'
}
Java
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()) { }
TypeScript
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
// 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(), ",")
Java
var b = new StringBuilder(64);
for (String id : ids) b.append(id).append(',');
String out = b.toString().replaceAll(",$", "");
TypeScript
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.ToLower for email addresses or identifiers is locale-blind and does not normalise Unicode. Compare with EqualFold instead 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 []byte to string inside a hot loop, then refactor to avoid the allocation.
  • Predict the output of for i, r := range "héllo" before running it.
stored in this browser only