Strings & Formatting
Strings are immutable UTF-8 bytes, not UTF-16 chars. Indexing, runes, building strings efficiently, fmt verbs and strconv.
Section 04 of 315 min read
A Go string is an immutable sequence of bytes, conventionally holding UTF-8 text. A Java String is a sequence of UTF-16 chars. That one difference explains every string surprise a Java developer hits in Go: len counts bytes, indexing returns a byte, and ranging over a string yields Unicode code points.
Length, indexing and runes
String s = "héllo";
s.length(); // 5 (UTF-16 units)
s.charAt(1); // 'é'
s.codePointCount(0, s.length()); // 5
for (char c : s.toCharArray()) {
// breaks on emoji: surrogate pairs
}
s.equals("héllo"); // true; == compares referencess := "héllo"
len(s) // 6: é is two bytes
s[1] // 195: a byte, not a char
utf8.RuneCountInString(s) // 5 code points
for i, r := range "hé!" {
// i = byte offset, r = rune (int32)
fmt.Println(i, string(r)) // 0 h, 1 é, 3 !
}
s == "héllo" // true: == compares contentsEveryday operations: the strings package
| Java | Go |
|---|---|
s.contains(x) / s.startsWith(p) | strings.Contains(s, x) / strings.HasPrefix(s, p) |
s.split(",") (regex!) | strings.Split(s, ","): a literal separator, no regex |
s.split("\\s+") | strings.Fields(s): split on runs of whitespace |
s.strip() / s.trim() | strings.TrimSpace(s) |
s.equalsIgnoreCase(t) | strings.EqualFold(s, t) |
s.indexOf("=") + two substring calls | before, after, ok := strings.Cut(s, "=") |
String.join(",", list) | strings.Join(list, ",") |
s.replace(a, b) | strings.ReplaceAll(s, a, b) |
s.lines() | strings.Lines(s): an iterator (Go 1.24) |
"ab".repeat(3) | strings.Repeat("ab", 3) |
Building strings
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 3; i++) {
sb.append(i).append(',');
}
String csv = sb.toString(); // "0,1,2,"var b strings.Builder // zero value is ready to use
for i := range 3 {
fmt.Fprintf(&b, "%d,", i) // Builder is an io.Writer
}
csv := b.String() // "0,1,2,"
// Or collect and join:
parts := []string{"a", "b", "c"}
joined := strings.Join(parts, ",")Strings and byte slices
b := []byte("hi") // copy: strings are immutable, []byte is not
b[0] = 'H'
s := string(b) // copy again: "Hi"
// Raw string literals: backticks, no escapes, may span lines.
// The Go equivalent of a Java text block (""" ... """).
query := `
SELECT id, email
FROM users
WHERE email LIKE '%@example.com'`Formatting with fmt
fmt.Sprintf is String.format with better verbs for inspecting values. %v prints any value in a default format, which is what you reach for while debugging.
u := User{ID: 7, Name: "Ada"}
fmt.Printf("%v\n", u) // {7 Ada} default format
fmt.Printf("%+v\n", u) // {ID:7 Name:Ada} with field names
fmt.Printf("%#v\n", u) // main.User{ID:7, Name:"Ada"} Go syntax
fmt.Printf("%T\n", u) // main.User the type
fmt.Printf("%q\n", "tab\there") // "tab\there" quoted and escaped
fmt.Printf("%8.2f|\n", 3.14159) // " 3.14|" width 8, 2 decimals
fmt.Printf("%-6s|\n", "ab") // "ab |" left-aligned
fmt.Printf("%x %08b\n", 255, 5) // "ff 00000101"
err := fmt.Errorf("load user %d: %w", u.ID, cause) // %w wraps an errorParsing and converting: strconv
int n = Integer.parseInt("42");
// throws NumberFormatException on bad input
long big = Long.parseLong("9000000000");
double d = Double.parseDouble("2.5");
String s = String.valueOf(99);
boolean ok = Boolean.parseBoolean("true");n, err := strconv.Atoi("42x")
// n = 0, err = strconv.Atoi: parsing "42x": invalid syntax
big, err := strconv.ParseInt("9000000000", 10, 64)
d, err := strconv.ParseFloat("2.5", 64)
s := strconv.Itoa(99) // "99"
ok, err := strconv.ParseBool("true") // "1", "t", "TRUE"… also work