Tomas Vik

Go

[[Go/Code Review]]

Learning resources:

Setup

  • $GOROOT - where your SDK is
  • $GOPATH - where your go src, pkg, and bin live

Modules

  • got mod init gitlab.com/viktomas/{name}
  • go mod init creates a new module, initializing the go.mod file that describes it.
  • go build , go test , and other package-building commands add new dependencies to go.mod as needed.
  • go list -m all prints the current module’s dependencies.
  • go get changes the required version of a dependency (or adds a new dependency).
  • go mod tidy removes unused dependencies.
  • TODO read about minimal version selection https://stackoverflow.com/a/70006832/606571

Build

  • go build -C dir -o output-file this will build the package in dir and will produce output-file if the package is main package. It will follow all the imports and will build those packages as well

Variables

  • Naming conventions

    • camel case - seasonNumber
    • acronyms all uppercase - theURL
    • first letter case decides public/private
    • longer names for longer lifespan
  • Declaration

    • var foo int - declare in different scope than where we assign, or just want to create zero value
    • var foo int = 42 - when compiler guesses wrong
    • foo := 42 - all the time
  • Type conversions

    • var i int = 42
      var j float32
      j = float32(i)
      
    • for converting numbers to strings, you have to use strconv package, otherwise go will use int number as the unicode number (e.g. converting 42 to *)
    • go doesn’t do automatic type conversion because it might loose information
    • It’s different from casting, casting is telling the runtime to trust us that there is a different type, conversion transforms one value in memory to a different representation
    • You can do explicit type conversion between compatible structs

Primitive types

  • Boolean

    • zero value is false
  • Numeric types

    • zero value is always 0 or equvialent
    • Integer int

      • we don’t know the size, it will be at least 32, but it might be 64 bits
      • also can be unsigned int with uint
    • Float

      • f := 3.14 - always initialises to float64
  • String

    • collection of UTF-8 characters
    • Immutable
    • can be converted back and forth with []byte array

Constants

  • const keyword
  • naming is the same as variables
  • they need to be set up at compile time, you can assign the result of a function call to constant
    • const gets replaced with literal everywhere
  • iota
    • scoped to a constant block
    • every time we use it it’s like using i++ starting with 0
    • const (
      	a = iota // will be 0
          b // 1
          c // 2
      )
      
    • be careful, zero value is 0 so uninitialised variable is going to be equal to the first constant
      • solution _ = iota or errorState = iota
    • iota should be used only for constants where the numerical representation doesn’t matter and is not stored
      • Don’t use iota for defining constants where its values are explicitly defined (elsewhere). For example, when implementing parts of a specification and the specification says which values are assigned to which constants, you should explicitly write the constant values. Use iota for “internal” purposes only. That is, where the constants are referred to by name rather than by value. That way you can optimally enjoy iota by inserting new constants at any moment in time / location in the list without the risk of breaking everything.

      • Danny van Heumen

Arrays

  • a := [3]int{97, 85, 93}
    • shorthand a := [...]int{97, 85, 93}
  • len(a) to find out the length
  • arrays are values
    • copied during re-assignment
    • passed as values to functions

Slices

  • a := []int{97, 85, 93}
    • same initialisation as an array, but without specifying the length
  • Slicing operations
    • a := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
      b := a[:] // slice of all elements
      c := a[3:] // slice from 4th element (index 3 inclusive)
      d := a[:6] // slice first 6 elements (index 6 exclusive)
      e := a[3:6] // slice the 4th, 5th, and 6th elements
      
      • remove first element a[1:]
      • remove last element a[:len(a)-1]
  • make([]int, len, cap) - initialise slice with len length and cap capacity
  • append(arr, el, el1, el2) appends element to the slice, it makes the underlying array larger if needs be
    • append can use “spread” operator to append another array: a = append(a, []int{1,2,3,4}...)
  • all stack operations are manipulating one underlying array

Maps

  • m := map[string]int{"a": 1, "b": 2}
  • key type has to be testable for equality, struct, map and function can’t be a key
  • val := myMap["key"]
  • val, ok := myMap["key"] - we can check if the key exists, ok is convention
  • you can use len()
  • delete(myMap, "key")

Structs

  • type Doctor struct {
      number int
      actorName string
      companions []string
    }
    d := Doctor{
      number: 3,
      actorName: "John Doe",
      companions: []string{"a", "b"}
    }
    //or positional syntax (not recommended)
    d = Doctor{3, "John Doe", []string{"a", "b"}}
    
  • Anonymous struct: a := struct{name: string}{name: "Tomas"}
  • Structs are passed by value
  • Embedding type Bird struct {Animal, canFly: bool}
    • I need to be aware of the embedding when creating an instance: bird := Bird{Animal: Animal{}, canFly: true}
    • Embedding is a good idea when we want consumers of our libraries to embed a complicated structure rather than extend an interface.
  • Tags
    • type Animal struct {
        Name string `required max:"100"`
        Origin string
      }
      t := reflect.TypeOf(Animal{})
      field, _ := t.FieldByName("Name")
      field.Tag
      
  • When not working with JSON (or other external protocols), resist the temptation to use a pointer field to indicate no value. While a pointer does provide a handy way to indicate no value, if you are not going to modify the value, you should use a value type instead, paired with a boolean.

    • John Bonder - Learning Go
  • Internal implementation introduces another two terms: Alignment and Padding
    • alignment and padding dictate how is the struct stored in memory, depending on the size of the word. It is a mechanism to avoid storing a value over two words, which would mean doubling the memory access for working with that value
  • Embedding

    • You can embed a struct in another struct, then you can access the child fields and methods through the parent:
      • type user struct {
          name string
          email string
        }
        
        type admin struct {
          user
          accesLevel string
        }
        

If statement

  • Initializer:
    • if val, ok := myMap["key"]; ok {
        fmt.Println(val)
      }
      
      • The variables are scoped to the if block
  • Short circuiting - when evaluating || go stops evaluating after the first true, && stops evaluating after the first false

Switch statement

  • switch <tag> {
    case 1, 5, 10:
      fmt.Println("one, five, or ten")
    default: 
      fmt.Println("not one")
    }
    
  • You can use initialiser as well
  • No braces, any statements between the case and the next case are part of the block
  • Tagless syntax:
    • each case can have a full comparison case i < 10:
  • Implicit break between cases, you can force immediate execution of the next case (without checking the condition) by using the fallthrough keyword
  • Type switch: switch i.(type) {}
  • Break early with break

For statement

  • for i:=0;i<10;i++
  • for i, j := 0, 0; i<5; i, j =i+1, j+1
  • TODO i++ is a statement, not an expression - investigate more about this design choice
  • we can omit the initialiser for ; i<10;i++ or even the continuation statement for ;i<10; (which is effectively a while loop)
    • syntax sugar for while loop: for i<10 (omitting the semicolons)
  • for { break } for when we don’t know when to finish
  • continue - skip this iteration and start over with the next loop iteration
  • we can label which statement we want to break out of
    • Label:
      for i := 0;i<5;i++ {
        for j := 0;j<5;j++ {
          if i*j > 10 {
            break Label
          }
          fmt.Println(i*j)
        }
      }
      
  • range : for k,v := range s {} to loop over arrays, slices, and maps

Defer

  • defer moves statement after the function is done, but before it returns any results to the calling function
  • a := "start"
    defer fmt.Println(a)
    a = "end"
    
    • Will print “start” because defer evaluates the argument the moment it is called
  • panic happens after defer statements have been executed
  • we can pass anonymous function to the defer statement

Panic/Recover

  • panic(error)
  • err := recover()
    • defer func(){
        err := recover()
        handleError()
      }()
      

Pointers

  • var a *int - declare pointer
  • & - “address of” operator - a := &b
  • * - “dereferencing” operator a := *b
    • lower precedence than the . operator => (*a).val
  • pointer arithmetic available in the unsafe package
  • new(myStruct) - we get back a pointer to an empty struct
  • compiler automatically dereferences before using the. operator
    • (*a).val == a.val
  • When function returns pointer to a local variable, the Go runtime has to copy the value on the heap

Functions

  • You can pass arguments by reference or by value
  • maps and slices are always passed by reference
  • func sum(values ...int) int {} Variadic parameter
    • values is a slice
  • You can return local variable as a pointer
  • func sum(values ...int) (result int){}
    • named return variable result
  • func divide(a float64, b float64) (float64, error)
    • we can return error object as the second parameter
  • Anonymous functions

    • best practice is to pass in the variable from outer scope as arguments
      • func(a){
          fmt.Println(a)
        }()
        
    • var divide func(float64, float64)(float64, error)

Methods

  • functions that are executed on a know context (structs, primitive types)
  • func (g greeter) greet() {}
    • g greeter is a value receiver
      • Use value receiver if all methods can use value receivers - that means they don’t need to mutate data and don’t work with synchronisation primitives
    • g *greeter is a pointer receiver
      • Use pointer receiver if even a single method needs to use pointer receiver
    • Mixing the two is bad, because the type dictates whether it’s a value or a reference (user is a reference, time is a value) then you should respect the type and use its semantics even if you would otherwise use the other option

Interface

  • Interface should represent behaviour, Animal -> Barker
  • Interface value is internally represented as a pair of pointers:
  • type Writer interface {
      Write([]byte) (int, error)
    }
    
  • Naming convention is that if there’s only one method, we name the interface with the name of the method + er e.g. Write => Writer
  • When I want to add methods to a type, I have to have a control over it in my package. type Counter int
  • Composing same as struct embedding
    • type WriterCloser interface {
        Writer
        Closer
      }
      
  • t, ok := val.(TypeToConvertTo) - type conversion for interfaces
  • interface{} - empty interface
  • Receiver trickery:
    • if the interface is stored as pointer, it can use both value and pointer receivers
    • if the interface is stored as a value, it can only use value receivers

Printing

  • %v - default printer for the value
  • %+v - prints keys as well
  • %#v - prints the full go syntax (invokes the go.Stringer interface if the value implements it)
  • %[1]v - refers to the first argument (1 indexed)

Errors

  • Internally, error is implemented with errorString in the errors package
  • fmt.Errorf("Error happened %w") - %w format verb wraps the error so it can be later unwrapped:
  • When you handle multiple errors with the same treatment (e.g. prepending the same message) you can use defer with named return value:
    • Before: collapsed:: true
      • func DoSomeThings(val1 int, val2 string) (string, error) {
            val3, err := doThing1(val1)
            if err != nil {
                return "", fmt.Errorf("in DoSomeThings: %w", err)
            }
            val4, err := doThing2(val2)
            if err != nil {
                return "", fmt.Errorf("in DoSomeThings: %w", err)
            }
            result, err := doThing3(val3, val4)
            if err != nil {
                return "", fmt.Errorf("in DoSomeThings: %w", err)
            }
            return result, nil
        }
        
    • After: collapsed:: true
      • func DoSomeThings(val1 int, val2 string) (_ string, err error) {
            defer func() {
                if err != nil {
                    err = fmt.Errorf("in DoSomeThings: %w", err)
                }
            }()
            val3, err := doThing1(val1)
            if err != nil {
                return "", err
            }
            val4, err := doThing2(val2)
            if err != nil {
                return "", err
            }
            return doThing3(val3, val4)
        }
        
      • `
  • Error values

    • You can define a “sentinel” error as var ErrBadRequest := errors.New("Bad Request")
    • This is useful if there is no need for additional context
  • Using type to provide more context

    • course material
    • Type assertion switch:
      • if err != nil {
          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)
        }
        

Performance

Design values

  • Write less code, for every 20 lines of code you’ll introduce one bug

[[Go/Concurrency]]