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.
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.
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):
- A pointer to the backing array
- Its length
- 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
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. withvar
) (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)
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:
- By calling
os.Exit(<exit code>)
- 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:
- Handle it (and log it).
- 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)
}
}
Comments