Go cheatsheet

These notes are based on Bill Kennedy’s course Ultimate go Programming

Variables

Declaration

// Zero value initialization
var a string                
var b int                   // size of int depends on the architecture
var c float64
var d bool  

// Short variable declaration - declare and initialize
aa := 10
bb := "hello"
cc := 3.14159
dd := true

Strings

Strings are 2 words, and the word size depends on the architecture. A word in 32-bit systems is 4 bytes, while a word on 64-bit systems is 8 bytes. So string variables are 8 bytes on 32-bit machines, and 16 bytes on 64-bit machines.

Strings mental model

Ranging Over a String

When ranging over a string, we range through its codepoints. Codepoints are between 1-4 bytes, and represent 1 UTF-8 character.

// Declare a string with both chinese and english characters.
	s := "世界 means world"

	// UTFMax is 4 -- up to 4 bytes per encoded rune.
	var buf [utf8.UTFMax]byte

	// Iterate over the string.
	for i, r := range s {

		// Capture the number of bytes for this rune.
		rl := utf8.RuneLen(r)

		// Calculate the slice offset for the bytes associated
		// with this rune.
		si := i + rl

		// Copy of rune from the string to our buffer.
		copy(buf[:], s[i:si])

		// Display the details.
		fmt.Printf("%2d: %q; codepoint: %#6x; encoded bytes: %#v\n", i, r, r, buf[:rl])
	}

Structs

Padding

//declaration. Name type
type example struct {
	flag    bool
	counter int16
	pi      float32
}

How much memory does struct example consume? 7 bytes, right? Not quite! The main memory is word - aligned, which means that addresses need to be a multiple of the word size. Therefore, that struct needs 8 bytes, 1 of which is just padding.

Struct memory layout

Every value needs to be properly aligned in memory, based on its size:

  • 2byte values should be put on “multiple of 2 addresses”. That is xxxxxxxx0, xxxxxxxx2, xxxxxxxx4 ….
  • 4byte values should be put on “multiple of 4 addresses”. That is xxxxxxxx0, xxxxxxxx4, xxxxxxxx8 ….

Ordering fields from largest to smallest helps reduce the padding.

Struct Types

//declaration. Name type
type example struct {
	flag    bool
	counter int16
	pi      float32
}

type example2 struct {
	flag    bool
	counter int16
	pi      float32
}

//construction
e := example{
	flag:     true,
	counter:  10,
	pi:       3.141592,
}

//anonymous type and literal construction - struct. Literal type
e1 := struct {
	flag    bool
	counter int16
	pi      float32
}{}

On type creation:

  • Only declare types that represent something new or unique
  • Validate that a value of any type is created or used on its own.
  • Embed types to reuse existing behaviors you need to satisfy.
  • Question types that are an alias or abstraction for an existing type.
  • Question types whose sole purpose is to share common state.

Conversion

Go doesn’t have casting (unless we use the unsafe package). It has type conversion.

aa := 10
aaa := int32(aa)            // completely different memory location than aa. Has the cost of allocating new memory, but it's safer. 
//Implicit conversion for name types - won't compile
var a example
var b example2
a = b               // won't work
a = example(b)      // explicit conversion, will work
a = e1              // implicit conversion works, for literal types 

Implicit conversion is not allowed in go, for name types! We can’t assign a var of type example to another, identical struct even if the 2 struct types are pretty much the same - we would have to do explicit conversion. We can, however, assign a variable of the literal type e1 to a variable of type example.

Pointers

Everything in Go is pass by value (addresses are also data, and they ‘re passed by value).

// value semantics, the value of inc is copied
func increment(inc int) int {
  inc++ 
  return inc
}

// pointer semantics, the address of inc is copied
func increment(int *int) {
  *inc++
}

Value Semantics: Isolation, Immutability (a function operates on its own copy of data). But there is the cost of copying the data. Pointer Semantics: Efficiency, data sharing. But there are side effects, when modifying shared state.

Escape Analysis

Go figures out which variables need to be put on the heap (because we ‘re sharing them up the call stack), by performing a static code analysis. Stacks should be our first priority, cause they’re pre-allocated, faster, self-cleaning.


type data struct {
  foo int
  bar string
}

func createData() *data {
  d := data{
    foo: 1,
    bar: "norf",
  } 
  
  //escapes the stack (heap allocation)
  return &d
}

On return statements: Prefer return &d over return d (where d is declared as d := &data{...}) as the former makes it clear that there is a heap allocation (we ‘re sharing data up the call stack). If we use the latter, we need to track where the data var is declared, notice that it’s a pointer type and then conclude that a heap allocation is taking place.

On Stacks:

  • Operating System Threads: Stacks are usually 1 MB.
  • Goroutines: Stacks are 2 KB.
  • When a goroutine runs out of stack space, Go allocates a 25% bigger stack, and copies the stack frames from the older stack.

Garbage Collection

Tri color, mark and sweep concurrent collector. The are still some stop-the-world pauses, but they’re being kept at their bare minimum. It’s not compacting (i.e. memory on the heap is not moved around, like jvm’s garbage collector for instance).

The pacing algorithm makes sure that the stop-the-world pauses don’t last longer than 100 μs, per run. It can take up to 25% of the available CPU capacity.

Constants

// Constants of a kind - can be implicitly converted by the compiler
const ki = 5345     // kind: integer
const kf = 4.7366  // kind: floating-point

// Constants of a type 
const ti int = 5345        // type: int 
const tf float64 = 4.7366  // type: float64

// Kind Promotion - ret is of type float64
var ret = 4 * 1.824  // KindFloat(4) * KindFloat(1.824)

Constants in go have 256 bits of precision.

Practical example of constant usage, in the time package:

type Duration int64

const (
       Nanosecond duration = 1
       Microsecond         = 1000 * Nanosecond
       Milisecond          = 1000 * Microsecond
       Second              = 1000 * Milisecond
       // ...
)

iota

const (
       A1 = iota // 0
       B1        // 1
       C1        // 2
)

Arrays

var fruits [3]string
fruits[0] = "Apple"
fruits[1] = "Banana"
fruits[2] = "Pear"

// declare and initialize
var fruits := [3]string{"Apple", "Banana", "Pear"}

Cache lines are usually 64 bytes.

Predictable access patterns to memory.

TLB misses (TLB lookup translates virtual to physical memory addresses)

Memory Pages usually 4k or 8k

Slices

Slices are 3 word data structures (24 bytes on 64bit architectures):

  1. A pointer to the backing array
  2. Its length
  3. Its capacity (if that’s not defined on the make call, it’s the same as the length)
fruits := make([]string, 3) 
fruits[0] = "Apple"
fruits[1] = "Banana"
fruits[2] = "Pear"

var data []string   // initialized to its zero value (nil,0,0)
data := []string{}  // empty slice ( != to the above line, which is the zero value for slice ). It points to the empty struct{}, which is unique in the go runtime

length := len(data)
capacity := cap(data)

We can also create a slice from any given array by applying that syntax:

arr := []string{"A", "B", "C"}
sl := arr[:]   // Creates a slice backed by the 'arr' backing array, with length == capacity == 3

Appending

fruits = append(fruits, "kiwi")                     // value semantic API. Operates on a copy of the original slice, then returns it
fmt.Printlf("%s - %s", len(fruits), cap(fruits))    // prints (4, 6). When length == capacity, append doubles the backing array to add a new element 
Append
fruits := make([]string, 0, 4) // if we know the number of elements that we re going to add beforehand, we can pre
                               // allocate the backing array by setting lenth to 0 and capacity to the number of elements 
fruits := make([]string, 4)    // or even better, we can pre allocate, init to 0 value, and set elements by indexing: data[0]=...

When length == capacity, appends creates a new backing array. References to elements pointing to the old backing array, are memory leaks and can cause nasty bugs.

Slicing

	data := []string{"a", "b", "c", "d", "e"}
	
	fmt.Println(data[2:2+2])              // Prints [c d]. Note that it's [2, 4) - i.e. not including the 4th element. 
	fmt.Println(len(data[2:2+2]))         // Prints 2.
	fmt.Println(cap(data[2:2+2]))         // Prints 3. The new slice can still access "d" and "e". Append will not allocate a new slice in this case.
	
	data := data[2:4:4]                   // Also sets the capacity of the new slice. This can be helpful, as any future additions
	                                      // to the new slice are not visible to the original slice, as append will return a new slice
	                               
  data2 := make([]string, len(data))    // Make a new slice, with length and capacity == len(data), cap(data)
  copy(data2, data)                     // And use the built-in copy, to copy the contents

Ranging over a slice

Value Semantics

// We re iterating over a copy of the items slice - we don't see the original slice modification, in our iteration
items := []string{"foo", "bar", "norf", "qux", "foobar"}
for i, v := range items {
        items[1] = "baz"
        if i == 1 {
                 fmt.printf(v) // prints bar - value semantics means we 're iterating over the a copy and not the original items
        }
        fmt.Printf("v[%s]", item)
}

Pointer Semantics

// We re iterating over the original slice
items := []string{"foo", "bar", "norf", "qux", "foobar"}
for i := range items {
        items[1] = "baz"
        if i == 1 {
                 fmt.printf(items[1]) // prints baz - pointer semantics means we 're iterating over the original slice
        }
        fmt.Printf("v[%s]", item)
}

Maps

ids := make(map[string]int)
id["Mike"] = 1
id["John"] = 2

// literal construction
ids := map[string]int{
        "Mike" : 1,
        "John" : 2,
       }

id := id["Mike"]       // 1
id, ok := id["George"] // 0 (that's always the value's zero value), false
  • We can’t change the hashing algorithm
  • In order for a map to be usable, we need to call make. It’s not usable in its zero value state (i.e. with var) (in contrast to slices, for example) { :.notice–info}

Ranging over a map

for key, value := range ids {
  fmt.Println(key, value)
}
for key := range ids {
  fmt.Println(key, id["key"])
}

Note: When ranging over the map, the order is random.

Methods

Methods are functions with receivers. This way we can have data with behavior.


type person struct {
  name      string
  address   string
}


// Value received
func (p person) alert() {
  fmt.Println("%s : %s", p.name, p.address)
}

// Pointer receiver
func (p *person) changeAddress(address string) {
  p.address = address
}

mike := person{"Mike", "London"}


mike.alert()                    // equivalent to person.alert(mike) - methods are just syntactic sugar

mike.changeAddress("Athens")    // equivalent to (*person).changeAdress(&mike, "Athens")

}

Value vs Pointer Semantic

When in doubt:

Type Semantic
Built in Types Value
Reference Types Value (except when decoding, unmarshalling)
User Defined Types Pointer (*)
  • For struct types, it’s worth asking if mutating the data is still the same data (i.e. modifying a person’s address) - pointer semantics - or a new piece of data (i.e. modifying a point in time or a string) - value semantics.
  • If we are using someone else’s struct type, we should follow the semantic from the factory functions that create those struct values.

Interfaces

Interface types are not “real” types. They only define a set of methods (behavior). They’re reference types, 2 words in size:

  • The 1st word is a pointer to the iTable (iTable : Matrix of function pointers - similar to the vTable in C++).
  • The 2nd word is a pointer to the concrete data that satisfies that interface.
type Reader interface {
    read(b []byte) (int, error)
}

type File struct {..}
type Pipe struct {..}

func (f File) read(b []byte) (int, error) {..}
func (p Pipe) read(b []byte) (int, error) {..}

func retrieve(r reader) error {...}

var f File
var p Pipe

retrieve(f)                 // We can also call retrieve(&f). This is usefull on some ocassions (decoding, unmarshalling)
retrieve(p)                 // We can also call retrive(&p). This is usefull on some ocassions (decoding, unmarshalling)
Append

As we can see in the above figure, decoupling has that extra lookup cost.

Bad practices

  • There is only one implementation. Unless we foresee an upcoming change request, why use an interface? Use the concrete type instead.
  • The interface is exported but the concrete type is unexported
  • A factory function returns the interface value with the unexported concrete type
  • The interface can be swapped with the concrete type, and nothing changes for the user of the API
  • The interface is not decoupling the API from change

Method Sets


type Dev struct {..}
func (d *Dev) read{..}      // Dev implements Reader using pointer semantics
var d dev

retrive(d)                  // can't call retrieve using a value of type Dev. Needs to be a pointer of type Dev!            
Interface Implementation Receiver Type
Value Receiver (T) Value, and Pointer (i.e. can call(d), call(&d). Useful when decoding, unmarshalling)
Pointer Receiver (*T) Pointer only (i.e. can only call(&d). Copying the value that a pointer points to is generally not safe, compilers forbids this use case)

Embedding

Embedding lets us embed a type into another type, which allows composition. The inner type promotion lets us use the methods of the embedded type from the outer type. Embedding is not subtyping!

// Embedding a user value into an admin value. User becomes an "inner" type of admin
type admin struct {
    user
    level string
}

ad := admin{
        user: user{
                name: "John Doe",
                email: "john@doe.com",
        },
        level: "root",
}

// can call 
ad.admin.notify()
// but also use the inner type promotion, to call
ad.notify()

Exporting

The most basic unit of compilation is go, is the package (== a folder). The package name (i.e. package <package name) at the top of a go file, needs to match the folder name that contains that go file (the compiler won’t throw an error if it doesn’t, but there are several tools that expect that).

Everything that starts with a capital letter is exported (i.e. can be used outside of that package).


package counters

// Exported, starts with a capital letter
type AlertCounter int

// Not exported, starts with a lowercase letter
type internalCounter int

It may be useful for a type to be unexported, but their fields to be exported, to enable unmarshalling / decoding. { :.notice–info}

Conversion - Type Assertions

b := someInterface.(typea)    // panics if the value inside the interface is not of type (can be another interface as well) "typea" 
b, ok := someInteface.(typea) // checks and sets b to typea's zero value and ok to false

Useful when we want to provide some default behavior, but also let the user provide a custom one.

Error Handling

There are two ways to shut down a go programme:

  1. By calling os.Exit(<exit code>)
  2. By using the built in panic. Use that, if you need a stack trace.

The Error Interface

type error interface {
    Error() string
}

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

func New(text string) error {
    return &errorString{text}
}

Error Variables

The convention is, that those error variables start with Err....


var (
    // We 'd better put those on the top of our source files
    ErrBadRequest = errors.New("Bad Request")
    ErrServerUnavaiable = errors.New("Server Unavaliable")
)

Error Types

The convention is, that those error types end with ...Error.

Type as Context

The idea is that we determine what to do by type asserting the Error type (if it’s UnmarshalTypeError do this, if it’s InvalidUnmarshalError do that, etc).

Examples from the Unmarshal code:


// An UnmarshalTypeError describes a JSON value that was
// not appropriate for a value of a specific Go type.
type UnmarshalTypeError struct {
    Value string       // description of JSON value
    Type  reflect.Type // type of Go value it could not be assigned to
}


// Error implements the error interface.
func (e *UnmarshalTypeError) Error() string {
    return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String()
}

err := Unmarshal([]byte(`{"name":"bill"}`), u) // Run with a value and pointer.
    if err != nil {
        // Type assertion on switch
	      switch e := err.(type) {
		    case *UnmarshalTypeError:
			      fmt.Printf("UnmarshalTypeError: Value[%s] Type[%v]\n", e.Value, e.Type)
		    case *InvalidUnmarshalError:
			      fmt.Printf("InvalidUnmarshalError: Type[%v]\n", e.Type)
		    default:
			      fmt.Println(err)
		}
		return
	}

Our first preference should be default error variables (string). Type as context is useful when we move data across program boundaries, but it can lead to cascading changes in the codebase, if those type change. In the next section, we’ll see how we can achieve that from a decoupled state, to minimize changes.

Behaviour as Context

Instead of asking if an Error is of a certain type, we can ask if that Error has a certain behaviour (e.g. does it implement temporary).

This way, we can have unexported types with unexported fields and provide method sets of behaviour (i.e. interfaces) that the user can use to perform error handling from a decoupled state.

So instead of type asserting on the concrete Error type:

// TypeAsContext shows how to check multiple types of possible custom error
// types that can be returned from the net package.
func (c *client) TypeAsContext() {
	for {
		line, err := c.reader.ReadString('\n')
		if err != nil {
			switch e := err.(type) {
			case *net.OpError:
				if !e.Temporary() {
					log.Println("Temporary: Client leaving chat")
					return
				}

			case *net.AddrError:
				if !e.Temporary() {
					log.Println("Temporary: Client leaving chat")
					return
				}

			case *net.DNSConfigError:
				if !e.Temporary() {
					log.Println("Temporary: Client leaving chat")
					return
				}

			default:
				if err == io.EOF {
					log.Println("EOF: Client leaving chat")
					return
				}

				log.Println("read-routine", err)
			}
		}

		fmt.Println(line)
	}
}

we can check for a specific behaviour:


// temporary is declared to test for the existence of the method coming
// from the net package.
type temporary interface {
	Temporary() bool
}

// BehaviorAsContext shows how to check for the behavior of an interface
// that can be returned from the net package.
func (c *client) BehaviorAsContext() {
	for {
		line, err := c.reader.ReadString('\n')
		if err != nil {
			switch e := err.(type) {
			case temporary:
				if !e.Temporary() {
					log.Println("Temporary: Client leaving chat")
					return
				}
			default:
				if err == io.EOF {
					log.Println("EOF: Client leaving chat")
					return
				}
				log.Println("read-routine", err)
			}
		}
		fmt.Println(line)
	}
}

The most common behaours that Error types can have are shown below. If that’s the case, we’d better make those errors and their fileds unexported, and let the user check for that behaviour:

  • Temporary
  • TimeOut
  • NotFound
  • NotAuthorized

Wrapping Errors

Generally a function should do two things when it comes to an error:

  1. Handle it (and log it).
  2. Or Wrap it with context (arguments used, line of code that the error took place) and return it to the caller

We can use the errors package and the Wrapf , Cause functions to accomplish those tasks.


// AppError represents a custom error type.
type AppError struct {
	State int
}

// Error implements the error interface.
func (c *AppError) Error() string {
	return fmt.Sprintf("App Error, State: %d", c.State)
}

// firstCall makes a call to a second function and wraps any error.
func firstCall(i int) error {
	if err := secondCall(i); err != nil {
		return errors.Wrapf(err, "firstCall->secondCall(%d)", i)
	}
	return nil
}

// secondCall makes a call to a third function and wraps any error.
func secondCall(i int) error {
	if err := thirdCall(); err != nil {
		return errors.Wrap(err, "secondCall->thirdCall()")
	}
	return nil
}

// thirdCall create an error value we will validate.
func thirdCall() error {
	return &AppError{99}
}

func main() {

	// Make the function call and validate the error.
	if err := firstCall(10); err != nil {

		// Use type as context to determine cause.
		switch v := errors.Cause(err).(type) {
		case *AppError:

			// We got our custom error type.
			fmt.Println("Custom App Error:", v.State)

		default:

			// We did not get any specific error type.
			fmt.Println("Default Error")
		}

		// Display the stack trace for the error.
		fmt.Println("\nStack Trace\n********************************")
		fmt.Printf("%+v\n", err)
		fmt.Println("\nNo Trace\n********************************")
		fmt.Printf("%v\n", err)
	}
}

Updated:

Comments