User Tools

Site Tools


go:love_of_go

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

Go has a type to communicate errors - error. Example usage:

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
go/love_of_go.1763460462.txt.gz · Last modified: by v1ctor