Jim Cheung

Reading notes: Go in Action

Chapter 1. Introducing Go

When pointers to the data are being exchanged, each goroutine still needs to be synchronized if reads and writes will be performed by the different goroutines.

In Go, if your type implements the methods of an interface, a value of your type can be stored in a value of that interface type. No special declarations are required.

Chapter 2. Go Quick Start

import (
    "log"
    "os"
    _ "some_package"
)

Using the blank identifier as the alias name for the import.

The blank identifier _ allows the compiler to accept the import and call any init functions that can be found in the different code files within that package.

func init() {
    log.SetOutput(os.Stdout)
}

All init functions in any code file that are part of the program will get called before the main function.

By default, the logger is set to write to the stderr device.

Unexported identifiers start with a lowercase letter and can't be directly accessed by code in other packages.

make(map[string]Matcher)

A map is a reference type that you're required to make in Go.

In Go, all variables are initialized to their zero value.

for pointers, the zero value is nil.

variables declared as a reference type set to their zero value will return the value of nil.

feeds, err := RetrieveFeeds()
if err != nil {
    log.Fatal(err)
}

A slice is a reference type that implements a dynamic array.

If an error occurs, never trust the other values being returned from the function.

results := make(chan *Result)

A good rule of thumb when declaring variables is to use the keyword var when declaring variables that will be initialized to their zero value,

var waitGroup sync.WaitGroup

waitGroup.Add(len(feeds))

In Go, once the main function returns, the program terminates.

A WaitGroup is a counting semaphore,

for _, feed := range feeds {
    ...
}

When we use for range to iterate over a slice, we get two values back on each iteration. The first is the index position of the element we're iterating over, and the second is a copy of the value in that element.

matcher, exists := matchers[feed.Type]

if !exists {
    matcher = matchers["default"]
}

When looking up a key in a map, you have two options: you can assign a single variable or two variables for the lookup call.

The first variable is always the value returned for the key lookup, and the second value, if specified, is a Boolean flag that reports whether the key exists or not.

go func(matcher Matcher, feed *Feed) {
    Matcher(matcher, feed, searchTerm, results)
    waitGroup.Done()
}(matcher, feed)

Pointer variables are great for sharing variables between functions.

In Go, all variables are passed by value.

Since the value of a pointer variable is the address to the memory being pointed to, passing pointer variables between functions is still considered a pass by value.

Go supports closures.

Thanks to closures, the function can access those variables directly without the need to pass them in as parameters. (waitGroup, searchTerm and results above)

go func() {
    waitGroup.Wait()
    close(results)
}()

method Wait on the WaitGroup value, which is causing the goroutine to block until the count for the WaitGroup hits zero.

const dataFile = "data/data.json"

Go compiler can deduce the type from the value on the right side of the assignment operator, specifying the type when declaring the constant is unnecessary.

file, err := os.Open(dataFile)
if err != nil {
    return nil, err
}

defer file.Close()

keyword defer is used to schedule a function call to be executed right after a function returns.

This will happen even if the function panics and terminates unexpectedly.

type Matcher interface {
    Search(feed *Feed, searchTerm string,) ([]*Result, error)
}

Naming convention in Go when naming interfaces:

If the interface type contains only one method, the name of the interface ends with the er suffix.

When multiple methods are declared within an interface type, the name of the interface should relate to its general behavior.

type defaultMatcher struct{}

An empty struct allocates zero bytes when values of this type are created.

func (m defaultMatcher) Search ...

receiver with any function declaration declares a method that's bound to the specified receiver type.

func (m defaultMatcher) Search ...
func (m *defaultMatcher) Search ...

Whether we use a value or pointer of the receiver type to make the method call, the compiler will reference or dereference the value if necessary to support the call.

It's best practice to declare methods using pointer receivers.

(Search method above doesn't use pointer receiver because defaultMatcher is a zero allocation type.)

Unlike when you call methods directly from values and points, when you call a method via an interface type value, the rules are different.

Methods declared with pointer receivers can only be called by interface type values that contain pointers.

Methods declared with value receivers can be called by interface type values that contain both values and pointers.

for result := range results {
    ...
}

The for range loop will block until a result is written to the channel. As each goroutine writes its results to the channel, the for range loop wakes up and is given those results.

Once the channel is closed, the for range loop is terminated.

if matched {
    results = append(results, &search.Result{
        Field: "Title",
        Content: channelItem.Title,
    })
}

use the ampersand (&) operator to get the address of the value.

Chapter 3. Packaging and Tooling

All .go files must declare the package that they belong to as the first line of the file excluding whitespace and comments.

Packages are contained in a single directory.

When naming your packages and their directories, you should use short, concise, lowercase names.

A unique name is not required, because you import the package using its full path.

Package main

The package name main has special meaning in Go. It designates to the Go command that this package is intended to be compiled into a binary executable.

When the main package is encountered by the compiler, it must also find a function called main().

Imports

Packages that are created by you or other Go developers live inside the GOPATH, which is your own personal workspace for packages.

The compiler will stop searching once it finds a package that satisfies the import statement.

The Go installation directory is the first place the compiler looks and then each directory listed in your GOPATH in the order that they're listed.

Named imports

Packages can be imported by using named imports.

This is performed by giving one of the packages a new name to the left of the import statement.

init

Each package has the ability to provide as many init functions as necessary to be invoked at the beginning of execution time.

Go Tools

go build path/to/package/...

Three periods (...) in your package specifier indicate a pattern matching any string.

go vet main.go

go vet catches common errors, it's a great idea to run it before you commit.

Documentation

command line:

go doc tar

To start your own documentation server, type the following command into a terminal session:

godoc -http=:6060

To be included in the godoc generated documentation, start by adding comments directly above the identifiers you want to document. This works for packages, functions, types, and global variables.

Creating repositories for sharing

just put the package source files at the root of the public repository, not src or code sub-directories.

Dependency management

(note, starting from v1.6, Go will also searches vendor directory within a project for imports)

(The book suggests gb )

Chapter 4. Arrays, Slices and Maps

Array

Arrays are valuable data structures because the memory is allocated sequentially.

var array [5]int

Once an array is declared, neither the type of data being stored nor its length can be changed.

When an array is initialized, each individual element that belongs to the array is initialized to its zero value.

Array literals allow you to declare the number of elements you need and specify values for those elements.

array := [5]int{10, 20, 30, 40, 50}

If the length is given as ..., Go will identify the length of the array based on the number of elements that are initialized.

array := [...]int{10, 20, 30, 40, 50}

To access an individual element, use the [ ] operator.

array[2] = 35

The type of an array variable includes both the length and the type of data that can be stored in each element.

Only arrays of the same type can be assigned.

var array1 [5]string
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}

array1 = array2

Arrays are always one-dimensional, but they can be composed to create multidimensional arrays.

array := [4][2]int{{10,11}, {20,21}, {30,31}, {40,41}}

To access an individual element, use the [ ] operator again and a bit of composition.

array[0][0] = 10

Passing an array between functions can be an expensive operation in terms of memory and performance.

Using pointer, the address of the array, which only requires 8 bytes of memory to be allocated on the stack for the pointer variable.

Slice

One way to create a slice is to use the built-in function make.

// length and capacity of 5
slice := make([]string, 5)

// length of 3 and capacity of 5
slice := make([]string, 3, 5)

Slice literal, it's similar to creating an array, except you don't specify a value inside of the [ ] operator.

slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}

// length and capacity of 100
slice := []string{99: ""}

Remember, if you specify a value inside the [ ] operator, you're creating an array. If you don't specify a value, you're creating a slice.

A nil slice is created by declaring a slice without any initialization.

var slice []int

An empty slice by declaring a slice with initialization.

slice := make([]int, 0)

// or
slice := []int{}

An empty slice contains a zero-element underlying array that allocates no storage.

Regardless of whether you're using a nil slice or an empty slice, the built-in functions append, len, and cap work the same.

slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]

You need to remember that you now have two slices sharing the same underlying array.

Changes made to the shared section of the underlying array by one slice can be seen by the other slice.

When your append call returns, it provides you a new slice with the changes. The append function will always increase the length of the new slice.

slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]
newSlice = append(newSlice, 60)

When there's no available capacity in the underlying array for a slice, the append function will create a new underlying array, copy the existing values that are being referenced, and assign the new value.

The append operation is clever when growing the capacity of the underlying array.

Capacity is always doubled when the existing capacity of the slice is under 1,000 elements.

Detaching the new slice from its original source array makes it safe to change.

source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}

// restrict the capacity, force to create new underlying array when append new element
slice := source[2:3:3]

slice = append(slice, "Kiwi")

If you use the ... operator, you can append all the elements of one slice into another.

s1 := []int{1, 2}
s2 := []int{3, 4}

fmt.Printf("%v\n", append(s1, s2...))

// output [1 2 3 4]

Go has a special keyword called range that you use in conjunction with the keyword for to iterate over slices.

slice := []int{10, 20, 30, 40}

for index, value := range slice {
    fmt.Printf("Index: %d Value: %d\n", index, value)
}

It's important to know that range is making a copy of the value, not returning a reference.

The address of each individual element can be captured using the slice variable and the index value. (&slice[index] below)

slice := []int{10, 20, 30, 40}

for index, value := range slice {
    fmt.Printf("Value: %d Value-addr: %X ElemAddr: %X\n", value, &value, &slice[index])
}

There are two special built-in functions called len and cap that work with arrays, slices, and channels.

Slices are cheap and passing them between functions is trivial.

On a 64-bit architecture, a slice requires 24 bytes of memory. The pointer field requires 8 bytes, and the length and capacity fields require 8 bytes respectively.

Map

A map is a data structure that provides you with an unordered collection of key/value pairs.

every iteration over a map could return a different order.

(internal) The purpose of the hash function is to generate an index that evenly distributes key/value pairs across all available buckets.

You can use the built-in function make, or you can use a map literal.

Using a map literal is the idiomatic way of creating a map.

dict := make(map[string]int)

dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}

The map key can be a value from any built-in or struct type as long as the value can be used in an expression with the == operator.

Slices, functions, and struct types that contain slides can't be used as map keys.

A nil map can't be used to store key/value pairs.

var colors map[string]string

colors["Red"] = "#da1337"

// Runtime Error

When retrieving a value from a map, you have two choices. You can retrieve the value and a flag that explicitly lets you know if the key exists.

value, exists := colors["Blue"]

if exists {
    fmt.Println(value)
}

If you want to remove a key/value pair from the map, you use the built-in function delete.

delete(colors, "Coral")

Passing a map between two functions doesn't make a copy of the map. Maps are designed to be cheap, similar to slices.

Chapter 5. Go's Type System

Based on the type information on the right side of the operator, the short variable declaration operator (:=) can determine the type for the variable.

user {
    name: "Lisa",
    email: "lisa@email.com",
    ext: 123,
    privileged: true,
}

A colon is used to separated the two, and it requires a trailing comma.

The order of the values does matter in this case and needs to match the order of the fields in the struct declaration.

lisa := user{"Lisa", "lisa@email.com", 123, true}

Methods

Methods provide a way to add behavior to user-defined types.

Methods are really functions that contain an extra parameter that's declared between the keyword func and the function name.

func (u user) notify() { ... }

The parameter between the keyword func and the function name is called a receiver and binds the function to the specified type.

When a function has a receiver, that function is called a method.

There are two types of receivers in Go: value receivers and pointer receivers.

The nature of types

Does adding or removing something from a value of this type need to create a new value or mutate the existing one? If the answer is create a new value, then use value receivers for your methods. If the answer is mutate the value, then use pointer receivers.

The idea is to not focus on what the method is doing with the value, but to focus on what the nature of the value is.

Built-in types are the set of types that are provided by the language. We know them as the set of numeric, string, and Boolean types. These types have a primitive nature to them. Because of this, when adding or removing something from a value of one of these types, a new value should be created.

Reference types in Go are the set of slice, map, channel, interface, and function types. When you declare a variable from one of these types, the value that's created is called a header value.

The header value contains a pointer; therefore, you can pass a copy of any reference type value and share the underlying data structure intrinsically.

When a factory function returns a pointer, it's a good indication that the nature of the value being returned is nonprimitive.

Since values of type File have a nonprimitive nature, they're always shared and never copied.

func (f *File) Chdir() error { ... }

The decision to use a value or pointer receiver should not be based on whether the method is mutating the receiving value. The decision should be based on the nature of the type.

Implementation of Interfaces

(read the book again for this part.)

Method sets define the set of methods that are associated with values or pointers of a given type.

If you implement an interface using a pointer receiver, then only pointers of that type implement the interface.

If you implement an interface using a value receiver, then both values and pointers of that type implement the interface.

The question now is why the restriction? The answer comes from the fact that it's not always possible to get the address of a value.

type duration int

func (d *duration) pretty() string { ... }

func main() {
    duration(42).pretty()
    
    // error, cannot take the address of duration(42)
}

Type embedding

The type that is embedded is then called an inner type of the new outer type.

But thanks to inner type promotion, the notify method can also be accessed directly from the ad variable.

type user struct { ... }
type admin struct {
    user // embedded type
    level string
}

func (u *user) notify() { ... }

func main() {
    ad.user.notify()
    
    // inner type's method is promoted
    ad.notify()
}

the inner type's implementation was not promoted once the outer type implemented the notify method.

Exporting and unexporting identifiers

When an identifier starts with a lowercase letter, the identifier is unexported or unknown to code outside the package. When an identifier starts with an uppercase letter, it's exported or known to code outside the package.

package counters

// unexported type
type alertCounter int

func New(value int) alertCounter {
    return alertCounter(value)
}

func main() {
    counter := counters.New(10)
    fmt.Printf("Counter: %d\n", counter)
}

This is possible for two reasons. First, identifiers are exported or unexported, not values.

Second, the short variable declaration operator (:=) is capable of inferring the type and creating a variable of the unexported type.

package entities

type user struct {
    Name string
    Email string
}

type Admin struct {
    user // embedded type
    Rights int
}

func main() {
    a := entities.Admin {
        Rights: 10
    }
    
    a.Name = "Bill"
    a.Email = "bill@email.com"
    fmt.Printf("User: %v\n", a)
}

The identifiers from the inner type are promoted to the outer type, those exported fields (Name, Email) are known through a value of the outer type.

Chapter 7. Concurrency Patterns

Chapter 6. Concurrency

Parallelism is about doing a lot of things at once. Concurrency is about managing a lot of things at once.

Go standard library has a function called GOMAXPROCS in the runtime package that allows you to specify the number of logical processors to be used by the scheduler.

import "runtime"

runtime.GOMAXPROCS(1)

// or
runtime.GOMAXPROCS(runtime.NumCPU())

Remember that goroutines can only run in parallel if there's more than one logical processor and there's a physical processor available to run each goroutine simultaneously.

Race conditions

Go has a special tool that can detect race conditions in your code.

go build -race

Atomic functions

AddInt64 function from the atomic package. This function synchronizes the adding of integer values by enforcing that only one goroutine can perform and complete this add operation at a time.

Mutexes

A mutex is used to create a critical section around code that ensures only one goroutine at a time can execute that code section.

var mutex sync.Mutex

func main() {
    for count := 0; count < 2; count++ {
        mutex.Lock()
        {
            ...
        }
        mutex.Unlock()
    }
}

The use of the curly brackets { ... } is just to make the critical section easier to see; they're not necessary. Only one goroutine can enter the critical section at a time. Not until the call to the Unlock() function is made can another goroutine enter the critical section.

Channels

When a resource needs to be shared between goroutines, channels act as a conduit between the goroutines and provide a mechanism that guarantees a synchronous exchange.

Creating a channel in Go requires the use of the built-in function make.

unbuffered := make(chan int)

buffered := make(chan string, 10)

Sending a value or pointer into a channel requires the use of the <- operator.

buffered <- "Gohpher"

For another goroutine to receive that string from the channel, we use the same <- operator, but this time as a unary operator.

value := <-buffered

Unbuffered channels require both a sending and receiving goroutine to be ready at the same instant before any send or receive operation can complete.

Synchronization is inherent in the interaction between the send and receive on the channel. One can't happen without the other.

value, ok := <-unbuffered

the ok flag is checked for false. A value of false indicates the channel was closed

Chapter 8. Standard Library

Chapter 9. Testing and benchmarking