Strings, bytes & runes
Every PHP developer has a scar from this. A user called Zoë signs up, something calls substr($name, 0, 3), and the welcome email says hello to Zo�. You learned to reach for mb_strlen and mb_substr, and to squint at any function without the mb_ prefix.
Go has the same underlying model, so you'll recognise the problem. It just gives you a cleaner way out.
| Signup | Name: Zoë |
|---|---|
| Welcome email | substr($name, 0, 3) says "Welcome, Zo�!" |
| Support ticket | "Why is my welcome email haunted?" |
A string is bytes
A Go string is a read-only sequence of bytes. By convention, and because Go source files are always UTF-8, those bytes are UTF-8 text. That's exactly what a PHP string holds, too. So the byte-level operations behave like PHP's byte-level ones:
dest := "Köln" len(dest) // 5, not 4: ö is two bytes dest[0] // 75, a byte (the K) dest[0:2] // "K\xc3": half an ö
Three things to notice:
lenisstrlen. It counts bytes.- Indexing gives a
byte, not a string. Abyteis a small number (it's another name foruint8), sofmt.Println(dest[0])prints75. Wrap it,string(dest[0]), to get"K". - Slicing is
s[start:end], end exclusive. That's notsubstr's start and length:dest[1:3]means bytes 1 and 2. Leave either side out to run to the edge, as indest[:2]ordest[2:].
Strings are also immutable. dest[0] = 'k' is a compile error. You build a new string instead, with +, with += (Go's spelling of .=), or with the strings functions below.
Runes are characters
A rune is one Unicode code point, which for most text means one character. You've met rune literals already: single quotes, 'ö', back in 0.2. Under the hood a rune is an int32 holding the code point.
To work in characters, convert the string to a slice of runes. A slice is Go's list type, which you glimpsed with variadics in 1.2. Unit 2 covers it properly, and for now indexing and len work as you'd expect:
runes := []rune("Köln") len(runes) // 4 string(runes[1]) // "ö" string(runes[:2]) // "Kö"
Here's how the two views line up for "Köln": the five bytes s[0] to s[4] on top, the four runes underneath. The ö is two bytes but one rune:
%%{init: {"flowchart": {"nodeSpacing": 18}}}%%
flowchart TB
accTitle: The string Köln as bytes and as runes
accDescr: The top row is the five bytes of Köln: 75 for K, then 195 and 182, which together make ö, then 108 for l and 110 for n. The bottom row is the four runes, K, ö, l and n. Both of ö's bytes point at the single ö rune.
b0["75"] --> r0["K"]
b1["195"] --> r1["ö"]
b2["182"] --> r1
b3["108"] --> r2["l"]
b4["110"] --> r3["n"]
[]rune(s) replaces the whole mb_ family. Count, index or slice the runes, then convert back with string(...). If you only need the count, utf8.RuneCountInString(s) from the unicode/utf8 package does it without building the slice.
Looping over a string with range, which you'll meet properly in 1.9, also walks it rune by rune. It gives you each rune and the byte position where it starts.
Mind the gap
len and slicing count bytes, so they cut characters in half. s[:10] to "shorten to 10 characters" works on every test string you type, because your test strings are ASCII. Then a Zürich or a Zoë arrives, and you either keep fewer characters than you meant to or slice through the middle of one. When the limit is about what a human sees, convert to []rune first.
The strings package
PHP's str_* and friends are global functions. Go's equivalents live in the strings package: strings.Contains, strings.HasPrefix, strings.Split, strings.Join, strings.ReplaceAll, strings.TrimSpace and plenty more. The Interchange tab has them next to their PHP names.
These are already UTF-8 aware, so there's no second set to remember. One that's quietly better than PHP's: since PHP 8.2, strtoupper('Genève') gives GENèVE, because it only touches ASCII. Go's strings.ToUpper gives GENÈVE. You'd need mb_strtoupper for that in PHP.
Laravel corner
Str::limit($name, 10) is Laravel's answer to the Zoë problem. It uses the mb_ functions underneath, so it never cuts a character in half. Go's strings package has no limit. You write it yourself from []rune, and that's this lesson's exercise.
Tip
Need a string with backslashes or several lines? Use backticks: `C:\trains\eurostar` is taken literally, like a nowdoc. Raw strings are great for SQL and regular expressions. No Go string interpolates variables, raw or not. Lesson 1.6 shows what to use instead.
Recap
- A string is read-only UTF-8 bytes.
len,s[i]ands[a:b]all work in bytes. s[i]is abyte(a number).s[a:b]is start to end, not start and length.- A rune is a character.
[]rune(s)lets you count, index and slice by character. Thenstring(...)converts back. - The
stringspackage has thestr_*functions, already UTF-8 safe. - Backticks make raw strings. Nothing interpolates.