Johnny.sh

Goroutines and Channels

Go uses something called goroutines as its paradigm for concurrency and asynchronous programming.

For the most part, this can be thought of as an abstraction on top of multithreaded computing, however we don’t deal with threads directly — that’s handled by the golang implementation, depends on the OS and whatnot.

The go Keyword

Firstly, the most basic concept is that every go program is at least one goroutine. This is the main function in the entrypoint of the program, and it’s goroutine is sually referred to as “the main goroutine”.

Any goroutine can spawn more goroutines. To spawn a goroutine, you just need to use the go keyword in your code in front of a function call. Then, that function will continue to execute asynchronously in another goroutine while your code keeps executing forward.

for _, url := urls {
  go fetch(url)
  fmt.Println("We are in the process of fetching %s", url)
}

A simple example — we keep moving forward while the URLs in the list are fetching, instead of waiting around for the response before initializing the fetching of the next URL.

Channels

We can use channels to communicate between goroutines. We can send stuff and receive stuff from channels. Honestly, a channel is…a channel. The name is quite fitting. We use <- to send and recieve from a channel.

messages := make(chan string)
err := connectToHost(responses)
if err != nil {
  fmt.Println("Failed to connect to host")
  os.Exit(1)
}
for {
  fmt.Println(<-messages)
}

This is kind of pseudocode, but assume that connectToHost is a function that takes a channel as an argument, and will periodically send messages to that channel. We use an infinite for loop, which will iterate everytime a message is sent on the responses channel. This is an example of receiving from a channel.

We can also send to a channel, which looks like this:

func listenForUserInput(conn io.Reader, messages chan<- string ) {
  input := bufio.NewScanner(conn)
  for input.Scan() {
      messages <- input.Text()
  }
}

Note: the syntax messages chan<- string indicates that messages channel can only be sent strings, not receive them. At least for this function.

Somewhere, in another program which is also connected to the same hypothetical host, we can listen to user input and push the text into the messages channel as well. This is hypothetical, the example is a bit contrived, but that’s what the syntax for sending into a channel looks like.

Closing Channels

Two things that must be noted about using channels:

  1. You will hit deadlock if you’re listening on a closed channel, you can check if a channel is closed by using the second return value (a boolean) when listening to a channel
// somewhere in a loop
msg, ok := <- messages
if !ok {
  break// break out of loop
}
  1. Always close a channel from the sending end, never close a channel from the receiving end.
func processOrders(orderIds []string, listener chan string) {
  defer close(listener)
  for orderId := range orderIds {
    // process order by id
    listener <- orderId
  }
}

If you try to send a message to an already closed channel, go runtime will panic and exit.

Multiplexing

There is a reserved keyword select in Go, which is used explicityly for multiplexing goroutines. It looks similar to a switch case.

select {
  case winner := <-raceChannel:
    fmt.Println("The winner is: %s", winner)
  case <- cancel:
    fmt.Println("The race has been cancelled. There will be no winner this year".)
}

Each case in a select statement refers to some communication from a channel. When one is received, the block for that case executes, and the rest of the program continues executing. The other cases do not execute.

A similar case to wrap things up, the example from the book:

fmt.Println("Commencing countdown.  Press return to abort.")
select {
  case <-time.After(10 * time.Second):
      fmt.Println("Count down done.")
  case <-abort:
      fmt.Println("Launch aborted!")
      return
} 
launch()

This function is cool because it gives an example of an inline channel, using time.After.

WaitGroups, what are they?

You also often see in Go code that uses sync.WaitGroup. This function comes from the sync package of go standard lib, and we can use it to await multiple goroutines to be done and closed.

import (
  "sync"
)

func main() {
  wg := &sync.WaitGroup{}
  for _, item := os.Args[1:] {
    wg.Add(1)
    go func(item){
      defer wg.Done()
      // do some async task with item
    }(item)
  }
  wg.Wait()
}

This will wait for all of the goroutines spawned in the for loop to finish executing. WaitGroup is basically a counter. You increment and decrement it, and wg.Wait essentially waits for the count to go back down to 0.

Resources

Some reading about goroutines and channels: syncrhonization patterns in go

Last modified: March 27, 2023
/about
/uses
/notes
/talks
/projects
/podcasts
/reading-list
/spotify
© 2024