[Book] Learning Go
Last Update:
Word Count:
Read Time:
El libro
Introduction
This article is used to keep notes and summaries of the book “Learning Go”.
The content will be continuously updated as I read through the book.
Chapter.1 - Setting Up Your Go Environment
The go command
1 | |
1 | |
1 | |
This creates an executable called hello (or hello.exe on Windows) in the current directory.
Makefiles
Here’s a sample Makefile to add to our very simple project:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17.DEFAULT_GOAL := build
fmt:
go fmt ./...
.PHONY:fmt
lint: fmt
golint ./...
.PHONY: lint
vet: fmt
go vet ./...
.PHONY:vet
build: vet
go build hello.go
.PHONY:build
Each possible operation is called a target. The .DEFAULT_GOAL defines which target is run when no target is specified. In our case, we are going to run the build target.
The world before the colon (:) is the name of the target. Any words after the target (like vet in the line build: vet) are the other targets that must be run before the specified target runs. The taks that are performed by the target are on the indented lines after the target (The)
Once theMakefile is in your directory, type:1
> make
Chapter.2 - Primative Types and Declarations
Complex Number
1 | |
var vs. :=
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
const
Code:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const x int64 = 10
const (
idKey = "id"
nameKey = "name"
)
const z = 20 * 10
func main() {
const y = "hello"
fmt.Println(x)
fmt.Println(y)
x = x + 1
y = "bye"
fmt.Println(x)
fmt.Println(y)
}
Output:1
2
3
4./prog.go:21:2: cannot assign to x (neither addressable nor a map index expression)
./prog.go:22:2: cannot assign to y (neither addressable nor a map index expression)
Go build failed.
Typed and Untyped Constant
Untyped constant:1
const x = 10
All of the following assignments are legal:1
2
3var y int = x
var x float64 = x
var d byte = x
Here’s what a typed constant declaration looks like:1
const typedX int = 10
This constant can only be assigned directly to an int. Assigning it to any other type produces a compile-time error.
Unused Variables
1 | |
The Go compiler allows you to create unread constants with const. This is because constants in Go are calculated at compile time and cannot have any side effects. This makes them easy to eliminate: if a constant isn’t used, it is simply not included in the compiled binary.
Naming Variables and Constants
1 | |
asd
While this code works DO NOT name your variable like this.
Chapter.3 - Composite Types
Arrays——Too Rigid to Use Directly
Arrays are rarely used directly in Go.
All of the elements in the array must be of the type that’s specified (this doesn’t mean they are always of the same type). There are a few different declaration styles.1
var x [3]int
1 | |
1 | |
When using an array literal to initialize an array, you can leave off the number and use ... instead:1
var x = [...]int{10, 20, 30}
You can use == and != to compare arrays:1
2
3var x = [...]int{1, 2, 3}
var y = [3]int{1, 2, 3}
fmt.Println(x == y) // prints true
Go only has one-dimensional arrays, but you can simulate multidimensional arrays:1
var x [2][3]int
This declares x to be an array of length 2 whose type is an array of ints of length 3.
Note that you cannot read or write past the end of an array or use negative index. If you do this with a constant or literal index, it is a compile-time error.
Finally, the built-in funciton len takes in an array and returns its length:1
fmt.Println(len(x))
Go considers the size of the array to be part of the type of the array. This makes an array that’s declared to be [3]int a different type from an array that’s declared to be [4]int. This also means that you cannot use a variable to specify the size of an array, because types must be resolved at compile time, not at runtime.
You cannot use a type conversion to convert arrays of different sizes to identical types.
Slices
Most of the time, when you want a data structure that holds a sequence of values, a slice is what you should use. WHat makes slices so useful is that the length is not part of the type for a slice.
1 | |
Using
[...]makes an array. Using[]makes a slice.
1 | |
You can simulate multidimensional slices and make a slice of slices:1
var x [][]int
1 | |
In Go, nil is an identifier that represents the lack of a value for some types. Like the untyped numeric constants we saw in the previous chapter,nil has no type, so it can be assigned or compared against values of different types. A nil slice contains nothing.
A slice is the type that isn’t comparable. It is a compile-time error to use == to see if two slices are identical or != to see if they are different. The only thing you can compare a slice with is nil:
1 | |
append
The built-in append function is used to grow slices1
2var x []int
x = append(x, 10)
The append function takes at least two parameters, a slice of any type and a value of that type. It returns a slice of the same type.1
2var x = []int{1, 2, 3}
x = append(x, 4)
You can append more than one value at a time:1
x = append(x, 5, 6, 7)
Capacity
1 | |
make
This built-in make function allows us to create an empty slice that already has a length or capacity specified. It allows us to specify the type, length, and, optionally, he capacity.1
x := make([]int, 5)
1 | |
1 | |
You can also create a slice with zero length, but a capacity that’s greater than zero:1
x := make([]int, 0, 10)
In this case, we have a non-nil slice with a length of 0, but a capacity of 10. Since the length is 0, we can’t directly index into it, but we can append values to it:1
2x := make([]int, 0, 10)
x = append(x, 5, 6, 7, 8)
The value of x is now [5 6 7 8], with a length of 4 and a capacity of 10.
Never specify a capacity that’s less than the length! It is a compil-time error to do so with a constant or numeric literal. If you use a variable to specify a capacity that’s smaller than the length, your program will panic at runtime.
Declaring Yoyr Slice
1 | |
You can create a slice using an empty slice literal:1
var x = []int{}
Declaring a slice with default values1
data := []int{2, 4, 6, 8} //numbers we appreciate
Slicing Slices
A slice expression creates a slice from a slice. It’s written inside brackets and consists of a starting offset and an ending offset, separated by a colon (:).1
2
3
4
5
6
7
8
9
10x := []int{1, 2, 3, 4}
y := x[:2]
z := x[1:]
d := x[1:3]
e := x[:]
fmt.Println("x:", x)
fmt.Println("y:", y)
fmt.Println("z:", z)
fmt.Println("d:", d)
fmt.Println("e:", e)
Output:1
2
3
4
5x: [1 2 3 4]
y: [1 2]
z: [2 3 4]
d: [2 3]
e: [1 2 3 4]
When you take a slice from a slice, you are NOT making a copy of the data. Instead, you now have two variables that are sharing memory. This means that changes to an element in a slice affect al slices that share that element.
1 | |
Output:1
2
3x: [10 20 30 4]
y: [10 20]
z: [20 30 4]
Slicing slices gets extra confusing when combined with append:1
2
3
4
5
6
7x := []int{1, 2, 3, 4}
y := x[:2]
fmt.Println(cap(x), cap(y))
y = append(y, 30)
fmt.Println("x: ", x)
fmt.Println("y: ", y)
Output:1
2
34 4
x: [1 2 30 4]
y: [1 2 30]
Whenever you take a slice from another slice, the subslice’s capacity is set to the capacity of the original slice, minus the offset of the subslice within the original slice. This means that any unused capacity in the original slice is also shared with any subslices.
When we make the y slice from x, the length is set to 2, but the capacity is set to 4, the same as x. Since the capacity is 4, appending onto the end of y puts the value in the third position of x.
This behavior creates some very odd scenarios, with multiple slices appending and overwriting each other’s data:
1 | |
Output:1
2
3
45 5 3
x: [1 2 30 40 70]
y: [1 2 30 40 70]
z: [30 40 70]
To avoid complicated slice situations, you should either never use append with a subslice or make sure that append doesn’t cause an overwrite by using a full slice expression:1
2y := x[:2:2]
z := x[2:4:4]
Converting Arrays to Slices
1 | |
Output:1
2
3x: [10 6 7 8]
y: [10 6]
z: [7 8]
copy
1 | |
Output:1
[1 2 3 4] 4
1 | |
1 | |
1 | |
Output:1
2[5 6]
[1 2 3 4]
Strings and Runes and Bytes
1 | |
1 | |
Maps
map[keyType]valueType
1var nilMap map[string]int
1totalWins := map[string]int{}
1
2
3
4
5teams := map[string][]string {
"Orcas": []string{"Fred", "Ralph", "Bijou"},
"Lions": []string{"Sarah", "Peter", "Billie"},
"Kittens": []string{"Waldo", "Raul", "Ze"},
}
1ages := make(map[int][]string, 10)
- Maps automatically grow as you add key-value pairs to them.
- If you know how many key-value pairs you plan to insert into a map, you can use
maketo create a map with a specific initial size. - Passing a map to the
lenfunction tells you the number of key-value pairs in a map. - The zero value for a map is
nil - Maps are not comparable. You can check if they are equal to
nil, but you cannot check if two maps have identical keys and values using==or differ using!=
The key for a map can be any comparable type. This means you cannot use a slice or a map as the key for a map.
Learn more about hash map: GopherCon 2016, Inside the
Map Implementation.
Reading and Writing a Map
1 | |
The comma ok Idiom
1 | |
Rather than assign the result of a map read to a single variable, with the comma ok idiom you assign the results of a map read to two variables. The first gets the value associated with the key. The second value returned is a bool. It is usually named ok. If ok is true, the key is present in the map. If ok is false, the key is not present.
Deleting from Maps
1 | |
Using Maps as Sets
1 | |
Structs
Go doesn’t have classes, because it doesn’t have inheritance. This doesn’t mean Go doesn’t have some of the features of object-oriented languages, it just does things a little differently.
1 | |
1 | |
1 | |
1 | |
1 | |
1 | |
Anonymous Structs
1 | |
Comparing and Converting Structs
Whether or not a struct is comparable depends on the struct’s field. Structs that are entirely composed of comparable types are comparable; those with slice or mmap fields are not.
1 | |
We can use a type conversion to convert an instance of firstPerson to secondPerson, but we cannot use == to compare an instance of firstPerson and an instance of secondPerson, because they are different types:1
2
3
4type secondPerson struct {
name string
age int
}
We can’t convert an instance of firstPerson to thirdPerson, because the fields are in different order:1
2
3
4type thirdPerson struct {
age int
name string
}
We can’t convert an instance of firstPerson to fourthPerson, because the fields names don’t match:1
2
3
4type fourthPerson struct {
firstName string
age int
}
we can’t convert an instance of firstPerson to fifthPerson because there’s
an additional field:1
2
3
4
5type fifthPerson struct {
name string
age int
favoriteColor string
}
Anonymous structs add a small twist to this: if two struct variables are being compared
and at least one of them has a type that’s an anonymous struct, you can compare
them without a type conversion if the fields of both structs have the same names,
order, and types1
2
3
4
5
6
7
8
9
10
11
12
13
14
15type firstPerson struct {
name string
age int
}
f := firstPerson{
name: "Bob",
age: 50,
}
var g struct {
name string
age int
}
// compiles -- can use = and == between identical named and anonymous structs
g = f
fmt.Println(f == g)
Chapter.4 - Blocks, Shadows, and Control Structures
Blocks
Each place where a declaration occurs is called a block. Variables, constants, types, and functions declared outside of any functions are placed in the package block.
Shadowing Variables
1 | |
Output:1
2
310
5
10
A shadowing variable is a variable that has the same name as a variable in a containing block. For as long as the shadowing variable exists, you cannot access a shadowed variable.
1 | |
Output:1
25 20
10
if
1 | |
If you run this code, you’wll find that it always assigns 1 to n. This happends because the default random number seed in math/rand is hard-coded.
Go doesn’t need parenthesis around the condition. But there’s another feature that Go adds to if statements that helps you better manager your variables.
1 | |
It lets you create variables that are available only where they are needed. Once the series of if/else statements ends, n is undefined.
for, Four Ways
1 | |
1 | |
1 | |
break and continue
There is no Go equivalent of the do keyword in Java, C and JavaScript. If you want to iterate at least once, the cleanest way is to use an infinite for loop that ends with an if statement:1
2
3
4
5
6for {
//do something.
if !CONDITION {
break
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14//Example 4-11. Confusing code
for i := 1; i <= 100; i++ {
if i%3 == 0 {
if i%5 == 0 {
fmt.Println("FizzBuzz")
} else {
fmt.Println("Fizz")
}
} else if i%5 == 0 {
fmt.Println("Buzz")
} else {
fmt.Println(i)
}
}
Go encourages short if statement bodies, as left-align as possible. Nested code is difficult to follow. Using a continue statement makes it easier to understand what’s going on.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16//Example 4-12. Using continue to make code clearer
for i := 1; i <= 100; i++ {
if i%3 == 0 && i%5 == 0 {
fmt.Println("FizzBuzz")
continue
}
if i%3 == 0 {
fmt.Println("Fizz")
continue
}
if i%5 == 0 {
fmt.Println("Buzz")
continue
}
fmt.Println(i)
}
The for-range Statement
1 | |
Output:1
2
3
4
5
60 2
1 4
2 6
3 8
4 10
5 12
The first variable is the position in the data structure being iterated, while the second is the value at that position.1
2
3
4
5//Example 4-14. Ignoring the key in a for-range loop
evenVals := []int{2, 4, 6, 8, 10, 12}
for _, v := range evenVals {
fmt.Println(v)
}
Output:1
2
3
4
5
62
4
6
8
10
12
Iterating over maps
1 | |
Output:1
2
3
4
5
6
7
8
9
10
11
12Loop 0
c 3
b 2
a 1
Loop 1
a 1
c 3
b 2
Loop 2
a 1
c 3
b 2
The order of the keys and values varies; some runs may be identical. This is actually a security feature. In earlier Go versions, the iteration order for keys in a map was usually (but not always) the same if you inserted the same items into a map. This caused two problems:
- People would write code that assumed that the order was fixed, and this would break at weired times.
- If maps always hash items to the exact same values, and you know that a server is storing some user data in a map, you can actually slow down a server with an attack called Hash DoS by sending it specially crafted data where all of the keys hash to the same bucket.
Iterating over strings
1 | |
Output:1
2
3
4
5
6
7
8
9
10
11
12
13
140 104 h
1 101 e
2 108 l
3 108 l
4 111 o
0 97 a
1 112 p
2 112 p
3 108 l
4 101 e
5 95 _
6 960 π
8 33 !
In the first column, we have the index; in the second, the numeric value of the letter; and in the third, we have the numeric value of the letter type converted to a string.
For apple_π, notice that the first column skips the number 7. Second, the value at position 6 is 960. That’s far larger than what can fit in a byte (2^8 = 256)
switch
1 | |
Output:1
2
3
4a is a short word!
cow is a short word!
smile is exactly the right length: 5
anthropologist is a long word!
1 | |
Output:1
2
3
4
5
6
7
8
9
100 is even
1 is boring
2 is even
3 is divisible by 3 but not 2
4 is even
5 is boring
6 is even
Exit the loop
8 is even
9 is divisible by 3 but not 2
This is not what we intended. To break to loop, we can modify the example 4-20 to 4-21:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16func main() {
loop:
for i := 0; i < 10; i++ {
switch {
case i%2 == 0:
fmt.Println(i, "is even")
case i%3 == 0:
fmt.Println(i, "is divisible by 3 but not 2")
case i%7 == 0:
fmt.Println("Exit the loop")
break loop
default:
fmt.Println(i, "is boring")
}
}
}
Output:1
2
3
4
5
6
7
80 is even
1 is boring
2 is even
3 is divisible by 3 but not 2
4 is even
5 is boring
6 is even
Exit the loop
Of course, we can change the label loop to anything like Peter.
Blank Swiches
Chapter.5 - Functions
Declaring and Calling Functions
1 | |
When you have multiple input parameters of the same type, you can write your input parameters like this:1
2
3func div(numerator, denominator int) int {
}
Go doesn’t have named and optiona input parameters. You must apply all of the parameters for a function. If you want to emulate named and optional parameters, define a struct that has fields that match the desired parameters, and pass the struct to your function.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//Example 5-1.
type MyFuncOpts struct {
FirstName string
LastName string
Age int
}
func MyFunc(opts MyFuncOpts) error {
//do something here
}
func main() {
MyFunc(MyFuncOpts {
LastName: "Patel",
Age: 50,
})
MyFunc(MyFuncOpts {
FirstName: "Joe",
LastName: "Smith",
})
}
In practice, not having named and optional parameters isn’t a limitation. A function shouldn’t have more than a few parameters, and named and optional parameters are mostly useful when a function has many inputs.