Strings and runes
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:
ToLowermakes everything lower case, andToUpperdoes the opposite.TrimSpaceremoves spaces, tabs and newlines from both ends. The brackets in the example show that the spaces are gone.Containsanswers true or false: does the first string hold the second anywhere?HasPrefixandHasSuffixask 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.Indexgives the byte position where the second string first appears, or-1when it is not there."ph"starts at position 2 ofgopher.Countcounts how many times a piece appears.mississippihas fourss.ReplaceAllswaps every match for something else.Replacetakes one more number: how many matches to swap, here just the first one.TrimPrefixandTrimSuffixremove one piece from the start or end, and only if it is there.Trimremoves any of the listed characters from both ends.Repeatjoins copies of a string end to end.EqualFoldasks 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
%sis a string,%da whole number,%ta bool, and%fa decimal number. A plain%fshows six decimals, so19.99becomes19.990000.%.2fmeans "two digits after the decimal point", and it rounds. This is the verb for money.%qprints a string in double quotes, which makes invisible spaces easy to see.%vmeans "whatever it is, in the default format".%cprints a rune as a symbol,%Uprints its Unicode number, and%xprints a number in base 16.%Tprints the type of a value. A rune literal such as'a'shows up asint32, becauseruneis another name forint32.
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
lento count characters. It counts bytes, solen("über")is 5. Useutf8.RuneCountInStringfor symbols. - Putting a quote inside a string without a backslash, or expecting
\nto 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 instring(...)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 changename. It returns a new string, so keep the result. - Writing
Printfwithout\nand getting everything on one line. - Mixing up
%dand%s. A%!d(string=...)in the output is the clue. - Using
%ffor money and getting19.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:
- Print
len("Gopher")andlen("über"), and read why they differ. - Print
utf8.RuneCountInString("über"), thenlenand the rune count of"go🚀". Addimport "unicode/utf8"first. - Declare
var r rune = 'ü', print it, then printstring(r). Then printstring('m' + 2). You should see252,üando. - With
day := "Wednesday", printstring(day[0]),day[2:6]andday[6:]. Then printday[len(day)-2:]and check it against the last two letters. - Print
unicode.IsUpper('Q'),unicode.IsDigit('x')andstring(unicode.ToUpper('ü')). Addimport "unicode"first. - Take
raw := " Blue Whale "and print the result ofstrings.TrimSpace,strings.ToUpperandstrings.Contains(raw, "Whale"). Then trystrings.ReplaceAll(raw, "e", "3")and read the result. - Make
msg := fmt.Sprintf("%s costs $%.2f", "Coffee", 4.5)and print it. You should seeCoffee costs $4.50. - Print
"She said \"hi\""and the same text as a raw string between backticks, then print a raw string that holdsC:\temp\new. - Break it on purpose:
fmt.Printf("%d\n", "Go")and read the%!d(string=Go)that appears.