====== THE LOVE OF GO BOOK NOTES ======
==== TIPS ====
* When designing an application, think in term of //behaviours//, instead of, for example, functions. This keeps our mind focused on users and what they want, instead of what kind of program architecture seems logical to a software engineer.
* A good way to start thinking seriously about what a package needs to do is to write some user stories: brief descriptions of some interaction with the program from the user’s point of view.
==== MODULES & PACKAGES====
''PACKAGE'' - is collection of related Go source files that are organised together. Each file in a package shares the same package name. Only one package is allowed in a folder!
''MODULE'' - defines a project name. It consists of a collection of related packages, that are versioned together as a single unit.
You create a new Project/Module in a folder with a command:
$ go mod init github.com/username/yourproject
This Module's folder can have any number of nested folders, each representing a package. For example, package ''utils'':
% tree
.
├── go.mod
├── main.go
└── utils # 'utils' is a package inside 'yourproject' module.
└── math.go
And the code in ''math.go'':
package utils
func Add(a, b float64) float64 {
return a + b
}
Now, calling that ''Add'' function from the ''main.go'' in the module's folder:
package main
import (
"fmt"
"github.com/username/yourproject/utils"
)
func main() {
r := utils.Add(2, 3)
fmt.Println(r)
}
==== FORMATTING ====
$ gofmt -d calculator.go # just shows a diff
$ gofmt -w calculator.go # rewrites the file
==== TESTING ====
Each test in a Go project is a function. In order for a function to become a //"test function"// there are couple of requirements:
* It should be in a file with a name ending **_test.go**
* The name of the function should begin with **Test**
Running a test for a package:
$ go test
$ go test -count 100 # repeat a test 100 times
Guideline:
* Use the "one behaviour, one test" rule (rather, than "one function, one test")
* For "something and error" function, the "error" test only need to check the error value, and make sure it's not a nil
Test Coverage
% go test -cover
% go test -coverprofile=coverage.out # generate coverage profile
% go tool cover -html=coverage.out # inspect it in the browser
==== ERRORS ====
Go has a type to communicate errors - **error**. Example usage:
func (book *Book) SetCopies(copies int) error {
if copies < 0 {
return fmt.Errorf("negative number of copies: %d", copies)
}
book.Copies = copies
return nil
}
err := book.SetCopies(-1)
if err != nil {
fmt.Println("Oh dear, something went wrong:", err)
}
==== VARIABLES ====
Go assigns a default **zero** value to any variable, that is declared but not assigned a value.
Style guide\\
If we declare a variable, and would like its starting value to be zero, we do like this:
// declaring with a zero value
var x int
If you want a starting value to be something else - use short declaration '':='':
// declaring with a specific value
y := 4
==== SLICES ====
Slice can be declared as a **nil** or as an **empty** slice:
var b = []Book // nil slice, declared but not initialised
var b = []Book{} // empty slice, initialised with 0 elements, points to the empty array. Slice literal
''=='' operator isn't defined for Slices! We can use ''slices.Equal'' function to compare slices:
import "slices"
if !slices.Equal(want, got) {
t.Fatalf("want: %q, got: %q", want, got)
}
**Sorting a Slice of Struct**
type Person struct {
Name string,
Age int,
}
people := []Person{
{"Alice", 25},
{"Bob", 30},
}
slices.SortFunc(people, func(a,b Person) int { // slice.SortFunc - generic function for sorting slices
return cmp.Compare(a.Age, b.Age) // with a custom comparison.
}) // cmp.Compare - a helper that returns -1, 0, or 1
==== MAPS ====
Map is a reference type - when we pass it to the function, we are passing a reference, not a copy.
Map syntax #1:
colors := map[string]string{
"red": "#ff0000",
"green": "#008000",
}
Map syntax #2:
colors := make(map[string]string) // Creates an empty map
colors["red"] = "#ff0000" // Add key/value to it
delete(colors, "red") // Deleting a key
Iterating over a map:
func printMap(c map[string]string) {
for key, value := range c {
fmt.Println(key, value)
}
}
Go doesn't allow to update the field of map elements directly! Instead, you have to use a temp variable:
// Doesn't work!
catalog[1].Title = "New Title"
// Use temp variable instead
b := catalog[1]
b.Title = "New Title"
catalog[1] = b
Retrieving non-existing elements.\\
An interesting property of Go maps is that looking up a non‐existent key doesn’t cause an error: instead, it returns the zero value of the element type.
**Get map's values:**
import (
"maps"
"slices"
)
books := maps.Values(catalog) // returns an Iterator
books_slice := slices.Collect(books) // "collects" all elements from the Iterator into the Slice
**Checking for a missing value:**
book, ok := catalog["some-key]"] // maps support "comma, ok" pattern. ok == false if key is missing
==== STRUCTS ====
Empty literal:
b := Book{}
Defining a **method** - similar to defining a function, but it also has a **receiver** - that represents the object that the method is called on.
==== Wrapping existing type with struct ====
It's not possible to add methods to the existing type. E.g. following doesn't work:
type MyB strings.Builder
func TestMyB(t *testing.T) {
var mb mytypes.MyB
mb.WriteString("Hello") // FAILS! We can't access strings.Builder's methods if we define
} // our type based on it
Instead, we can create a **struct** type, with a field of type we want.
==== COMMA, OK PATTERN ====
Sometimes we need to know whether a function succeeded - for example, whether a search found anything. We can design the function to return two values: the result and a boolean indicating success.
func GetBook(id string) (Book, bool) {
for _, book := range catalog {
if book.ID == id {
return book, true
}
}
return Book{}, false
}
==== POINTERS ====
x := 5
y := &x // `y` is a pointer to `x`. `&` is an address operator
fmt.Println(*y) // *y dereferences y - it retrieves the value that y “points” to
==== OBJECTS ====
In order to create a Method, we need to specify the **receiver** parameter. It tells Go that this method should be called on a specific object value, and that value is available inside the method:
func (book Book) String() string { // (book Book) - method's receiver
return fmt.Sprintf("%v by %v (copies: %v)",
book.Title, book.Author, book.Copies)
}
There are two types of receivers - **Value** and **Pointer** receivers. Think of it as //how the function gets access to the object://
* By Copy (value receiver) - this **copies the data** into the method
* By Value (pointer receiver) - this gives the method a **reference to the original object**
func (book Book) SetCopies(copies int) {
book.Copies = copies // only affects local `book`, not the original
}
And the //pointer// example:
func (book *Book) SetCopies(copies int) {
book.Copies = copies // Go provides an automatic de-referencing when we use
// pointers to struct. Pointers can't have fields, so there
// is no ambiguity here. Otherwise it would look like:
// (*book).Copies = copies
}
book := books.Book{
Copies: 5,
}
book.SetCopies(12) // That's here, when we pass `value` or `reference` to
// the method