All parts · this is part 2
- 1.Introduction to Go
- 2.Constants, Variables and Basic Data Types
- 3.Functions and Control Structures
- 4.Arrays, Slices, Maps and Loops
- 5.Strings, Runes and Bytes
- 6.Structs and Interfaces
- 7.Pointers
- 8.Goroutines
- 9.Channels
- 10.Generics
- 11.Building an API
Extras
Declaring variables
To make a variable you use the var keyword, then the name, then the type. But there are a few shortcuts too:
package main
import "fmt"
func main() {
var intNum int // declared, set to int's zero value: 0
var name string = "Gopher" // type and value
var count = 10 // type inferred: int
city := "Toronto" // short form: inferred, no var
var a, b int = 1, 2 // several at once
x, y := "x", 3.5 // they can even be different types
fmt.Println(intNum, name, count, city, a, b, x, y)
}Notice I'm printing everything at the end. That's because, just like imports, if you declare a variable and never use it, Go won't compile. It's part of Go keeping things simple: no pointless variables hanging around in your code.
A couple of gotchas with :=:
package main
import "fmt"
var version = "1.0" // fine: var works outside functions
// build := 42 // won't compile: := only works inside a function
func main() {
n := 1
n, m := 2, 3 // fine: m is new, so := is allowed, and n is just reassigned
fmt.Println(version, n, m)
}Practice
- Practice with AI
PracticeWhen does := work?
This program uses := outside a function, and twice in a row on the same name, so it won't compile. Fix both, and say what each error means.
- Practice with AI
Warm-upFour ways to make 42
Make an int that holds 42 in four different ways and print them all on one line.
Integers and their sizes
Go doesn't just have one integer type, it has a bunch, and the difference is how much memory they use. Bigger types can hold bigger numbers but take up more space. There are also unsigned ones (uint) that can only be positive, which means they can go twice as high with the same memory.
| Type | Bits | Range |
|---|---|---|
| int8 | 8 | -128 to 127 |
| int16 | 16 | -32,768 to 32,767 |
| int32 | 32 | about ±2.1 billion |
| int64 | 64 | about ±9.2 quintillion |
| uint8 (byte) | 8 | 0 to 255 |
| uint16 / uint32 / uint64 | 16 / 32 / 64 | 0 up to about twice the signed version's max |
| int / uint | 32 or 64 | Depends on your machine, which these days is pretty much always 64 |
Now here's where it gets fun. If you try to put a number that's too big into a type, like 32,768 into an int16, the compiler catches it. But only if it can see it. If the number gets too big while your program is running, Go doesn't throw an error. It just wraps around and keeps going, and now you've got a weird number and no idea where it came from.
package main
import "fmt"
func main() {
// var big int16 = 32768 // compile error: 32768 overflows int16
var n int16 = 32767
n = n + 1
fmt.Println(n) // -32768: no error, it just wraps around
var u uint8 = 0
u--
fmt.Println(u) // 255
}Moral of the story: think about what values you might actually run into and pick types that fit. It'll save you some late night debugging sessions.
Practice
- Practice with AI
PracticeWatch it wrap
An int8 starts at 120 and has 1 added to it ten times, printing each time. Guess what it prints, then run it. Why didn't the compiler catch it?
- Practice with AI
StretchAdd without overflowing
Write addInt8(a, b int8) (int8, bool) that gives back false instead of a wrapped number when the answer doesn't fit.
Floating-point numbers
For decimal numbers you've got float32 and float64. There's no plain float, you have to pick one. float64 holds bigger and more precise numbers, and it's what you get by default if you write something like x := 2.5.
Thing is, most decimals can't be stored exactly on a computer, so you get the closest number the type can hold. With 32 bits that's often noticeably off:
package main
import "fmt"
func main() {
var f32 float32 = 12345678.9
var f64 float64 = 12345678.9
fmt.Println(f32) // 1.2345679e+07
fmt.Println(f64) // 1.23456789e+07
fmt.Println(0.1+0.2 == 0.3) // true: these are constants, worked out exactly
a, b := 0.1, 0.2
fmt.Println(a+b == 0.3) // false: a+b is 0.30000000000000004
}That last one is the one that'll get you. a + b is actually 0.30000000000000004, so it's not equal to 0.3. If you're comparing floats after doing math on them, don't use ==. Check if they're close enough instead:
package main
import (
"fmt"
"math"
)
func almostEqual(a, b float64) bool {
return math.Abs(a-b) < 1e-9
}
func main() {
a, b := 0.1, 0.2
fmt.Println(almostEqual(a+b, 0.3)) // true
}Practice
- Practice with AI
PracticeTen cents at a time
Add 0.1 to a float64 ten times and compare it to 1.0. Then do the same thing in cents with an int64. Which one gets it right?
Arithmetic and type conversion
You can add, subtract, multiply and all that stuff like you'd expect, but there's a catch: you can't mix types. You can't add an int and a float32, and you can't even multiply an int32 by an int64. If you need to, you convert one of them, which looks like calling the type as a function:
package main
import "fmt"
func main() {
var count int = 3
var price float32 = 2.5
// total := count * price // won't compile: int and float32
total := float32(count) * price // convert one side
fmt.Println(total) // 7.5
fmt.Println(int(3.9 * float64(count))) // 11: converting to int drops the decimals
fmt.Println(3 / 2) // 1
fmt.Println(-3 / 2) // -1: rounds toward zero, not down
fmt.Println(7 % 3) // 1
fmt.Println(-7 % 3) // -1: the remainder takes the sign of the left side
fmt.Println(3.0 / 2) // 1.5: a float on either side gives float division
}A few things worth pointing out in there:
- Integer division gives you an integer.
3 / 2is1, and the decimal just gets chopped off. Careful with negatives though:-3 / 2is-1, not-2, because Go rounds toward zero. %gives you the remainder, and it comes out negative if the left side is negative. So if you're checking whether something's odd, usen%2 != 0.n%2 == 1will lie to you for negative numbers.- Converting a float to an int doesn't round, it just drops everything after the decimal.
There are also shorthands like x += 2, x -= 2, x *= 2 and x /= 2, plus x++ and x-- to add or subtract one. One weird thing: in Go, x++ is its own statement, so something like y := x++ won't work.
Practice
- Practice with AI
Warm-upAverage of three
Fill in average(a, b, c int) float64 so it gives back the average of three int scores. 1, 2 and 2 should give you 1.666…, not 1.
- Practice with AI
PracticeMinutes and seconds
Fill in split(total int) (minutes, seconds int) so 135 gives back 2 and 15, using / and %.
- Practice with AI
StretchOdd numbers, negatives too
Write isOdd(n int) bool that works for -3, 0 and 7. Why doesn't n%2 == 1 work?
Strings, at first glance
This might blow your mind, but you use the string type to store strings. You can make one with double quotes or back quotes. Double quotes have to stay on one line, so if you want a line break you use \n. Back quotes print exactly what you type, line breaks and all, which is super handy for stuff like JSON or SQL.
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
greeting := "Hello\nWorld" // \n is a newline
raw := `Hello
World` // back quotes: printed exactly as typed, \n and all
fmt.Println(greeting)
fmt.Println(raw)
fmt.Println("Hello" + " " + "Gopher") // + joins strings
fmt.Println(len("A")) // 1
fmt.Println(len("γ")) // 2: len counts bytes, and γ takes two
fmt.Println(utf8.RuneCountInString("γ")) // 1: counts characters
var r rune = 'a' // single quotes make a rune: one character
fmt.Println(r) // 97: a rune is a number
}Now here's where strings get a little tricky. len doesn't give you the number of characters, it gives you the number of bytes. Go stores strings as UTF-8, where normal English letters take one byte but stuff like accents, Greek letters or emoji take two to four. So if you actually want the number of characters, use utf8.RuneCountInString.
Practice
- Practice with AI
Warm-upTwo kinds of quotes
Print a two-line poem with double quotes and \n, then again with back quotes. Make them come out exactly the same.
- Practice with AI
PracticeCount the characters
Guess len and the rune count for "hello", "héllo" and "日本", then run it. Why are the numbers different?
Booleans and zero values
Booleans are easy. They're true or false. Congrats, you now understand booleans in Go.
One thing if you're coming from Python or JavaScript: Go doesn't treat 0 or "" as false. So if count { } won't compile. You have to write if count != 0.
Also, so far we've mostly set variables when we made them, but you don't have to. If you don't, Go gives the variable a default value based on its type, called its zero value:
package main
import "fmt"
func main() {
var i int
var f float64
var s string
var b bool
var r rune
fmt.Printf("%v %v %q %v %v\n", i, f, s, b, r) // 0 0 "" false 0
}| Type | Zero value |
|---|---|
| Integers, floats, runes | 0 |
| string | "" (the empty string) |
| bool | false |
| Pointers, slices, maps, errors | nil (parts 3, 4 and 7) |
This actually comes in handy a lot. A counter starts at 0 without you doing anything, and structs (part 6) are often ready to use as soon as you declare them.
Practice
- Practice with AI
Warm-upGuess the zero value
Declare a uint8, float32, string, bool and rune without giving them values. Guess what each one prints, then check.
- Practice with AI
PracticeNo truthy values here
Fix if name { … } and if !count { … } so they compile but still do what the person meant.
Constants
Constants are like variables, except once you set them you can't change them. Everything else we talked about still applies, with one catch: you can't just declare a constant, you have to give it a value right away.
package main
import "fmt"
const pi = 3.14159
// const later int // won't compile: a constant needs its value right away
func main() {
const radius = 2
area := pi * radius * radius // radius is untyped, so it works with pi
fmt.Println(area) // 12.56636
// pi = 3 // won't compile: you can't assign to a constant
}They're great for values nobody should be messing with later. Take pi. There's no reason for anyone to change it, and if someone did, every calculation using it would be off.
Something you might notice: pi * radius works even though pi looks like a float and radius looks like an int. That's because constants without a type written out are untyped, so they just become whatever type they need to be. Two variables with those same values wouldn't multiply.
One more trick. If you need a bunch of related numbers in a row, like days of the week, Go has iota. In a const block it starts at 0 and goes up by one each line, and if you leave a line without a value it reuses the one above:
package main
import "fmt"
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
)
const (
KB = 1 << (10 * (iota + 1)) // 1024
MB // 1048576
GB // 1073741824
)
func main() {
fmt.Println(Sunday, Monday, Tuesday)
fmt.Println(KB, MB, GB)
}Practice
- Practice with AI
PracticeCircle math
The template has const pi and area(radius int) float64. pi * radius * radius compiled when radius was a constant, but radius is a variable now and it doesn't, even though pi is still a constant. Explain the error, then fix it.
- Practice with AI
PracticeSizes with iota
Use iota to make Small, Medium and Large. Then use iota for KB, MB and GB constants, and give back how many bytes are in 3 MB.