Course outline
Course / The terminal and the language

Strings and runes

Reading

The idea

Picture a string as a necklace of beads. Each bead is one small number, and the necklace is a whole piece of text. English letters take one bead each, but many other symbols take two, three or four beads that must stay together, like a charm made of several beads. Most of the surprises in this lesson come from forgetting which one you are counting: beads or charms.

This lesson has six parts: how Go stores text, how to write quotes and backslashes inside it, how it names a whole symbol, how to pick out parts of a string, the ready-made tools in the strings package, and how to fill in a template with fmt. The snippets go inside func main(), and each one shows the imports it needs.

Strings are bytes

A byte is a whole number from 0 to 255. You met that range in the integer sizes lesson: a byte is another name for uint8. Go stores every string as a row of bytes, one bead per byte. The built-in len tells you the length of what you give it. For a string, len counts bytes.

animal := "Gopher"
fmt.Println(animal, len(animal))
german := "über"
fmt.Println(german, len(german))
Gopher 6
über 5

"Gopher" has six letters and six bytes. "über" has four letters but len says 5, because the ü is a charm made of two beads. English letters, digits and common punctuation use one byte each. Many accented letters use two bytes, and symbols such as € or Chinese characters use three.

Some bytes are invisible. Inside double quotes, a backslash starts an escape sequence: \n is one byte that starts a new line, and \t is one byte that jumps to the next tab stop.

note := "one\ntwo"
fmt.Println(len(note))
fmt.Println(note)
7
one
two

The note has seven bytes: one, the newline and two. Println printed the newline as a line break.

A string is also read-only. You cannot change a bead in place:

s := "Gopher"
s[0] = 'J'
./main.go:9:2: cannot assign to s[0] (neither addressable nor a map index expression)

The first part is the file, line and column. Then Go says it cannot assign to s[0], the first byte of s. To get a different string you build a new one, for example by joining pieces with +. The strings package later in this lesson does that for you.

Escapes and raw strings

A string starts and ends with a double quote, so a quote in the middle needs a backslash in front of it, \", to say "this one belongs to the text". A backslash itself is written \\.

fmt.Println("She said \"hi\"")
fmt.Println("C:\\temp")
She said "hi"
C:\temp

Each pair, \" and \\, counts as one byte. Lots of backslashes get hard to read, so Go has a second kind of string, the raw string, written between backticks (the key left of 1). Nothing inside it is treated specially: no escapes at all, and it may span several lines.

fmt.Println(`C:\temp\new`)
fmt.Println(`{"user": "ada", "id": 7}`)
msg := `line one
line two`
fmt.Println(msg)
C:\temp\new
{"user": "ada", "id": 7}
line one
line two

In a normal string \n would have become a line break. Inside backticks it stays two characters, a backslash and an n. Raw strings are the tidy way to write file paths, JSON and anything else full of quotes and backslashes.

Runes and unicode

The necklace is made of beads, but people think in whole symbols. Unicode is a worldwide catalogue that gives every symbol, from a to € to 👋, its own number, called a code point. Go calls one code point a rune. UTF-8 is the way Go turns those numbers into beads: one to four bytes per rune.

A rune literal uses single quotes, which is what the earlier lesson meant when it said single quotes are different from double quotes.

var r rune = 'ü'
fmt.Println(r)
fmt.Println(string(r))
252
ü

A rune is really a number, so Println shows 252, the catalogue number of ü. Converting it with string(r) turns that number back into the symbol. This is the conversion the type conversion lesson warned about: string(42) gives *, the symbol numbered 42. Here that is exactly what you want. Because runes are numbers, you can do arithmetic on them:

next := 'm' + 1
fmt.Println(next, string(next))
fmt.Println(string(rune(9731)))
110 n
☃

'm' is 109, so 'm' + 1 is 110, and 110 is n. rune(9731) is the same conversion syntax as float64(total), and 9731 is the number of the snowman symbol.

The unicode package has ready-made questions you can ask about a single rune. Add import "unicode" to use them:

fmt.Println(unicode.IsLetter('ü'), unicode.IsUpper('Q'), unicode.IsLower('Q'))
fmt.Println(unicode.IsDigit('7'), unicode.IsSpace(' '))
fmt.Println(string(unicode.ToUpper('ü')), string(unicode.ToLower('Q')))
true true false
true true
Ü q

IsLetter, IsUpper, IsLower, IsDigit and IsSpace answer yes or no, and ToUpper and ToLower return the converted rune. They understand every alphabet, not just English, which a hand-written range check like r >= 'a' && r <= 'z' does not.

To count symbols instead of bytes, use utf8.RuneCountInString from the unicode/utf8 package. Bring it in with import "unicode/utf8" next to your other imports.

word := "über"
fmt.Println(len(word), utf8.RuneCountInString(word))
love := "I♥Go"
fmt.Println(len(love), utf8.RuneCountInString(love))
rocket := "go🚀"
fmt.Println(len(rocket), utf8.RuneCountInString(rocket))
5 4
6 4
6 3

In each line the first number counts beads and the second counts charms. über has 4 symbols in 5 bytes. In I♥Go, the heart takes 3 bytes and the other three symbols take 1 each, so 6 bytes for 4 symbols. The rocket emoji alone is 4 bytes. When you need "how long is this text to a person", use the rune count. When you need "how many bytes will this take", use len.

Indexing and slicing strings

Every bead has a position, and positions start at 0. Indexing means pointing at one position with square brackets, and slicing means cutting out a piece between two positions, as if with scissors.

day := "Wednesday"
fmt.Println(day[0])
fmt.Println(string(day[0]))
fmt.Println(string(day[len(day)-1]))
87
W
y

day[0] is a single byte, so Println shows its number, 87, the code for W. Wrapping it in string(...) turns the number into a symbol. The last position is always len(s)-1, because counting starts at 0.

Cutting a piece uses s[start:end]. The piece begins at start and stops just before end:

day := "Wednesday"
fmt.Println(day[2:6])
fmt.Println(day[:4], day[6:])
dnes
Wedn day

day[2:6] takes positions 2 to 5. Leaving out the start means "from the beginning", and leaving out the end means "to the end", so day[6:] is everything from position 6 on.

Positions are bead positions, not charm positions, so be careful with non-English text:

w := "señor"
fmt.Println(w[0:4])
fmt.Println(w[0:3])
fmt.Println(w[2])
señ
se�
195

w[0:4] takes s and e (1 byte each) and ñ (2 bytes), so the charm is whole. w[0:3] cuts through the middle of the ñ, and the terminal shows �, the replacement symbol for broken text. w[2] is only the first half of the ñ, the byte 195. Slicing and indexing are safe on plain English text. With other scripts, cut where whole symbols end.

Asking for a position past the end of the string stops your program with an "index out of range" error. Keep your positions between 0 and len(s).

The strings package

You do not have to build text tools yourself. The strings package is a drawer of ready-made tools. Bring it in with import "strings". Each function gives you a new string and never changes the original, which fits with strings being read-only.

name := "Linus"
shout := strings.ToUpper(name)
fmt.Println(name, shout)
Linus LINUS

name is still Linus. The upper-case copy is a separate string. These are the tools you will use most:

raw := "  Blue Whale  "
fmt.Println(strings.ToLower("RustLang"))
fmt.Println("[" + strings.TrimSpace(raw) + "]")
fmt.Println(strings.Contains("gopher", "ph"), strings.Contains("gopher", "xyz"))
fmt.Println(strings.HasPrefix("invoice-42.pdf", "invoice"), strings.HasSuffix("invoice-42.pdf", ".txt"))
fmt.Println(strings.Index("gopher", "ph"), strings.Index("gopher", "z"))
fmt.Println(strings.Count("mississippi", "s"))
fmt.Println(strings.ReplaceAll("banana", "a", "o"))
fmt.Println(strings.Replace("banana", "a", "o", 1))
fmt.Println(strings.TrimPrefix("Bearer abc123", "Bearer "), strings.TrimSuffix("backup.tar.gz", ".gz"), strings.Trim("--hi--", "-"))
fmt.Println(strings.Repeat("ab", 3))
fmt.Println(strings.EqualFold("RustLang", "rustlang"))
rustlang
[Blue Whale]
true false
true false
2 -1
4
bonono
bonana
abc123 backup.tar hi
ababab
true

Read them in order:

  • ToLower makes everything lower case, and ToUpper does the opposite.
  • TrimSpace removes spaces, tabs and newlines from both ends. The brackets in the example show that the spaces are gone.
  • Contains answers true or false: does the first string hold the second anywhere?
  • HasPrefix and HasSuffix ask whether a string starts or ends with something. They are the tools for checking how a name begins or what kind of file it is, such as .pdf.
  • Index gives the byte position where the second string first appears, or -1 when it is not there. "ph" starts at position 2 of gopher.
  • Count counts how many times a piece appears. mississippi has four ss.
  • ReplaceAll swaps every match for something else. Replace takes one more number: how many matches to swap, here just the first one.
  • TrimPrefix and TrimSuffix remove one piece from the start or end, and only if it is there. Trim removes any of the listed characters from both ends.
  • Repeat joins copies of a string end to end.
  • EqualFold asks whether two strings match while ignoring upper and lower case.

A few more tools return more than one result. strings.Cut arrives in the functions lesson, and strings.Split, strings.Fields and strings.Join, which cut a line into words and glue words back together, arrive with slices.

fmt verbs and Sprintf

So far you have joined text with + or let Println put spaces between values. A template is better when you have several pieces to fit into one sentence: think of a form letter with blanks that get filled in. In fmt, the blanks are called verbs. Each starts with % and says what kind of value goes there.

player := "Linus"
points := 42
fmt.Printf("%s scored %d points\n", player, points)
msg := fmt.Sprintf("%s scored %d points", player, points)
fmt.Println(msg)
Linus scored 42 points
Linus scored 42 points

%s is the blank for a string and %d is the blank for a whole number. The values after the template fill the blanks in order. Printf prints the result, but it does not add a new line, so the template ends with \n. Sprintf does not print anything: it hands the finished text back as a string, which you can keep in a variable or return from a function. The S stands for string. You will use Sprintf the most.

Here are the verbs you will use most:

fmt.Printf("%s|%d|%f|%t|%q|%v\n", "Go", 42, 19.99, true, "hi", 3.5)
fmt.Printf("%.2f %.1f\n", 19.999, 3.14159)
fmt.Printf("%c %U %x\n", 233, 233, 255)
fmt.Printf("%T %T %T %T %T\n", 42, 4.2, "x", true, 'a')
Go|42|19.990000|true|"hi"|3.5
20.00 3.1
é U+00E9 ff
int float64 string bool int32
  • %s is a string, %d a whole number, %t a bool, and %f a decimal number. A plain %f shows six decimals, so 19.99 becomes 19.990000.
  • %.2f means "two digits after the decimal point", and it rounds. This is the verb for money.
  • %q prints a string in double quotes, which makes invisible spaces easy to see.
  • %v means "whatever it is, in the default format".
  • %c prints a rune as a symbol, %U prints its Unicode number, and %x prints a number in base 16.
  • %T prints the type of a value. A rune literal such as 'a' shows up as int32, because rune is another name for int32.

A number between % and the verb sets a width, which pads the output to line up columns. A - pads on the right, and a 0 pads with zeros. To print a real percent sign, write %%.

fmt.Printf("[%5d] [%-5d] [%05d]\n", 42, 42, 42)
fmt.Printf("[%8s] [%-8s]\n", "Go", "Go")
fmt.Printf("%d%%\n", 90)
[   42] [42   ] [00042]
[      Go] [Go      ]
90%

%5d means "at least 5 characters wide", right-aligned. %-5d is left-aligned, and %05d fills the empty space with zeros.

If the verb and the value do not match, fmt does not stop. It prints what went wrong right inside the output:

fmt.Printf("%d\n", "Go")
fmt.Printf("%d %d\n", 1)
fmt.Printf("%d\n", 1, 2)
%!d(string=Go)
1 %!d(MISSING)
1
%!(EXTRA int=2)

Read %!d(string=Go) as "the verb %d was given a string with the value Go". %!d(MISSING) means there was a blank but no value to fill it. %!(EXTRA int=2) means a value had no blank. Whenever you see %! in output, check that your verbs and your values line up.

Common mistakes

  • Using len to count characters. It counts bytes, so len("über") is 5. Use utf8.RuneCountInString for symbols.
  • Putting a quote inside a string without a backslash, or expecting \n to break a line inside backticks.
  • Using double quotes for a single rune. 'é' is a rune and "é" is a string.
  • Printing s[0] and expecting a letter. Indexing gives a number. Wrap it in string(...) for a letter.
  • Forgetting that the end of s[start:end] is not included, and counting positions from 1 instead of 0.
  • Cutting a string between the bytes of one symbol. The result shows �.
  • Expecting strings.ToUpper(name) to change name. It returns a new string, so keep the result.
  • Writing Printf without \n and getting everything on one line.
  • Mixing up %d and %s. A %!d(string=...) in the output is the clue.
  • Using %f for money and getting 19.990000. Use %.2f.

Try it

The labs read text from variables that sit outside the functions, and the tests set those variables to different values, including ones with accents and emoji. Try these in your own main.go first:

  1. Print len("Gopher") and len("über"), and read why they differ.
  2. Print utf8.RuneCountInString("über"), then len and the rune count of "go🚀". Add import "unicode/utf8" first.
  3. Declare var r rune = 'ü', print it, then print string(r). Then print string('m' + 2). You should see 252, ü and o.
  4. With day := "Wednesday", print string(day[0]), day[2:6] and day[6:]. Then print day[len(day)-2:] and check it against the last two letters.
  5. Print unicode.IsUpper('Q'), unicode.IsDigit('x') and string(unicode.ToUpper('ü')). Add import "unicode" first.
  6. Take raw := " Blue Whale " and print the result of strings.TrimSpace, strings.ToUpper and strings.Contains(raw, "Whale"). Then try strings.ReplaceAll(raw, "e", "3") and read the result.
  7. Make msg := fmt.Sprintf("%s costs $%.2f", "Coffee", 4.5) and print it. You should see Coffee costs $4.50.
  8. Print "She said \"hi\"" and the same text as a raw string between backticks, then print a raw string that holds C:\temp\new.
  9. Break it on purpose: fmt.Printf("%d\n", "Go") and read the %!d(string=Go) that appears.

Something wrong with this lesson? Report a problem