Kodewick
Start learning →
Lesson 9 of 24Contents

Strings and runes

Lesson 9 of 24 · Part 2 · Data · about 15 minutes

Quick question: how many characters are in "café"? Four. Go disagrees:

prog.go
1package main2 3import "fmt"4 5func main() {6	fmt.Println(len("Go"))7	fmt.Println(len("café"))8	fmt.Println(len("你好"))9}
Output
2
5
6

len doesn't count characters; it counts bytes. A Go string is a sequence of bytes, with the text stored in the UTF-8 encoding: plain English letters and digits take one byte each, an accented letter like é takes two, and a Chinese character takes three. So "café" is 3 + 2 = 5, and "你好" ("hello" in Chinese) is 6.

An index gives a byte

s[i] takes byte number i. Its type is byte, and it shows as a number:

prog.go
1package main2 3import "fmt"4 5func main() {6	s := "café"7	fmt.Println(s[0], s[1])8	fmt.Println(string(s[0]))9	fmt.Println(s[3])10	fmt.Println(s[3:5])11}
Output
99 97
c
195
é

The code for c is 99 and a is 97; string(s[0]) turns it back into text. s[3] is the first of the two bytes of é, which means nothing on its own. The slice s[3:5] is exactly those two bytes, so it shows é.

Strings can't be changed

Strings are read-only: you can't change one of their bytes.

prog.go
1package main2 3import "fmt"4 5func main() {6	word := "hello"7	word[0] = 'H'8	fmt.Println(word)9}
Output
# command-line-arguments
./prog.go:7:2: cannot assign to word[0] (neither addressable nor a map index expression)

The compiler says you can't assign to word[0]. For a different string, build a new one, such as word = "H" + word[1:]; the variable word can hold another string, but the old bytes never change. 'H' in single quotes is one character; more on that in the next section.

Walking by character with range

for i, r := range s takes one character at a time: i is the byte where it starts, and r is the character, of type rune (the character's number, also a number).

prog.go
1package main2 3import "fmt"4 5func main() {6	for i, r := range "café" {7		fmt.Println(i, r, string(r))8	}9}
Output
0 99 c
1 97 a
2 102 f
3 233 é

There's no index 4: é takes bytes 3 and 4. r itself shows as a number (é is 233), and string(r) turns it back into text. A character in single quotes, like 'é', is a rune. Press Step and watch how i moves.

[]rune counts characters

[]rune(s) turns a string into a slice of runes, one element per character. Use it to count characters or to take them by position:

prog.go
1package main2 3import "fmt"4 5func main() {6	title := "Crème brûlée"7	runes := []rune(title)8	fmt.Println(len(title), len(runes))9	fmt.Println(string(runes[0]))10	fmt.Println(string(runes[:5]))11}
Output
15 12
C
Crème

string(runes[:5]) turns the first five characters back into a string. Writing title[:5] would take five bytes, which here is only Crèm.

Guess first

What does this show?

1package main2 3import "fmt"4 5func main() {6	s := "€10"7	fmt.Println(len(s), len([]rune(s)))8}

The strings package

The strings package has the everyday string functions. None of them change the string you give them; they return a new one:

prog.go
1package main2 3import (4	"fmt"5	"strings"6)7 8func main() {9	s := "go is fun"10	fmt.Println(strings.Contains(s, "fun"))11	fmt.Println(strings.ToUpper(s))12	fmt.Println(strings.Repeat("-", 9))13	fmt.Println(strings.Count(s, "o"))14}
Output
true
GO IS FUN
---------
1

ToUpper only affects letters that have a capital form; Chinese characters, for example, stay the same.

Splitting and joining: Split cuts a string into a slice at a separator you choose, Fields cuts at spaces (several spaces in a row count as one break), and Join glues a slice into one string with a separator:

prog.go
1package main2 3import (4	"fmt"5	"strings"6)7 8func main() {9	list := "milk,bread,eggs"10	items := strings.Split(list, ",")11	fmt.Println(items, len(items))12	fmt.Println(strings.Join(items, " / "))13 14	words := strings.Fields("  I like   Go  ")15	fmt.Println(words, len(words))16}
Output
[milk bread eggs] 3
milk / bread / eggs
[I like Go] 3

For the word count in lesson 8, strings.Fields is how you'd split a sentence into words first.

Numbers to strings and back

"Total: " + 3 doesn't compile: you can't add a string and a number. The strconv package converts between them: strconv.Itoa turns an int into a string, and strconv.Atoi turns a string into an int.

prog.go
1package main2 3import (4	"fmt"5	"strconv"6)7 8func main() {9	count := 310	label := "Total: " + strconv.Itoa(count) + " items"11	fmt.Println(label)12 13	n, _ := strconv.Atoi("42")14	fmt.Println(n + 1)15}
Output
Total: 3 items
43

Atoi returns two values: the number, and whether something went wrong (when the string isn't a number). For now _ drops the second one; lesson 14 covers it.

strings.Builder builds bit by bit

To build a string piece by piece in a loop, use strings.Builder: sb.WriteString adds to the end (what comes after sb. is a function that belongs to it, called a method; lesson 12 covers them), and String() gives you the whole thing.

prog.go
1package main2 3import (4	"fmt"5	"strconv"6	"strings"7)8 9func main() {10	var sb strings.Builder11	for i := range 5 {12		if i > 0 {13			sb.WriteString(", ")14		}15		sb.WriteString(strconv.Itoa((i + 1) * 10))16	}17	fmt.Println(sb.String())18}
Output
10, 20, 30, 40, 50

The zero value of var sb strings.Builder is ready to use. Joining with += works too, but it builds a new string every time; when you join a lot, Builder is much faster.

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

Bytes and characters

Finish sizes so it returns the number of bytes and the number of characters in the string. It should show the two lines below.

It should print
6 5
6 2
exercise1.go
1package main2 3import "fmt"4 5func sizes(s string) (int, int) {6	return 0, 07}8 9func main() {10	b, c := sizes("naïve")11	fmt.Println(b, c)12	b, c = sizes("你好")13	fmt.Println(b, c)14}
Output
Press Run or Ctrl+Enter
Exercise 2

Tidy up a tag

Finish tidy: make the tag lowercase, drop the extra spaces, and join the words with -. It should show go-web-api and hello-world.

It should print
go-web-api
hello-world
exercise2.go
1package main2 3import (4	"fmt"5	"strings"6)7 8func tidy(tag string) string {9	return strings.ToLower(tag)10}11 12func main() {13	fmt.Println(tidy("  Go  web   API "))14	fmt.Println(tidy("Hello World"))15}
Output
Press Run or Ctrl+Enter
Exercise 3

Fix it: a number in the label

This program doesn't compile. Fix it so it shows Total: 3 items and Total: 12 items.

It should print
Total: 3 items
Total: 12 items
exercise3.go
1package main2 3import "fmt"4 5func label(n int) string {6	return "Total: " + n + " items"7}8 9func main() {10	fmt.Println(label(3))11	fmt.Println(label(12))12}
Output
Press Run or Ctrl+Enter

What you learned

  • A string is a read-only sequence of bytes (UTF-8); len(s) counts bytes: é takes 2, a Chinese character 3.
  • s[i] is a byte (a number); strings can't be changed, so build a new one instead.
  • for i, r := range s walks by character, and r is a rune; string(r) turns it back into text.
  • []rune(s) has one element per character: len([]rune(s)) counts characters, and string(runes[:n]) takes the first n.
  • The strings package: Contains, ToUpper, Repeat, Count, Split, Fields and Join all return new strings.
  • strconv.Itoa turns an int into a string and strconv.Atoi does the reverse (it also returns an error; see lesson 14).
  • To build a string in a loop, use strings.Builder: WriteString, then String().
Finished this lesson?Mark it. Progress is saved in this browser, and you can undo it.