This is an old revision of the document!
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
Creating a custom error:
return 0, errors.New("division by zero not allowed")
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 }
The 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 }
