$30 off During Our Annual Pro Sale. View Details »

Go Gotchas

Mike Kaperys
September 12, 2019

Go Gotchas

I’m sure everyone at some point has spent, probably, far longer than you should have, staring at a piece of code thinking “why the hell isn’t this working?!” - myself included. This talk somewhat aims to prevent that happening. We’ll cover some counter-intuitive language gotchas, some common mistakes and some general “tip hazards”.

Mike Kaperys

September 12, 2019
Tweet

More Decks by Mike Kaperys

Other Decks in Technology

Transcript

  1. Go Gotchas
    Mike Kaperys

    View Slide

  2. Hey!
    I’m Mike, I’m a software engineer
    on the telecoms team at Utility
    Warehouse (we’re hiring!).
    I’ve been using Go for around 2 and
    a half years, and a software
    engineer for around 5.
    I’m a co-organiser of GoSheffield.
    Please come and speak to us if
    you’re interested in giving a talk!
    @_kaperys
    kaperys.io

    View Slide

  3. We’ll cover...
    ● Some counter-intuitive
    language gotchas
    ● Some common mistakes and
    misconceptions
    ● Some things you might not
    know

    View Slide

  4. package main
    import "fmt"
    type customError struct{}
    func (customError) Error() string { return "" }
    func main() {
    fmt.Println(f() == nil)
    }
    func f() error {
    var err *customError
    fmt.Println(err == nil)
    return err
    }

    View Slide

  5. View Slide

  6. Why?
    An interface contains two values under the hood - a type (T), and a value
    (V). For an interface to equate to nil, both values must be unset.
    ”An interface value is nil only if the V and T are both unset, (T=nil, V is not set)
    [...]. Such an interface value will therefore be non-nil even when the pointer value
    V inside is nil.”
    https://golang.org/doc/faq#nil_error

    View Slide

  7. package main
    import "fmt"
    type customError struct{}
    func (customError) Error() string { return "" }
    func main() {
    fmt.Println(f() == nil) //
    }
    func f() error {
    var err *customError // nil
    fmt.Println(err == nil)
    return err // conversion to error interface
    }

    View Slide

  8. package main
    import "fmt"
    func main() {
    var src, dst []int
    src = []int{1, 2, 3}
    copy(dst, src)
    fmt.Println("dst:", dst)
    }

    View Slide

  9. View Slide

  10. Why?
    The number of elements copied by the copy function is min(a, b).
    Therefore, the destination slice, a, must be allocated with enough space
    for len(b) elements.
    ”Both arguments must have identical element type T and must be assignable to a
    slice of type []T. The number of elements copied is the minimum of len(src) and
    len(dst). As a special case, copy also accepts a destination argument assignable
    to type []byte with a source argument of a string type.”
    https://golang.org/ref/spec#Appending_and_copying_slices

    View Slide

  11. package main
    import "fmt"
    func main() {
    var src, dst []int
    src = []int{1, 2, 3}
    dst = make([]int, len(src))
    copy(dst, src)
    fmt.Println("dst:", dst) // dst: [1 2 3]
    }

    View Slide

  12. package main
    import (
    "fmt"
    "strings"
    )
    func main() {
    res := strings.TrimRight("ABBA", "BA")
    fmt.Println(res)
    }

    View Slide

  13. View Slide

  14. Why?
    The Trim, TrimLeft and TrimRight functions remove all unicode code
    points in the cutset (the second argument). Therefore, all As and Bs are
    removed from the string.
    ”TrimRight returns a slice of the string s, with all trailing Unicode code points
    contained in cutset removed. To remove a suffix, use TrimSuffix instead.”
    https://golang.org/pkg/strings/#TrimRight

    View Slide

  15. package main
    import (
    "fmt"
    "strings"
    )
    func main() {
    res := strings.TrimSuffix("ABBA", "BA")
    fmt.Println(res) // AB
    }

    View Slide

  16. package main
    import "log"
    func main() {
    for {
    switch {
    case true:
    break
    default:
    log.Fatal("I don't know what to do!")
    }
    log.Println("We broke out of the switch!")
    }
    log.Println("We broke out of the loop!")
    }

    View Slide

  17. View Slide

  18. Why?
    The break keyword breaks only the innermost for, switch or select
    statement. Therefore breaking out of the switch, but remaining inside
    the for loop.
    Using a “label” enables break (as well as continue and goto) to target a
    specific statement.
    https://golang.org/ref/spec#Break_statements

    View Slide

  19. package main
    import "log"
    func main() {
    loop:
    for { // `break loop` breaks this for
    switch { // Previously `break` broke this switch
    case true:
    break loop
    default:
    log.Fatal("I don't know what to do!")
    }
    log.Println("We broke out of the switch!")
    }
    log.Println("We broke out of the loop!")
    }

    View Slide

  20. package main
    import "fmt"
    func main() {
    fmt.Println(100 + 010 + 001)
    }

    View Slide

  21. View Slide

  22. Why?
    An integer type prefixed with 0 indicates a base 8 value. So in this case,
    we’re computing 100 + 8 (010) + 1 (001).
    “An optional prefix sets a non-decimal base: 0 for octal, 0x or 0X for
    hexadecimal.”
    https://golang.org/ref/spec#Integer_literals

    View Slide

  23. package main
    import "fmt"
    func main() {
    fmt.Println(100 + 10 + 1) // 111
    }

    View Slide

  24. package main
    import (
    "fmt"
    "time"
    )
    func main() {
    values := []string{"A", "B", "C"}
    for _, val := range values {
    go func() {
    fmt.Println(val)
    }()
    }
    time.Sleep(1 * time.Second)
    }

    View Slide

  25. View Slide

  26. Why?
    A variable captured in a closure is evaluated at the time the closure is
    executed. Therefore, our example prints “C, C, C” because each goroutine
    uses the same val variable.
    “When the closure runs, it prints the value of v at the time fmt.Println is executed,
    but v may have been modified since the goroutine was launched.”
    https://golang.org/doc/faq#closures_and_goroutines

    View Slide

  27. package main
    import (
    "fmt"
    "time"
    )
    func main() {
    values := []string{"A", "B", "C"}
    for _, val := range values {
    go func(val string) {
    fmt.Println(val) // val is now local
    }(val) // inject the variable
    }
    time.Sleep(1 * time.Second)
    }

    View Slide

  28. package main
    import (
    "fmt"
    "unsafe"
    )
    type myStruct struct {
    myInt bool // 1 byte
    myFloat float64 // 8 bytes
    myBool int32 // 4 bytes
    }
    func main() {
    a := myStruct{}
    fmt.Println(unsafe.Sizeof(a))
    }

    View Slide

  29. View Slide

  30. Why?
    Memory is allocated consecutively in chunks of 8 bytes.
    https://medium.com/@felipedutratine/how-to-organize-the-go-struct-in-order-to-save-memory-c78afcf59ec2
    8 + 8 + 8 = 24 bytes.
    We can optimise the struct by rearranging its properties.

    View Slide

  31. Why?
    Moving the float64 to the top of the struct changes the way the
    memory is allocated.
    https://medium.com/@felipedutratine/how-to-organize-the-go-struct-in-order-to-save-memory-c78afcf59ec2
    Now, the float64 occupies the first 8 bytes. The following bool and
    int32 are padded in the following 8 bytes to respect data alignment.
    8 + 8 = 16 bytes.

    View Slide

  32. package main
    import (
    "fmt"
    "unsafe"
    )
    type myStruct struct {
    myFloat float64 // 8 bytes
    myInt bool // 1 byte
    myBool int32 // 4 bytes
    }
    func main() {
    a := myStruct{}
    fmt.Println(unsafe.Sizeof(a)) // 16
    }

    View Slide

  33. View Slide

  34. Tooling to the rescue!
    ● cmd/vet (golang.org/cmd/vet)
    ● staticcheck (staticcheck.io)
    ● golangci-lint (github.com/golangci/golangci-lint)
    ● go-critic (go-critic.github.io)
    ● pprof (https://golang.org/pkg/runtime/pprof/)

    View Slide

  35. Thanks!
    Questions?
    @_kaperys
    kaperys.io

    View Slide