THE LOVE OF GO BOOK NOTES

TIPS

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:

Running a test for a package:

$ go test
$ go test -count 100 # repeat a test 100 times

Guideline:

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:

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