Skip to content

Instantly share code, notes, and snippets.

@bnguyensn
Last active February 14, 2022 20:59
Show Gist options
  • Save bnguyensn/c8b98880cf8988103563bf96ca1f20fd to your computer and use it in GitHub Desktop.
Save bnguyensn/c8b98880cf8988103563bf96ca1f20fd to your computer and use it in GitHub Desktop.
Go notes

Go Notes

CLI

The most important Go CLIs:

Command Description
go build Compile packages and dependencies
go fmt Format package sources
go install Build and produce an executable binary of the current package, and install it to the path specified by either GOBIN or GOPATH
go run Compile and run program
go test Test packages

Basic

In Go, a name is exported if it begins with a capital letter

Short variable declarations

The syntax := for short variable declaration can only be used within a function.

Types

Basic types:

bool

string

// The int, uint, and uintptr types are usually 32 bits wide on 32-bit systems and 64 bits wide on 64-bit systems. When you need an integer value you should use int unless you have a specific reason to use a sized or unsigned integer type. 
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

 // alias for uint8
byte

// alias for int32
// represents a Unicode code point
rune 

float32 float64

complex64 complex128

Control

if and switch are the 2 ways to check conditions.

if and switch accepts an optional initialization statement. This is commonly used to set up a local variable.

You can label a loop and break the surrounding loop using break LoopLabel (details here)

// The local variable 'err' is setup in the 'if's initialization statement.
if err := file.Chmod(0664); err != nul {
  return err
}

switch {
case 0:
  // ...
case 1, 2: // Cases can be comma-separated
  // ...
}

There is no do or while, only for loops.

// Like a C for
for i := 0; i < 10; i++ {
  // ...
}

// Like a C while
i := 0
for i < 10 {
  // ...
  i++
}

// Like a C for(;;)
for {
  // Infinite loop...
}

// To loop over an array, slice, string, map, channel, use 'range'
for key, value := range r {
  // ...
}

// Same as above, but used when only the keys or indices of the range are needed
for key := range r {
  // ...
}

// Same as above, but used when only the values of the range are needed
// The '_' is called a blank identifier.
for _, value := range r {
  // ...
}

Functions

Functions can return multiple values. We can use this to assign multiple variables at once, for example.

func returnMultiple() (int, int) {
  return 1, 2
}

var a, b := returnMultiple()

Functions' return results can be named.

Defer

The keyword defer can be used to defer function execution. Deferred function calls are pushed onto a stack. When a function returns, its deferred calls are executed in last-in-first-out order.

This is an effective way to deal with situations such as resources that must be released regardless of which path a function takes to return.

Data

There are 2 allocation primitives: new and make.

new allocates memory but does not initialize, but zeroes it. new(T) allocates zeroed storage for a new item of type T and returns its addresss, a value of type *T (a pointer to a newly allocated zero value of type T).

make only creates slices, maps, and channels. make(T, args) returns an initialized (i.e., not zeroed) value of type T (not *T). make does not return a pointer.

Pointers

Basic

The & operator generates a pointer.

The * operator denotes a pointer's underlying value.

i, j := 1, 100

p := &i // 'p' is a pointer that holds the memory address of 'i'

fmt.Println(*p) // Will print the underlying value that pointer 'p' points to (i.e. 1)
fmt.Println(p) // Will print the memory address held by pointer 'p'

*p = 2 // Set the value of 'i' to 2 through the pointer 'p'
*p = *p + 1 // Another operation. Printing *p should now give the value 3

p = &j // 'p' now points to 'j'

Working with pointers

Here are pointers' most common use cases:

  • Passing pointers to functions allows the functions to mutate the pointers' underlying data.
  • Pointers help distinguish between a zero value and an unset value.

Pointers and optimization

Pointers are NOT a silver bullet for performance optimization. Using pointers have its own costs.

Additional reading

Go: Are pointers a performance optimization? by Kale Blankenship

Structs

A struct is a collection of fields.

type Example struct {
  X int
  Y int
}

e := Example{1, 2}

p := &e // Create pointer 'p' that points to the struct 'e'

fmt.Println(v.X) // Will print 1
fmt.Println(p.X) // Will print 1. Note that we don't need to do (*p).X

Arrays & Slices

Arrays:

// Declare 'a' as an array of 10 integers
// Note that you can't do a := [10]int
// Arrays can't be resized
var a [10]int 

// To use the ':=' syntax, you need to define the array's variables as well
// Any variable not defined inside the "{}" will default to its zero value e.g. 0, false, "", etc.
b := [3]int{1, 2, 3} // [1 2 3]
b := [3]int{} // [0 0 0]

Slices are dynamically-sized, flexible views into the elements of arrays.

Slices do not store any data.

Changes in the elements of slices also modifies the corresponding elements of their underlying arrays.

A slice's length is the number of elements it contains. This can be obtained with len(s). A slice's length can be extended by re-slicing it (provided the new length does not exceed its capacity).

A slice's capacity is the number of elements in the underlying array. This can be obtained with cap(s).

The make function can be used to create slices, and in turn, dynamically-sized arrays.

a := [5]int{1, 2, 3, 4, 5} // [1 2 3 4 5]

var s []int = a[1:3] // Create the slice [2 3]
s2 := a[1:3] // Create the slice [2 3]
s3 := a[:] // Create the slice [1 2 3 4 5]

// Create the slice [1 2 3] without any base array
// Essentially this creates an array, and then builds a slice that references it
s3 := []int{1, 2, 3}

// Create a zeroed array and returns a slice that refers to that array
s4 := make([]int, 5) // len(s4) = 5
s5 := make([]int, 5, 6) // len(s5) = 5; cap(s5) = 6

Working with slices:

var s []int

s = append(s, 1) // [1]

Range

range iterates over a slice or map.

for i, v := range []int{1, 2, 3} {
  fmt.Println(i, v) // Will print "0 1", "1 2", "2 3"
}

for i, _ := range []int{1, 2, 3} {
  // Iterate through the slice, not caring about the slice's values
}

for i := range []int{1, 2, 3} {
  // Iterate through the slice, not caring about the slice's values (shorter way)
}

for _, v := range []int{1, 2, 3} {
  // Iterate through the slice, not caring about the index
}

Map

Maps hold key-value pairs.

A nil map has no keys, and additional keys can't be added to nil maps.

The make function returns a map of the given type.

type Person struct {
  FirstName, LastName string
}

// Declare map 'n'
var n map[string]Person

// Initialize map 'm' and add a value to it
m := make(map[string]Person)
m["a"] = Person{
  "John", "Smith",
}

// Declare map literal 'o'
var o = map[string]Person{
  "a": {"John", "Smith"},
}

Working with maps:

m := make(map[string]int)

// Insert / update key 'a' of map 'm'
m["a"] = 1

// Retrieve key 'a' of map 'm'
m["a"]

// Delete key 'a' of map 'm'
delete(m, "a")

// Test that key 'a' is present
// If key 'a' is in 'm', 'ok' will be 'true', else 'ok' will be 'false'
// If key 'a' is not in 'm', 'elem' will be the zero value for the element type of map 'm'
elem, ok := m["a"]

Methods

Go does not have classes. However, methods can be defined on types.

A method is a regular function with a special receiver argument.

type Pair struct {
  A, B int
}

// Declare method Sum() with receiver of type 'Pair' named 'p'
func (p Pair) Sum() int {
  return p.A + p.B
}

p := Pair{1, 2}
p.Sum() // 3

Methods can be declared on non-struct types.

type MyInt int

func (i MyInt) Abs() int {
  if i < 0 {
    return int(-i)
  }
  return int(i)
}

f := MyInt(-1)
f.Abs() // 1

Methods can be declared with pointer receivers. Methods with pointer receivers can modify the value to which the receiver points to.

By using pointer receivers, methods can modify the values that their receivers point to. This will also avoid copying values on each method call.

All methods on a given type should either have value or pointer receivers, but not a mixture of both.

type Pair struct {
  A, B int
}

func (p *Pair) IncreaseBy(n int) {
  p.A = p.A + n
  p.B = p.B + n
  
  // If the receiver is not a pointer receiver (i.e., if the '*' is removed from '(p *Pair)'),
  // the underlying p.A and p.B will never be modified. The operation p.A + n and p.B + n will
  // create a copy of p.A and p.B and operate on these copies instead.
  // Printing p.A and p.B within this method will thus give the correct modified values, but
  // printing p.A and p.B outside this method will give the original values.
  
  // Methods with value receivers can take either a pointer or a value as the receiver when 
  // they are called, and vice versa.
  // Functions, however, are more strict. Functions with a pointer argument must take a
  // pointer when they are called, for example.
}

p := Pair{1, 2}
p.IncreaseBy(10) // 'p' is now {11, 12}

Interfaces

An interface type is a set of method signatures.

A type implements an interface by implementing its methods. There is no explicit declaration of intent, or "implements" keyword.

An interface value is a tuple of a value and a concrete type: (value, type).

type Abser interface {
  Abs() int
}

type MyInt int

func (i MyInt) Abs() int {
  // Gracefully handle being called with a 'nil' receiver
  if i == nil {
    fmt.Println("<nil>")
    return
  }

  if (i < 0) {
    return int(-i)
  }
  return int(i)
}

a := MyInt(-1) // 'a' is a 'MyInt', which implements 'Abser'

The interface type that specifies zero methods is known as the empty interface. An empty interface may hold values of any type.

Empty interfaces are used by code that handles values of unknown type.

var i interface{}

func describe(i interface{}) {
	 fmt.Printf("(%v, %T)\n", i, i)
}

describe(i) // (<nil>, <nil>)

Type assertions

A type assertion provides access to an interface value's underlying concrete value.

// The interface value 'i' holds the concrete type 'T' and assigns the underlying 'T'
// value to the variable 't'.
// If 'i' does not hold a 'T', this statement will trigger a panic.
t := i.(T)

// This can be used to gracefully test whether an interface value holds a specific
// type. The 'ok' variable will be true with 'i' holds a 'T'
t, ok := i.(T)

Type switches

A type switch is a construct that permits several type assertions in series.

A type switch is like a regular switch statement, but the cases specify types rather than values, and those values are compared against the type of the value held by the given interface value.

switch v := i.(type) {
case T:
  // here v has type T
case S:
  // here v has type S
default:
  // no match; here v has the same type as i
}

Stringers

A Stringer is a type that can describe itself as a string.

type Stringer interface {
  String() string
}

Errors

Functions often return an error value, and calling code should handle errors by testing whether the error equals nil.

type error interface {
  Error() string
}

i, err := strconv.Atoi("42")
if err != nil {
    fmt.Printf("couldn't convert number: %v\n", err)
    return
}
fmt.Println("Converted integer:", i)

Readers

The io package specifies the io.Reader interface, which represents the read end of a stream of data.

Implementations of this interface can be found in the Go standard library here.

A common pattern is an io.Reader that wraps another io.Reader, modifying the stream in some way.

// Read populates the given byte slice with data and returns the number of bytes
// populated and an error value. It returns an 'io.EOF' error when the stream
// ends.
func (T) Read(b []byte) (n int, err error)

/*
 * An example reader:
 */

import (
  "fmt"
  "io"
  "strings"
}

func main() {
  r := strings.NewReader("Hello there!")
  
  // The reader will consume its output 8 bytes at a time
  b := make([]byte, 8)

  for {
    n, err := r.Read(b)
    
    fmt.Printf("n = %v err = %v b =%v\n", n, err, b)
    fmt.Printf("b[:n] = %q\n, b[:n]) // Will print "Hello, t", "here!", and ""
    
    if err == io.EOF {
      break
    }
  }
} 

Images

Package image defines the Image interface

package image

type Image interface {
  ColorModel() color.Model
  Bounds() Rectangle
  At(x, y int) color.Color
}

Goroutines

A goroutine is a lightweight thread managed by the Go runtime.

Goroutines run in the same address space, so access to shared memory must be synchronized. The sync package provides useful primitives, although you won't need them much in Go as there are other primitives.

// Start a new goroutine running f(x, y, z)
// The evaluation of 'f', 'x', 'y', and 'z' happens in the current goroutine
// and the execution of 'f' happens in the new goroutine.
go f(x, y, z)

Channels

Channels are typed conduits through which you can send and receive values with the channel operator <-.

Channels must be created before use e.g. make.

By default, sending and receiving blocks until the other side is ready.

ch := make(chan int)

// Send v to channel 'ch'
ch <- v

// Receive from 'ch' and assign valuye to 'v'
v := <- ch

Example channel usage:

// A function that sums a slice of integers and sends the result 
// to a channel
func sum(s []int, c chan int) {
  sum := 0
  
  for _, v := range s {
    sum += v
  }
  
  c <- sum // send sum to c
}

func main() {
  s := []int{7, 2, 8, -9, 4, 0}
  ch := make(chan int)
  
  // Split slice 's' in half and distribute work between 2
  // goroutines
  go sum(s[len(s)/2:], c)
  go sum(s[:len(s)/2], c)
    
  // Once both goroutines have completed their computations,
  // calculate the final result.
  x, y := <-ch, <-ch
  
  fmt.Println(x, y, x+y) // Will print "17 -5 12"
}

Channels can be buffered. Sends to a buffered channel block only when the buffer is full. Receives from a buffered channel block only when the buffer is empty.

// Create a channel with buffer length 100
ch := make(chan int, 100)

Range and Close

A sender can close a channel with close(ch) to indicate that no more values will be sent. Receivers can test whether a channel has been closed by assigning a second parameter to the receive expression.

When used with a loop, the expression for i := range ch receives values from the channel 'ch' repeatedly until 'ch' is closed.

Only the sender should close a channel. A receiver should never close a channel.

Channels aren't like files and normally don't need to be closed. It's only necessary to close channels when the receiver must be told there are no more values incoming (e.g. terminating a range loop).

// 'ok' is 'false' if there are no more values to receive
// and the channel is closed
v, ok := <- ch

Select

The select statement lets a goroutine wait on multiple communication oeprations.

A select blocks until one of its cases can run, then it executes that case. If multiple cases are ready, a random case will be chosen.

The default case in a select is run if no other case is ready.

select{
case i := <-ch:
  // Use i
default:
  // Receiving from 'ch' would block
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment