Go Study Questions

If you are brushing up on your Go skills, or learning Go, the following are things that you need to know about it.

Links

The Basics

Do not communicate by sharing memory; instead, share memory by communicating.

One of the underlying design principles behind Go is the idea that instead of managing access to shared memory between threads that you share the values of said memory between different threads.

https://go.dev/blog/codelab-share

Pass by Reference or Value

Go always passes a copy of the value. In some cases it may not seem like it because not all values are the actual object. Some values are actually addresses. For instance a slice value is a pointer to the address of the slice. The same is true with maps.

package main

import "fmt"

func mySlice(s []int) {
	fmt.Printf("%7s: %p\n\n", "mySlice", s)
}

func myMap(m map[string]int) {
	fmt.Printf("%7s: %p\n\n", "myMap", m)
}

func myInt(i int) {
	fmt.Printf("%7s: %v\n\n", "myInt", &i)
}

func main() {
	// Slice references are pointers, thus the pointer is copied
	s := []int{1, 2, 3}
	fmt.Printf("%7s: %p\n", "s", s)
	mySlice(s)

	// Same with maps
	m := map[string]int{
		"foo":  1,
		"blah": 2,
	}
	fmt.Printf("%7s: %p\n", "m", m)
	myMap(m)

	// A scalar variable not defined as a pointer has its value
	// copied, thus the address printed in the function is different
	// than the address outside the function.
	i := 647
	fmt.Printf("%7s: %v\n", "i", &i)
	myInt(i)
}

Outputs the following:

      s: 0xc00001a018
mySlice: 0xc00001a018

      m: 0xc0000161b0
  myMap: 0xc0000161b0

      i: 0xc00001c038
  myInt: 0xc00001c050

Further, the following items, along with anything that you use make, new, or & to create can be passed by reference, as opposed to being copied. That means the following are references and passed by reference:

  • slices
  • maps
  • channels
  • pointers
  • functions

String Formatting

Go provides string formatting inline with printf formatting rules

VerbType
%vvalue of a struct
%+vvalue of struct plus field names
%#vGo syntax representation of the source code that would produce the value
%Tthe type of the value
%tboolean
%dbase-10 formatting of integers
%fbase-10 decimal formatting of floats
%e or %E+/-e N scientific notation for floats
%bbinary representation
%cchar
%xhex encoded representation
will also render integers as base 16
%sstring
%qEscape double-quoted strings
%prepresentation of a pointer
%ncontrol the width and precision of a integer
%n.ncontrol the width and precision of a float
%nscontrol width of a string
%-nsright-padding for a string

Strings, bytes, runes and characters in Go

Slices

Slices are ubiquitous in Go and you can get by with a cursory understanding of them by treating them as arrays. This is not entirely incorrect and understanding the details of how they work is important for a competent go programmer.

Key Points

A slice created from another slice will point to and reference the same underlying array. Changes to the contents of the array via either slice will update the data in both slices.

s := []byte{'i', 'a', 'm', 'a', 's', 'l', 'i', 'c', 'e'}

// s1 slice will point the same underlying array
s1 := s[1:3]
fmt.Println(string(s1)) // am
s[1] = 'z'
fmt.Println(string(s1)) // zm

The difference between an array and a slice

package main

import (
	"fmt"
	"reflect"
)

func sliceAddress(s []string) {
        fmt.Printf("type=%+v, address=%p\n", s, s)
}

func arrayAddress(a [2]string) {
        fmt.Printf("type=%+v, address=%p\n", a, &a)
}

func main() {
	// Declare an array by specifying the number of elements/size of
	// the array that you want to allocate.
	a := [2]string{"an", "array"}

	// Declare a slice of the same type omitting the size which will
	// automatically create the underlying array and create a slice
	// that will point to the array.
	b := []string{"a", "slice"}

	fmt.Printf("a type=%+v, b type=%+v\n", reflect.TypeOf(a), reflect.TypeOf(b))


        fmt.Printf("address of slice in main=%+p\n", b)
        sliceAddress(b)
        fmt.Printf("address of array in main=%p\n", &a)
        arrayAddress(a)
}

// a type=[2]string, b type=[]string
//
// The slice has the same memory address in main and in the func.
// address of slice in main=+0xc00011e020
// type=[a slice], address=0xc00011e020
//
// The array has a different memory address indicating it was passed
// by value/copied.
// address of array in main=0xc00011e000
// type=[an array], address=0xc00011e040

Links

Arrays

Arrays are different from slices in that a slice is an abstraction over an array. That being said, there may be instances where you want to work with arrays directly.

One thing to keep in mind is that arrays are values. As such, when passing an array to a function you will pass a copy of the array and not a reference or pointer.

Arrays vs Slices

Range

When ranging over slices you have access to two values. For a slice: the index of the element in the slice and a copy of the value. The word copy is intentionally highlighted because that is exactly what you get, a copy of the element and not a reference to the element. If it is a pointer, you still get a copy to the pointer, and the behavior in that case is different than the following example where changes made to the copy that you get do not “write through” to the original element in the slice.

package main

import "fmt"

type Person struct {
	Name string
	Age  int
}

func NewPerson(name string, age int) Person {
	return Person{name, age}
}

func main() {
	// Create a slice of some Person instances
	a := []Person{NewPerson("Homer", 50), NewPerson("Marge", 49)}
	for _, p := range a {
		// p is a *copy* of the element in the underlying slice
		fmt.Printf("Before updating copy of element in range, p=%+v\n", p)
		p.Name = "Changed"
		fmt.Printf("After updating copy of element in range, p=%+v\n", p)
	}

	fmt.Println("Iterating over same slice after the update exersize")
	for _, p := range a {
		fmt.Println(p)
	}
}

This code will output the following, proving that, in at least this case where the elements are Person instances and not pointers, that range will return a copy of the element in the underlying array.

Before updating copy of element in range, p={Name:Homer Age:50}
After updating copy of element in range, p={Name:Changed Age:50}
Before updating copy of element in range, p={Name:Marge Age:49}
After updating copy of element in range, p={Name:Changed Age:49}
Iterating over same slice after the update exersize
{Homer 50}
{Marge 49}

Pointers and Structs

TBD

Functions, Function Values and Function Closures

Functions are first class objects in go. As a result they can be assigned to variables, passed as arguments to other functions and returned as values by other functions

Functions can be defined as types:

package main

import "fmt"

// Define a function type
type doSomething func(input string) error

// Define a function that accepts the doSomething type as an argument
func exec(arg string, d doSomething) error {
	return d(arg)
}

func main() {
	// Define a function that matches the doSomething signature
	f := func(input string) error {
		if input == "hello" {
			return nil
		}
		return fmt.Errorf("input was not 'hello'")
	}
	err := exec("goodbye", f)
	if err != nil {
		fmt.Println(err)
	}
}

Methods and Receiver Arguments

Value Receiver vs. Pointer Receiver

Interfaces

Interfaces in go are not defined explicitly. If something can do a specific set of things it can be used in a given context. Interfaces define the methods that a given type must implement to be of that interface type.

Embedding

Eli Bendersky has a great three-part series that explains embedding.

There are three basic kinds

  1. Structs in structs
  2. Interfaces in interfaces
  3. Interfaces in structs, which enables us to wrap an interface, reusing the embedded value to gain the base set of functionality with the ability to wrap/decorate said functionality.

Type assertions in switch cases

t, ok := i.(T)

switch v := i.(type) {
case T:
     // v is a type T
case U:
     // v is a type U
default:
     // v is the same type a i
}

type constraint

Stringer Interface

Readers

Generics

Type constraint

type Number interface {
     int64 | float64
}

Defer and Recover

defer enables you to specify statements to be executed after the completion of the enclosing function. Similar to finally in other languages. Defer statements are guaranteed to execute in the case of a panic in the function and enable the programmer to execute cleanup code after a function has run.

For instance, it is a good practice to use defer to close file handles, network sockets, or other resources immediately after they are opened in a function to best ensure that they will be closed after the function completes. They are executed in the order in which they are defined in a LIFO, stack-like fashion.

package main

import "fmt"

func main() {
   defer fmt.Println("defer 1")
   defer fmt.Println("defer 2")
   fmt.Println("something")
   defer fmt.Println("defer 3")
}

Prints out

something
defer 3
defer 2
defer 1

recover enables you to catch a panic in a function and then continue execution from where the function that caused the panic was called.

package main

import "fmt"

func mightPanic() {
   panic("encountered an error")
}

func doSomething() {
   defer fmt.Println("doSomething defer 1")

   // Ensure that we will not propagate a panic up the call stack
   defer func() {
      if r := recover(); r != nil {
         fmt.Printf("Recovered an error, r=%s\n", r)
      }
   }()
   defer fmt.Println("doSomething defer 2")
   mightPanic()
   fmt.Println("Returning normally from doSomething")
}

func main() {
   // Add to defer statements that will be executed after main completes
   defer fmt.Println("main defer 1")
   defer fmt.Println("main defer 2")

   // Call a function that then might call a function that panics
   fmt.Println("About to doSomething")
   doSomething()

   fmt.Println("After calling doSomething")
   defer fmt.Println("main defer 3")
}

Prints out

// Calling doSomething from main
About to doSomething

// The defers defined in doSomething execute in the reverse order in which
// they were defined.  The recover catches the panic.
doSomething defer 2
Recovered an error, r=encountered an error
doSomething defer 1

// doSomething DOES NOT continue executing after the mightPanic() function
// panics, and that function call is pulled off the call stack.  Control
// is returned to the main function.
After calling doSomething
main defer 3
main defer 2
main defer 1

Returning a value from a function that panics

Once a function panics and invokes the recover function, execution resumes AFTER the call to the panicking function. In most practical use-cases the function that might panic is doing something that is returning a value or at the very least somehow letting you know whether or not it succeeded. In order to return a value from a recover function we must define a named return for the function that defines the recover implementation. We can then set the named return value in the recover function

package main

import (
   "fmt"
   "os"
   "strconv"
)

func mightPanic(i int) (retval error) {
   defer func() {
      if r := recover(); r != nil {
         // Set the value to be returned from INSIDE the recover func
         retval = fmt.Errorf("recovered; r=%v", r)
      }
   }()
   if i%2 == 0 {
      panic("encountered an error")
   }
   return nil
}

func doSomething(i int) error {
   defer fmt.Println("doSomething defer 1")

   // Try to do something with data provided.  Whether or not mightPanic()
   // panics we will get back a value from the function call and resume
   // execution here.
   err := mightPanic(i)
   if err != nil {
      fmt.Printf("mightPanic returned an error; err=%+v\n", err)
   } else {
      fmt.Printf("mightPanic returned nil\n")
   }
   fmt.Println("After call to mightPanic")

   return nil
}

func main() {
   iStr := os.Args[1]
   i, err := strconv.Atoi(iStr)
   if err != nil {
      panic(err)
   }
   fmt.Printf("Input is i=%d\n", i)

   defer fmt.Println("main defer 1")
   defer fmt.Println("main defer 2")
   fmt.Println("About to doSomething")
   err = doSomething(i)
   fmt.Printf("After calling doSomething; err=%+v\n", err)
   defer fmt.Println("main defer 3")
}

Prints out

Input is i=2
About to doSomething
mightPanic returned an error; err=recovered; r=encountered an error
After call to mightPanic
doSomething defer 1
After calling doSomething; err=<nil>
main defer 3
main defer 2
main defer 1

Wait Groups

WaitGroups enable us to block while asynchronous tasks/go routines finish processing. Akin to Thread.join() in Java.

A few general points to keep in mind:

  1. Always create a pointer to a WaitGroup and pass a pointer to it to ensure that your code is actually decrementing the WaitGroup instance.
  2. Always call func (wg *WaitGroup) Add(delta int) outside of and before calling your go routine. Otherwise, the current thread might exit before the go routine actually runs.
  3. It is generally a good idea to call func (wg *WaitGroup) Done() in a defer early in your go routine in case it encounters an error. This ensures that you will decrement the WaitGroup and not deadock

Sync Pools

What are sync pools? https://medium.com/swlh/processing-16gb-file-in-seconds-go-lang-3982c235dfa2

Contexts and Cancel Functions

The key points of a context is that it carries (optional) deadlines, cancellation signals, and (optional) request scoped values across API boundaries when spinning up go routines that may then spin up other go routines to handle a request or complete a task. It enables the “grouping” of tasks together in a way that they can all be cancelled if the parent task signals completion or cancellation.

Some general guidelines (summarized from the API docs):

  • Do not pass Contexts inside structs, pass them explicitly to functions that require them as the first parameter, with the conventionally agreed upon name ctx.

Channels

  • Creating channels: ch := make(chan int)
  • Buffered channels: ch := make(chan int, 100)
  • Closing channels: A sender can close a channel to indicate that no more data will be written to it. Receivers can check whether a channel has been closed to checking for a boolean when reading from the channel; v, ok := <- ch. When a channel has been closed AND there are no more values to read from the channel ok will be false.
  • Channel directions
    • Read Only Channels: <-chan int
    • Write Only Channels: chan<- int
    • R/W Channels: chan int
  • Checking if a channel is empty: if len(chan) == 0

Timers and Tickers

TBD

Select Loops

Mutexes and Semaphores

Strings, chars, bytes, and runes

There is no char type in go. An ASCII character is 8-bits, or a byte which is a uint8. A rune is an int32 data type and meant for storing UTF-8 characters comprised of more than one byte.

To iterate over all of the characters in a string simply range over the string. Each element is a rune.

Go API Development