====== 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