gothamgo 2018 workshop afternoon

gothamgo 2018 workshop afternoon

Testing in Go

Maps

Initializing maps

  beatles := map[string]string{
    "John":   "guitar",
    "Paul":   "bass",
    "George": "guitar",
    "Ringo":  "drums",
  }

Map keys

type simple struct {
  ID int
}

type complex struct {
  f func(id int) simple
}

func main() {
  m := map[simple]string{}

  fmt.Println(m)

  // invalid map key type complex
  m1 := map[complex]string{}
}

Iterators

for key, value := range beatles {
  fmt.Printf("%s plays %s\n", key, value)
}

Validating if an accessed value exists

  // Look up the key `Bob` and detect that it wasn't found by printing `not found`
  _, ok := beatles["Bob"]

  if !ok {
    fmt.Println("not found")
  }

  ...
  // or more simply
  if _, ok := beatles["Bob"]; !ok {
    fmt.Println("not found")
  }

Thread Safety

Pointers

pass by value vs pass by reference

Pass by value

type Beatle struct {
  Name string
}

func main() {
  b := Beatle{Name: "Ringo"}
  changeBeatleName(b)
  fmt.Println(b.Name) // Ringo
}

func changeBeatleName(b Beatle) {
  b.Name = "George"
  fmt.Println(b.Name) // George
}

To pass by reference

To indicate we are expecting a pointer, we use the * modifier:

func changeBeatleName(b *Beatle) {}

To get the address of a value we use the & modifier:

&Beatle{}

And we can store pointers too:

b := &Beatle{}
func main() {
  x := "George"
  printValue(&x)
  fmt.Println(x)
}

func printValue(s *string) {
  // print the pointer value
  fmt.Println(s)

  // print the value of a pointer to this pointer
  fmt.Println(&s)

  // print the string value
  fmt.Println(*s)

  // change the string value
  *s = "Susan"
}

// Produces
// 0xc42000e1e0
// George
// Susan

Pointer type checking

go is so strongly typed that you can’t do this

type Beatle struct {
    Name string
}

type NonBeatle struct {
    Name string
}

func main() {
    b := &Beatle{Name: "Ringo"}
    n := &NonBeatle{Name: "Susan"}

    changeUserName(n)
}

func changeUserName(b *Beatle) {
    b.Name = "George"
    fmt.Println(b.Name) // George
}

will result in

cannot use n (type *NonBeatle) as type *Beatle in argument to changeUserName

Security vs. Performance

By default go passes by value for security, but it can be more performant to pass a pointer to a large file by pointer (reference)

Interfaces

type Entertainer interface {
  Play()
}

// to invoke a passed concrete struct that implemented the interface
func entertain(e Entertainer) {
  e.Play()
}

Inteface implementation

type scribe struct {
  data []byte
}

// note the pointer receiver `s *scribe` is because you need to mutate the byte field, you have to pass a reference
// if you change it to a value accessor `s scribe`, value accessor will not allow a mutate
// the compiler will allow both but it's more maintainability
func (s *scribe) Write(p []byte) (int, error) {
  s.data = p
  return len(p), nil
}

Pointer receiver vs. value receivers

type User struct {
  First string
  Last  string
}

func (u *User) String() string {
  return fmt.Sprintf("%s %s", u.First, u.Last)
}

func pretty(v fmt.Stringer) {
  fmt.Println(v.String())
}

func main() {
  u := User{First: "Rob", Last: "Pike"}
  pretty(u)
}

/* produces
   cannot use u (type User) as type fmt.Stringer in argument to pretty:
   User does not implement fmt.Stringer (String method has pointer receiver)

 Instead
 A. use
 func (u User) String() string {
  return fmt.Sprintf("%s %s", u.First, u.Last)
 }

 or B. pass the reference
 pretty(&u)
*/

Stylist notes

// good
type Writer interface {
  Write(p []byte) (int, error)
}

// bad
type Emailer interface {
  Email string
}

Empty interface

// generic empty interface:
interface{}

// a named empty interface:
type foo interface{}

Errors

No try/catch

f, err := os.Open("/not/a/real/path")
if err != nil { // note you should explicitly check for nil, `if !err` only works for bool
  // alert
  return or break
}

// f is the file handle

creating errors

Defining errors in the standard library

// in the `errors` package
errors.New("a message")

// in the `fmt` package
fmt.Errorf("a %s message", "formatted")

Defining custom errors

type error interface {
  Error() string
}

Recovering from panics

func main() {
  defer func() {
    if err := recover(); err != nil {
      fmt.Println(err)
    }
  }()
  a := []string{}
  a[42] = "Bring a towel"
}

Sentinenal errors

Alternate error package with stack and wrap

Best practices

Concurrency

go routine

func sayHello() { fmt.Println(“hello”) }


This won't print anything, because main exited because the goroutine finished. Instead just wait.

### waitgroup
[waitgroup](https://golang.org/pkg/sync/#WaitGroup)

* Add function
Add adds delta, which may be negative, to the WaitGroup counter. If the counter becomes zero, all goroutines blocked on Wait are released. If the counter goes negative, Add panics.

```go
func main() {
  wg := &sync.WaitGroup{}
  wg.Add(1)
  go sayHello(wg)
  wg.Wait()
}

func sayHello(wg *sync.WaitGroup) {
  defer wg.Done()
  fmt.Println("hello")
}

anonymous functions and closure

Instead of explicitly defining sayHello, you can use an anonymous function

for i := 0; i < 5; i++ {
  go func(i int) {
    defer wg.Done()
    fmt.Printf("starting %d...\n", i)
    time.Sleep(1 * time.Second)
    fmt.Printf("ending %d...\n", i)
  }(i)
}

Note, you must pass in the i variable. If you don’t, the scope of i would at the outer scope. Since the goroutines are being added to the execution stack, but not run, by the time they run, and access that memory space, i has already been changed.

Sending i in as an argument creates a new scope and copies the value to a new memory location to preserve the proper value.

Mutex

A mutex allows you to synchronize goroutines for safe access to the same shared memory. There are two types of mutexes

  1. full lock
  2. read/write lock
func main() {
  var mu sync.Mutex
  var counter int

  set := func(i int) {
    mu.Lock()
    defer mu.Unlock()
    counter = i
  }

  get := func() int {
    mu.Lock()
    defer mu.Unlock() // Note, defer will execute at the exit of a function, not at the exit of a code {} block
    return counter
  }

  go func() {
    for {
      time.Sleep(500 * time.Millisecond)
      i := get()
      fmt.Printf("counter: %d\n", i)
    }
  }()

  var wg sync.WaitGroup

  for i := 0; i <= 10; i++ {
    wg.Add(1)
    go func(i int) {
      set(i)
    }(i)
    time.Sleep(750 * time.Millisecond)
  }

  wg.Done()
}

Channel

Channels are a typed conduit through which you can send and receive values.

initialize a channel

message := make(chan string)

Send and receive

ch <- // data is going into the channel
<- ch // data is coming out of the channel

Buffered channels

By default channels are unbuffered – sends on a channel will block until there is something ready to receive from it. You can create a buffered channel - which allows you to control how many items can sit in a channel unread.

  // Adding a second argument to the make function creates a buffered channel
  messages := make(chan string, 2)

Channels are not message queues. Only one channel will receive a message sent down a channel. If multiple goroutines are listening to a channel, only one will receive the message.

Closing Channels

You can signal that there are no more values in a channel by closing it.

func echo(s string, c chan string) {
  for i := 0; i < cap(c); i++ {
    // write the string down the channel
    c <- s
  }
  close(c)
}

func main() {
  // make a channel with a capacity of 10
  c := make(chan string, 10)

  // launch the goroutine
  go echo("hello!", c)

  for s := range c {
    fmt.Println(s)
  }
}

books and channel

Books

Videos