Strings and runes
Quick question: how many characters are in "café"? Four. Go disagrees:
1package main2 3import "fmt"4 5func main() {6 fmt.Println(len("Go"))7 fmt.Println(len("café"))8 fmt.Println(len("你好"))9}
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:
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}
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.
1package main2 3import "fmt"4 5func main() {6 word := "hello"7 word[0] = 'H'8 fmt.Println(word)9}
# 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).
1package main2 3import "fmt"4 5func main() {6 for i, r := range "café" {7 fmt.Println(i, r, string(r))8 }9}
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:
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}
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.
What does this show?
1package main2 3import "fmt"4 5func main() {6 s := "€10"7 fmt.Println(len(s), len([]rune(s)))8}
€ takes 3 bytes in UTF-8, and 10 takes 2, so len(s) is 5. As a []rune there is one element per character: €, 1 and 0, so 3.
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:
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}
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:
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}
[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.
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}
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.
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}
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.
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.
6 5 6 2
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}
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.
go-web-api hello-world
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}
Fix it: a number in the label
✓This program doesn't compile. Fix it so it shows Total: 3 items and Total: 12 items.
Total: 3 items Total: 12 items
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}
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 abyte(a number); strings can't be changed, so build a new one instead.for i, r := range swalks by character, andris arune;string(r)turns it back into text.[]rune(s)has one element per character:len([]rune(s))counts characters, andstring(runes[:n])takes the first n.- The
stringspackage:Contains,ToUpper,Repeat,Count,Split,FieldsandJoinall return new strings. strconv.Itoaturns an int into a string andstrconv.Atoidoes the reverse (it also returns an error; see lesson 14).- To build a string in a loop, use
strings.Builder:WriteString, thenString().