Concurrency With Channels: Safe Communication Between Goroutines
Overview
This chapter delves into Go’s powerful concurrency mechanism using channels to enable safe communication between goroutines. You will learn how to implement basic and advanced channel usage, including buffered and unbuffered channels, how to signal completion, and gracefully shut down applications. The chapter will guide you through practical examples and best practices for avoiding common concurrency pitfalls, making it easier to build robust, concurrent applications that scale efficiently.
Concurrency With Channels
Channels
Channels are a typed conduit through which you can send and receive values.
The key to understanding how channels work is quite simple:
- When does a channel block?
- When does a channel unblock?
Simple Channel Example
package main
import "fmt"
func main() {
// make a new channel of type string
messages := make(chan string)
// this goroutine launches immediately
go func() {
// This line blocks until the channel is read from
fmt.Println("waiting to write to messages")
messages <- "hello!"
fmt.Println("wrote to messages")
}()
// this line blocks until someone writes to the channel
fmt.Println("waiting to read from messages")
msg := <-messages
fmt.Println("read from messages")
fmt.Println(msg)
}package main
import "fmt"
func main() {
// make a new channel of type string
messages := make(chan string)
// this goroutine launches immediately
go func() {
// This line blocks until the channel is read from
fmt.Println("waiting to write to messages")
messages <- "hello!"
fmt.Println("wrote to messages")
}()
// this line blocks until someone writes to the channel
fmt.Println("waiting to read from messages")
msg := <-messages
fmt.Println("read from messages")
fmt.Println(msg)
}Understanding The Channel Arrows
In Go we use the <- operator to indicate sending or receiving information on a channel. At first it can be difficult to remember where the arrow goes.
To help remember where the arrow goes you just need to remember that the arrow points in the direction the data is traveling in regards to the channel.
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.
package main
import "fmt"
func main() {
// Adding a second argument to the make function creates a buffered channel
messages := make(chan string, 2)
// The program is no longer blocked on writing to a channel,
// as it has capacity to write
messages <- "hello!"
messages <- "hello again!"
// Reads are no longer blocked as there is already something to read from
fmt.Println(<-messages)
fmt.Println(<-messages)
}package main
import "fmt"
func main() {
// Adding a second argument to the make function creates a buffered channel
messages := make(chan string, 2)
// The program is no longer blocked on writing to a channel,
// as it has capacity to write
messages <- "hello!"
messages <- "hello again!"
// Reads are no longer blocked as there is already something to read from
fmt.Println(<-messages)
fmt.Println(<-messages)
}Buffered Channel And Delivery
Use buffered channels cautiously. They do not guarantee delivery of the message. It is your responsibility to ensure a channel is drained before exiting a routine.
messages := make(chan string, 10)
go func() {
for i := 0; i < 10; i++ {
msg := fmt.Sprintf("message %d", i)
messages <- msg
fmt.Printf("sent: %s\n", msg)
}
}()
m := <-messages
fmt.Printf("received: %s\n", m)
os.Exit(0)
// sent: message 0
// sent: message 1
// sent: message 2
// received: message 0
// sent: message 3
// sent: message 4package main
import (
"fmt"
"os"
)
func main() {
// section: code
messages := make(chan string, 10)
go func() {
for i := 0; i < 10; i++ {
msg := fmt.Sprintf("message %d", i)
messages <- msg
fmt.Printf("sent: %s\n", msg)
}
}()
m := <-messages
fmt.Printf("received: %s\n", m)
os.Exit(0)
// sent: message 0
// sent: message 1
// sent: message 2
// received: message 0
// sent: message 3
// sent: message 4
// section: code
}Channels Are Not Message Queues
In systems like RabbitMQ or Kafka, a message queue delivers each message to a specific consumer, and can guarantee fair distribution, redelivery on failure, and persistence. Developers coming from these systems sometimes expect Go channels to work the same way.
Go channels are simpler. They are an in-memory synchronization primitive, not a distributed messaging system:
- Only one goroutine receives each value sent on a channel
- There is no guaranteed fair distribution. One goroutine may receive more messages than another
- There is no redelivery. If a goroutine crashes after receiving, the value is gone
ch := make(chan int)
for i := 0; i < N; i++ {
go func(i int) {
for m := range ch {
fmt.Printf("routine %d received %d\n", i, m)
}
}(i)
}
for i := 0; i < N; i++ {
ch <- i
}
time.Sleep(time.Second)package main
import (
"fmt"
"time"
)
var N = 20
func main() {
// section: code
ch := make(chan int)
for i := 0; i < N; i++ {
go func(i int) {
for m := range ch {
fmt.Printf("routine %d received %d\n", i, m)
}
}(i)
}
for i := 0; i < N; i++ {
ch <- i
}
time.Sleep(time.Second)
// section: code
}
// section: output
// Output (order will vary):
// routine 0 received 0
// routine 1 received 18
// routine 3 received 7
// routine 7 received 3
// routine 13 received 10
// routine 11 received 1
// routine 9 received 5
// routine 2 received 19
// routine 6 received 2
// routine 16 received 8
// ...
// section: outputOutput:
// Output (order will vary):
// routine 0 received 0
// routine 1 received 18
// routine 3 received 7
// routine 7 received 3
// routine 13 received 10
// routine 11 received 1
// routine 9 received 5
// routine 2 received 19
// routine 6 received 2
// routine 16 received 8
// ...package main
import (
"fmt"
"time"
)
var N = 20
func main() {
// section: code
ch := make(chan int)
for i := 0; i < N; i++ {
go func(i int) {
for m := range ch {
fmt.Printf("routine %d received %d\n", i, m)
}
}(i)
}
for i := 0; i < N; i++ {
ch <- i
}
time.Sleep(time.Second)
// section: code
}
// section: output
// Output (order will vary):
// routine 0 received 0
// routine 1 received 18
// routine 3 received 7
// routine 7 received 3
// routine 13 received 10
// routine 11 received 1
// routine 9 received 5
// routine 2 received 19
// routine 6 received 2
// routine 16 received 8
// ...
// section: outputNotice each value is received by exactly one goroutine, but the distribution across goroutines is unpredictable.
Closing Channels
You can signal that there are no more values in a channel by closing it. Receivers can test to see if a channel is closed during a read.
package main
import "fmt"
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)
}
}package main
import "fmt"
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)
}
}Detecting Closed Channels On Read
This can be done, as with type detection and maps, by using the optional, boolean, second argument that comes with reading from a channel.
package main
import "fmt"
func main() {
c := make(chan int)
go count(c)
for {
i, ok := <-c
if !ok {
fmt.Println("closed channel")
return
}
fmt.Printf("read %d from channel\n", i)
}
}
func count(c chan int) {
for i := 0; i < 10; i++ {
c <- i
}
close(c)
}Zero Value On Closed Read
It is important to ignore the value returned on a closed read from a channel, as the runtime will explicitly set your values back to the zero values for the type defined in the channel:
package main
import "fmt"
type User struct {
ID int
Name string
}
func main() {
c := make(chan User, 1)
u := User{ID: 1, Name: "Rob"}
// Write the value into the channel
c <- u
// Read the value off the channel
u1 := <-c
fmt.Printf("read successful: %+v\n", u1)
// Close the channel
close(c)
// Read off the channel again...
u1 = <-c
fmt.Printf("read closed: %+v\n", u1)
var ok bool
// Proper way to read
if u1, ok = <-c; ok {
fmt.Printf("read successful: %+v\n", u1)
} else {
fmt.Println("attempted read of closed channel, ignore value returned")
}
}package main
import "fmt"
type User struct {
ID int
Name string
}
func main() {
c := make(chan User, 1)
u := User{ID: 1, Name: "Rob"}
// Write the value into the channel
c <- u
// Read the value off the channel
u1 := <-c
fmt.Printf("read successful: %+v\n", u1)
// Close the channel
close(c)
// Read off the channel again...
u1 = <-c
fmt.Printf("read closed: %+v\n", u1)
var ok bool
// Proper way to read
if u1, ok = <-c; ok {
fmt.Printf("read successful: %+v\n", u1)
} else {
fmt.Println("attempted read of closed channel, ignore value returned")
}
}Ranging Through Channels
You can use the range keyword to read channels. When
using range, it is a shortcut for using a for loop
with on conditionals.
func rangeLoop(messages chan string) {
// loop until the channel is closed
for m := range messages {
fmt.Println(m)
}
}
package main
import "fmt"
func main() {
// empty
}
// section: range
func rangeLoop(messages chan string) {
// loop until the channel is closed
for m := range messages {
fmt.Println(m)
}
}
// section: range
// section: for
func forLoop(messages chan string) {
// loop until the channel is closed
for {
m, ok := <-messages
if !ok {
// channel was closed
break
}
fmt.Println(m)
}
}
// section: forThe range keyword makes it very simple to drain a channel. Below is functionally equivalent code without using a range directive.
func forLoop(messages chan string) {
// loop until the channel is closed
for {
m, ok := <-messages
if !ok {
// channel was closed
break
}
fmt.Println(m)
}
}
package main
import "fmt"
func main() {
// empty
}
// section: range
func rangeLoop(messages chan string) {
// loop until the channel is closed
for m := range messages {
fmt.Println(m)
}
}
// section: range
// section: for
func forLoop(messages chan string) {
// loop until the channel is closed
for {
m, ok := <-messages
if !ok {
// channel was closed
break
}
fmt.Println(m)
}
}
// section: forReading From Closed Buffered Channels
Even if a buffered channel is closed, you can still read all the data written to it.
func main() {
c := make(chan int, 10)
// Write values into a channel
for i := 0; i < cap(c); i++ {
c <- i
}
// Close the channel
close(c)
// You can still read all the data in the channel
for i := range c {
fmt.Println(i)
}
// but you can no longer write to the channel as it's closed
c <- 11
}
package main
import (
"fmt"
)
// section: main
func main() {
c := make(chan int, 10)
// Write values into a channel
for i := 0; i < cap(c); i++ {
c <- i
}
// Close the channel
close(c)
// You can still read all the data in the channel
for i := range c {
fmt.Println(i)
}
// but you can no longer write to the channel as it's closed
c <- 11
}
// section: mainChannels As Signals
You can also use a channel as a signal only used to block a process.
func main() {
quit := make(chan struct{})
go monitor()
<-quit
}
func monitor() {
for {
log.Println("monitoring...")
time.Sleep(time.Second)
}
}
package main
import (
"log"
"time"
)
// section: main
func main() {
quit := make(chan struct{})
go monitor()
<-quit
}
func monitor() {
for {
log.Println("monitoring...")
time.Sleep(time.Second)
}
}
// section: mainThis program will block until you terminate the program.
Signal Vs. WaitGroup
For a simple program, you could use a channel in place of a sync.WaitGroup.
Given the following program with a WaitGroup:
func main() {
wg := &sync.WaitGroup{}
wg.Add(1)
go count(5, wg)
wg.Wait()
}
func count(i int, wg *sync.WaitGroup) {
defer wg.Done()
for j := 0; j < i; j++ {
fmt.Println("processing...", j)
}
}
package main
import (
"fmt"
"sync"
)
// section: code
func main() {
wg := &sync.WaitGroup{}
wg.Add(1)
go count(5, wg)
wg.Wait()
}
func count(i int, wg *sync.WaitGroup) {
defer wg.Done()
for j := 0; j < i; j++ {
fmt.Println("processing...", j)
}
}
// section: code
/*
// section: output
$ go run ./main.go
processing... 0
processing... 1
processing... 2
processing... 3
processing... 4
// section: output
*/It could be re-written with a channel:
func main() {
quit := make(chan struct{})
go count(5, quit)
<-quit
}
func count(i int, quit chan struct{}) {
defer close(quit)
for j := 0; j < i; j++ {
fmt.Println("processing...", j)
}
}
package main
import "fmt"
// section: code
func main() {
quit := make(chan struct{})
go count(5, quit)
<-quit
}
func count(i int, quit chan struct{}) {
defer close(quit)
for j := 0; j < i; j++ {
fmt.Println("processing...", j)
}
}
// section: code
/*
// section: output
$ go run ./main.go
processing... 0
processing... 1
processing... 2
processing... 3
processing... 4
// section: output
*/Output:
package main
import "fmt"
// section: code
func main() {
quit := make(chan struct{})
go count(5, quit)
<-quit
}
func count(i int, quit chan struct{}) {
defer close(quit)
for j := 0; j < i; j++ {
fmt.Println("processing...", j)
}
}
// section: code
/*
// section: output
$ go run ./main.go
processing... 0
processing... 1
processing... 2
processing... 3
processing... 4
// section: output
*/Channel Exercise (15 Min)
Create a program that does the following:
- has a buffered channel of type
stringwith a capacity of5 - add 5 messages to the channel
- have a function that ranges through the channel printing them out
- the function should close a
signalchannel to allow the program to exit - close the messages channel to end the goroutine previously launched
package main
import "fmt"
func process(messages chan string, quit chan struct{}) {
// Using the `range` operator, loop through messages
// until the channel is closed
for m := range messages {
_ = m // TODO: print the message
}
// TODO: close the `quit` channel
}
func main() {
// TODO: declare the messages channel of type string and capacity of 5
// TODO: declare a signal channel
// TODO: launch the `process` function in a goroutine
// declare 5 fruits in a []string
fruits := []string{
// TODO: add 5 fruits
}
// loop through the fruits and send them to the messages channel
for _, f := range fruits {
_ = f // TODO: send f to the messages channel
}
// TODO: close the messages channel
// TODO: wait for everything to finish (hint, block on the quit channel)
fmt.Println("done")
}package main
import "fmt"
func process(messages chan string, quit chan struct{}) {
// Using the `range` operator, loop through messages
// until the channel is closed
for m := range messages {
_ = m // TODO: print the message
}
// TODO: close the `quit` channel
}
func main() {
// TODO: declare the messages channel of type string and capacity of 5
// TODO: declare a signal channel
// TODO: launch the `process` function in a goroutine
// declare 5 fruits in a []string
fruits := []string{
// TODO: add 5 fruits
}
// loop through the fruits and send them to the messages channel
for _, f := range fruits {
_ = f // TODO: send f to the messages channel
}
// TODO: close the messages channel
// TODO: wait for everything to finish (hint, block on the quit channel)
fmt.Println("done")
}Channel Solution
package main
import (
"fmt"
)
func process(messages chan string, quit chan struct{}) {
// loop until the channel is closed
for m := range messages {
fmt.Println(m)
}
close(quit)
}
func main() {
messages := make(chan string, 5)
quit := make(chan struct{})
go process(messages, quit)
fruits := []string{"apple", "plum", "peach", "pear", "grape"}
for _, s := range fruits {
messages <- s
}
// closing the messages channel will end the for loop
// in the processes goroutine
close(messages)
<-quit
fmt.Println("done")
}package main
import (
"fmt"
)
func process(messages chan string, quit chan struct{}) {
// loop until the channel is closed
for m := range messages {
fmt.Println(m)
}
close(quit)
}
func main() {
messages := make(chan string, 5)
quit := make(chan struct{})
go process(messages, quit)
fruits := []string{"apple", "plum", "peach", "pear", "grape"}
for _, s := range fruits {
messages <- s
}
// closing the messages channel will end the for loop
// in the processes goroutine
close(messages)
<-quit
fmt.Println("done")
}Using Select
You can use a select statement to allow a goroutine to operate on multiple communication operations:
func main() {
c := make(chan string)
quit := make(chan struct{})
go func(messages []string) {
for _, s := range messages {
c <- s
}
close(quit)
}([]string{"hi", "bye"})
for {
select {
case message := <-c:
fmt.Println(message)
case <-quit:
fmt.Println("shutting down")
os.Exit(0)
}
}
}
package main
import (
"fmt"
"os"
)
// section: main
func main() {
c := make(chan string)
quit := make(chan struct{})
go func(messages []string) {
for _, s := range messages {
c <- s
}
close(quit)
}([]string{"hi", "bye"})
for {
select {
case message := <-c:
fmt.Println(message)
case <-quit:
fmt.Println("shutting down")
os.Exit(0)
}
}
}
// section: mainSelect With Default
If no operations inside a select statement can process, the select will block. To work around this problem, you can add a default statement to your select:
package main
import "time"
func main() {
tick := time.Tick(100 * time.Millisecond)
done := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
print("tick")
case <-done:
print(".done")
return
default:
print(".")
time.Sleep(10 * time.Millisecond)
}
}
}Break In Select
A common bug when using select inside a for loop is attempting to use break to exit the loop. According to the Go specification:
A “break” statement terminates execution of the innermost “for”, “switch”, or “select” statement.
This means break inside a select only breaks out of the select, not the surrounding for loop.
Break In Select: Common Mistake
The following code will run forever because the break statement only exits the select, not the for loop:
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
done := time.After(3 * time.Second)
for {
select {
case <-ticker.C:
fmt.Println("tick")
case <-done:
fmt.Println("done")
break // MISTAKE: this only breaks out of select, not the for loop!
}
}
// This line is never reached
fmt.Println("exiting")
}package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
done := time.After(3 * time.Second)
for {
select {
case <-ticker.C:
fmt.Println("tick")
case <-done:
fmt.Println("done")
break // MISTAKE: this only breaks out of select, not the for loop!
}
}
// This line is never reached
fmt.Println("exiting")
}Break In Select: The Fix
Wrap the for/select in an anonymous function. A return exits the function, which breaks the loop:
func main() {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
done := time.After(3 * time.Second)
func() {
for {
select {
case <-ticker.C:
fmt.Println("tick")
case <-done:
fmt.Println("done")
return // exits the anonymous function, breaking the loop
}
}
}()
fmt.Println("exiting")
}
package main
import (
"fmt"
"time"
)
// section: main
func main() {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
done := time.After(3 * time.Second)
func() {
for {
select {
case <-ticker.C:
fmt.Println("tick")
case <-done:
fmt.Println("done")
return // exits the anonymous function, breaking the loop
}
}
}()
fmt.Println("exiting")
}
// section: main
/*
// section: output
$ go run main.go
tick
tick
tick
tick
tick
tick
done
exiting
// section: output
*/Output:
package main
import (
"fmt"
"time"
)
// section: main
func main() {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
done := time.After(3 * time.Second)
func() {
for {
select {
case <-ticker.C:
fmt.Println("tick")
case <-done:
fmt.Println("done")
return // exits the anonymous function, breaking the loop
}
}
}()
fmt.Println("exiting")
}
// section: main
/*
// section: output
$ go run main.go
tick
tick
tick
tick
tick
tick
done
exiting
// section: output
*/Leaked Go Routines
A common bug that occurs when using select and channels as signals is a blocked (and therefor leaked) go routine.
Look at the following code:
package main
import (
"fmt"
"os"
"os/signal"
"time"
)
func main() {
go process()
// wait to exit based on user input
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
}
func process() {
// create a timeout.
timeout := time.NewTimer(1 * time.Second)
// this ensures we don't leak a go routine
defer timeout.Stop()
// create a channel to send our result
res := make(chan int)
// create another go routine to do some work
go func() {
time.Sleep(2 * time.Second)
res <- 1
fmt.Println("wrote to result channel")
}()
select {
case <-res:
fmt.Println("sub routine executed first")
case <-timeout.C:
fmt.Println("timeout executed first")
}
}
package main
import (
"fmt"
"os"
"os/signal"
"time"
)
func main() {
go process()
// wait to exit based on user input
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
}
// section: process
func process() {
// create a timeout.
timeout := time.NewTimer(1 * time.Second)
// this ensures we don't leak a go routine
defer timeout.Stop()
// create a channel to send our result
res := make(chan int)
// create another go routine to do some work
go func() {
time.Sleep(2 * time.Second)
res <- 1
fmt.Println("wrote to result channel")
}()
select {
case <-res:
fmt.Println("sub routine executed first")
case <-timeout.C:
fmt.Println("timeout executed first")
}
}
// section: processThe Problem
In the previous code, we see that we created a res channel in our process function to receive the output of our local anonymous function. We use this in combination with a timeout in our select statement.
The problem is that if the timeout occurs first, their is no process left to receive from the res channel. Because the res channel is unbuffered, there is no way for it to write.
func process() {
// create a timeout.
timeout := time.NewTimer(1 * time.Second)
// this ensures we don't leak a go routine
defer timeout.Stop()
// create a channel to send our result
res := make(chan int)
// create another go routine to do some work
go func() {
time.Sleep(2 * time.Second)
res <- 1 // <-- If timeout occurs first, this channel is blocked forever
fmt.Println("wrote to result channel")
}()
select {
case <-res:
fmt.Println("sub routine executed first")
case <-timeout.C:
fmt.Println("timeout executed first")
}
}
package main
import (
"fmt"
"os"
"os/signal"
"time"
)
func main() {
go process()
// wait to exit based on user input
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
}
// section: process
func process() {
// create a timeout.
timeout := time.NewTimer(1 * time.Second)
// this ensures we don't leak a go routine
defer timeout.Stop()
// create a channel to send our result
res := make(chan int)
// create another go routine to do some work
go func() {
time.Sleep(2 * time.Second)
res <- 1 // <-- If timeout occurs first, this channel is blocked forever
fmt.Println("wrote to result channel")
}()
select {
case <-res:
fmt.Println("sub routine executed first")
case <-timeout.C:
fmt.Println("timeout executed first")
}
}
// section: processThe Fix
Using a buffered channel will fix the bug:
func process() {
// create a timeout.
timeout := time.NewTimer(1 * time.Second)
// this ensures we don't leak a go routine
defer timeout.Stop()
// create a channel to send our result
res := make(chan int, 1)
// create another go routine to do some work
go func() {
time.Sleep(2 * time.Second)
res <- 1
fmt.Println("wrote to result channel")
}()
select {
case <-res:
fmt.Println("sub routine executed first")
case <-timeout.C:
fmt.Println("timeout executed first")
}
}
package main
import (
"fmt"
"os"
"os/signal"
"time"
)
func main() {
go process()
// wait to exit based on user input
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
}
// section: process
func process() {
// create a timeout.
timeout := time.NewTimer(1 * time.Second)
// this ensures we don't leak a go routine
defer timeout.Stop()
// create a channel to send our result
res := make(chan int, 1)
// create another go routine to do some work
go func() {
time.Sleep(2 * time.Second)
res <- 1
fmt.Println("wrote to result channel")
}()
select {
case <-res:
fmt.Println("sub routine executed first")
case <-timeout.C:
fmt.Println("timeout executed first")
}
}
// section: processCommon Goroutine Leak Patterns
Five Ways To Leak A Goroutine
Based on real bugs from production systems (CockroachDB, Kubernetes, etcd, gRPC):
- Early Return - Error handling orphans a sender
- Unbuffered Channel Worker - Parallel processing meets early exit
- No-Close Range - Worker pools that never end
- Context Timeout - When cancellation wins the race (covered above)
- Double Send - Missing return statement
These patterns all passed code review, testing, and shipped to production before being discovered.
Pattern: Unbuffered Channel Worker
A common pattern in parallel processing that leaks when you return early on error:
func processWorkItems(ws []workItem) ([]workResult, error) {
ch := make(chan result) // UNBUFFERED - problem!
for _, w := range ws {
go func(w workItem) {
res, err := processWorkItem(w)
ch <- result{res, err} // BLOCKS if early return happens
}(w)
}
var results []workResult
for i := 0; i < len(ws); i++ {
r := <-ch
if r.err != nil {
return nil, r.err // EARLY RETURN - remaining workers leak!
}
results = append(results, r.res)
}
return results, nil
}
package main
import (
"errors"
"fmt"
"time"
)
type workItem struct {
id int
data string
}
type workResult struct {
id int
result string
}
type result struct {
res workResult
err error
}
// section: process
func processWorkItems(ws []workItem) ([]workResult, error) {
ch := make(chan result) // UNBUFFERED - problem!
for _, w := range ws {
go func(w workItem) {
res, err := processWorkItem(w)
ch <- result{res, err} // BLOCKS if early return happens
}(w)
}
var results []workResult
for i := 0; i < len(ws); i++ {
r := <-ch
if r.err != nil {
return nil, r.err // EARLY RETURN - remaining workers leak!
}
results = append(results, r.res)
}
return results, nil
}
// section: process
func processWorkItem(w workItem) (workResult, error) {
time.Sleep(100 * time.Millisecond)
if w.id == 3 {
return workResult{}, errors.New("item 3 failed")
}
return workResult{id: w.id, result: fmt.Sprintf("processed %s", w.data)}, nil
}
func main() {
items := []workItem{
{id: 1, data: "a"},
{id: 2, data: "b"},
{id: 3, data: "c"}, // Will fail
{id: 4, data: "d"},
{id: 5, data: "e"},
}
results, err := processWorkItems(items)
if err != nil {
fmt.Println("Error:", err)
// Workers 4 and 5 are now leaked!
}
fmt.Println("Results:", results)
// Keep alive to observe leaked goroutines
time.Sleep(500 * time.Millisecond)
fmt.Println("Program ending - leaked goroutines are still blocked")
}Why It Leaks
When item 3 fails and we return the error:
- Workers for items 4 and 5 are still running
- They try to send on the unbuffered channel
- No one is receiving anymore
- They block forever
The Fix: Buffered Channel
Using a buffered channel allows all workers to complete their send:
func processWorkItems(ws []workItem) ([]workResult, error) {
ch := make(chan result, len(ws)) // BUFFERED - workers can always send
for _, w := range ws {
go func(w workItem) {
res, err := processWorkItem(w)
ch <- result{res, err} // Never blocks
}(w)
}
var results []workResult
for i := 0; i < len(ws); i++ {
r := <-ch
if r.err != nil {
// Workers complete even though we return early
return nil, r.err
}
results = append(results, r.res)
}
return results, nil
}
package main
import (
"errors"
"fmt"
"time"
)
type workItem struct {
id int
data string
}
type workResult struct {
id int
result string
}
type result struct {
res workResult
err error
}
// section: process
func processWorkItems(ws []workItem) ([]workResult, error) {
ch := make(chan result, len(ws)) // BUFFERED - workers can always send
for _, w := range ws {
go func(w workItem) {
res, err := processWorkItem(w)
ch <- result{res, err} // Never blocks
}(w)
}
var results []workResult
for i := 0; i < len(ws); i++ {
r := <-ch
if r.err != nil {
// Workers complete even though we return early
return nil, r.err
}
results = append(results, r.res)
}
return results, nil
}
// section: process
func processWorkItem(w workItem) (workResult, error) {
time.Sleep(100 * time.Millisecond)
if w.id == 3 {
return workResult{}, errors.New("item 3 failed")
}
return workResult{id: w.id, result: fmt.Sprintf("processed %s", w.data)}, nil
}
func main() {
items := []workItem{
{id: 1, data: "a"},
{id: 2, data: "b"},
{id: 3, data: "c"}, // Will fail
{id: 4, data: "d"},
{id: 5, data: "e"},
}
results, err := processWorkItems(items)
if err != nil {
fmt.Println("Error:", err)
}
fmt.Println("Results:", results)
time.Sleep(500 * time.Millisecond)
fmt.Println("All workers completed - no leaks")
}Pattern: No-Close Range
Worker pools using range that forget to close the channel:
func processWithWorkers(items []string, workers int) {
ch := make(chan string)
for i := 0; i < workers; i++ {
go func(id int) {
for item := range ch { // Blocks forever - ch never closed!
fmt.Printf("Worker %d processing: %s\n", id, item)
time.Sleep(50 * time.Millisecond)
}
fmt.Printf("Worker %d exiting\n", id) // NEVER PRINTED
}(i)
}
for _, item := range items {
ch <- item
}
// Returns without closing ch - workers stuck in range forever!
}
package main
import (
"fmt"
"time"
)
// section: problem
func processWithWorkers(items []string, workers int) {
ch := make(chan string)
for i := 0; i < workers; i++ {
go func(id int) {
for item := range ch { // Blocks forever - ch never closed!
fmt.Printf("Worker %d processing: %s\n", id, item)
time.Sleep(50 * time.Millisecond)
}
fmt.Printf("Worker %d exiting\n", id) // NEVER PRINTED
}(i)
}
for _, item := range items {
ch <- item
}
// Returns without closing ch - workers stuck in range forever!
}
// section: problem
func main() {
items := []string{"a", "b", "c", "d", "e"}
processWithWorkers(items, 3)
fmt.Println("Function returned - but workers are still blocked!")
time.Sleep(500 * time.Millisecond)
fmt.Println("Program ending - 3 leaked goroutines still waiting")
}Why It Leaks
The for item := range ch loop only exits when the channel is closed.
If you never close the channel:
- Workers block forever in the range loop
- Each worker becomes a permanent goroutine leak
- Memory grows until the process crashes
The Fix: Always Close
func processWithWorkers(items []string, workers int) {
ch := make(chan string)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for item := range ch {
fmt.Printf("Worker %d processing: %s\n", id, item)
time.Sleep(50 * time.Millisecond)
}
fmt.Printf("Worker %d exiting\n", id) // NOW PRINTED
}(i)
}
for _, item := range items {
ch <- item
}
close(ch) // Signal workers to exit
wg.Wait() // Wait for all workers to finish
}
package main
import (
"fmt"
"sync"
"time"
)
// section: fixed
func processWithWorkers(items []string, workers int) {
ch := make(chan string)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for item := range ch {
fmt.Printf("Worker %d processing: %s\n", id, item)
time.Sleep(50 * time.Millisecond)
}
fmt.Printf("Worker %d exiting\n", id) // NOW PRINTED
}(i)
}
for _, item := range items {
ch <- item
}
close(ch) // Signal workers to exit
wg.Wait() // Wait for all workers to finish
}
// section: fixed
func main() {
items := []string{"a", "b", "c", "d", "e"}
processWithWorkers(items, 3)
fmt.Println("All workers exited cleanly - no leaks")
}Key points:
- Always close channels when done sending
- Use
sync.WaitGroupto wait for workers - Consider
defer close(ch)to handle error paths
Pattern: Double Send
A missing return statement causes two sends on a channel:
func sendResult(ch chan string, err error) {
if err != nil {
ch <- "error occurred"
// Missing: return <- OOPS!
}
// Still executing here if err was not nil!
ch <- "success" // BLOCKS FOREVER if receiver already read
}
package main
import (
"errors"
"fmt"
"time"
)
// section: problem
func sendResult(ch chan string, err error) {
if err != nil {
ch <- "error occurred"
// Missing: return <- OOPS!
}
// Still executing here if err was not nil!
ch <- "success" // BLOCKS FOREVER if receiver already read
}
// section: problem
func main() {
ch := make(chan string, 1) // Only buffer for 1 message
go sendResult(ch, errors.New("something went wrong"))
result := <-ch
fmt.Println("Got:", result)
time.Sleep(500 * time.Millisecond)
fmt.Println("Goroutine leaked - blocked on second send forever")
}Why It Leaks
“One missing line. One missing return. Infinite goroutine leak.”
- If
err != nil, we send the error message - Without
return, we continue to the next line - The second send blocks if the receiver already read
- The goroutine is stuck forever
This often happens during refactoring when converting if/else to just if.
The Fix: Return After Send
func sendResult(ch chan string, err error) {
if err != nil {
ch <- "error occurred"
return // FIXED: explicit return after send
}
ch <- "success"
}
package main
import (
"errors"
"fmt"
"time"
)
// section: fixed
func sendResult(ch chan string, err error) {
if err != nil {
ch <- "error occurred"
return // FIXED: explicit return after send
}
ch <- "success"
}
// section: fixed
func main() {
ch := make(chan string, 1)
go sendResult(ch, errors.New("something went wrong"))
result := <-ch
fmt.Println("Got:", result)
time.Sleep(100 * time.Millisecond)
fmt.Println("Goroutine completed cleanly - no leak")
}Leak Pattern Summary
| Pattern | Cause | Fix |
|---|---|---|
| Early Return | Error path orphans senders | Buffered channel or drain |
| No-Close Range | Forgot to close channel | Always close(ch) when done |
| Double Send | Missing return statement | Explicit return after send |
| Context Timeout | Race with cancellation | Buffered channel |
Remember: All of these passed code review and testing before reaching production.
Knowing the patterns helps, but you don’t have to rely on code review alone. In tests, uber-go/goleak will fail any test that finishes with unexpected goroutines still running. Go 1.26 also adds a runtime goroutineleak pprof profile for catching leaks in production (experimental, behind GOEXPERIMENT=goroutineleakprofile, and expected to be generally available in Go 1.27). Both tools are covered in depth in the development workflow module.
Uni-Directional Channels
By default channels are bi-directional, meaning you can both send and receive data from the channel.
A common use for uni-directional channels is when you are passing a channel as an argument or receiving a channel as return value. This allows for control of the channel for the function/method and prevents outside callers from polluting the channel.
The standard library does this in the time package with methods like time.Tick.
Receive Only Channels
Here is an example of using a read only channel as an argument to a function:
package main
import "fmt"
func main() {
// make a full directional channel
c := make(chan string)
quit := make(chan struct{})
go receive(c, quit)
// Write into the channel
for _, s := range []string{"one", "two", "three"} {
c <- s
}
close(c)
// wait for quit channel to close
<-quit
}
// receive takes the first argument as a read only channel
func receive(c <-chan string, quit chan struct{}) {
// read from the channel
for s := range c {
fmt.Println(s)
}
// You can't write to this channel:
//c <- "foo"
// You also can't close a read only channel. This is to protect the sender from you also polluting this channel:
//close(c)
close(quit)
}package main
import "fmt"
func main() {
// make a full directional channel
c := make(chan string)
quit := make(chan struct{})
go receive(c, quit)
// Write into the channel
for _, s := range []string{"one", "two", "three"} {
c <- s
}
close(c)
// wait for quit channel to close
<-quit
}
// receive takes the first argument as a read only channel
func receive(c <-chan string, quit chan struct{}) {
// read from the channel
for s := range c {
fmt.Println(s)
}
// You can't write to this channel:
//c <- "foo"
// You also can't close a read only channel. This is to protect the sender from you also polluting this channel:
//close(c)
close(quit)
}Receive Only Channel Limitations
You can’t write to a receive only channel, that will result in a compile time error
receive.go:28:4: invalid operation: c <- "foo" (send to receive-only type <-chan string)
You can’t close a receive only channel, that will also result in a compile time error
/receive.go:31:7: invalid operation: close(c) (cannot close receive-only channel)
Send Only Channels
Here is an example of using a send only channel as an argument to a function:
package main
import "fmt"
func main() {
// make a full directional channel
c := make(chan string)
go send(c)
// read from the channel
for s := range c {
fmt.Println(s)
}
}
// send takes the first argument as a send only channel
func send(c chan<- string) {
// Write into the channel
for _, s := range []string{"one", "two", "three"} {
c <- s
}
// You can't read from this channel, that is a compile time error:
//s := <-c
// it is ok to close this channel
close(c)
}package main
import "fmt"
func main() {
// make a full directional channel
c := make(chan string)
go send(c)
// read from the channel
for s := range c {
fmt.Println(s)
}
}
// send takes the first argument as a send only channel
func send(c chan<- string) {
// Write into the channel
for _, s := range []string{"one", "two", "three"} {
c <- s
}
// You can't read from this channel, that is a compile time error:
//s := <-c
// it is ok to close this channel
close(c)
}Send Only Channel Limitations
You can’t read from a send only channel, that will result in a compile time error
send.go:25:7: invalid operation: <-c (receive from send-only type chan<- string)
Panics
When using channels, it is important to understand when a panic can occur.
Here are the four ways you can encounter a panic with channels:
package main
func main() {
// section: closed
c1 := make(chan int)
close(c1)
c1 <- 1 // panic: send on a closed channel
// section: closed
// section: double
c2 := make(chan int)
close(c2)
close(c2) // panic: closeing a channel that is already closed
// section: double
// section: send
var c3 chan int
c3 <- 1 // panic: send on a nil channel
// section: send
// section: read
var c4 chan bool
<-c4 // panic: read from a nil channel: all goroutines are asleep
// section: read
}package main
func main() {
// section: closed
c1 := make(chan int)
close(c1)
c1 <- 1 // panic: send on a closed channel
// section: closed
// section: double
c2 := make(chan int)
close(c2)
close(c2) // panic: closeing a channel that is already closed
// section: double
// section: send
var c3 chan int
c3 <- 1 // panic: send on a nil channel
// section: send
// section: read
var c4 chan bool
<-c4 // panic: read from a nil channel: all goroutines are asleep
// section: read
}package main
func main() {
// section: closed
c1 := make(chan int)
close(c1)
c1 <- 1 // panic: send on a closed channel
// section: closed
// section: double
c2 := make(chan int)
close(c2)
close(c2) // panic: closeing a channel that is already closed
// section: double
// section: send
var c3 chan int
c3 <- 1 // panic: send on a nil channel
// section: send
// section: read
var c4 chan bool
<-c4 // panic: read from a nil channel: all goroutines are asleep
// section: read
}package main
func main() {
// section: closed
c1 := make(chan int)
close(c1)
c1 <- 1 // panic: send on a closed channel
// section: closed
// section: double
c2 := make(chan int)
close(c2)
close(c2) // panic: closeing a channel that is already closed
// section: double
// section: send
var c3 chan int
c3 <- 1 // panic: send on a nil channel
// section: send
// section: read
var c4 chan bool
<-c4 // panic: read from a nil channel: all goroutines are asleep
// section: read
}Dealing With Back Pressure
There are times you may want to “skip” an operation due to back pressure in the system.
A common example would be to not log “info” type messages if it would result in slowing or blocking normal operation of your program.
You can use a buffered channel and a select with default to accomplish this.
package main
import (
"fmt"
"time"
)
func main() {
logChan := make(chan string, 5)
quit := make(chan struct{})
// Logger reading fro the logChan
go func() {
for msg := range logChan {
fmt.Println("delivered message: ", msg)
// create some back pressure
time.Sleep(10 * time.Millisecond)
}
close(quit)
}()
// simulate some load on the logger
go func() {
for i := 0; i < 20; i++ {
log(i, logChan)
}
// this will close the logger
close(logChan)
}()
<-quit
}
func log(i int, c chan string) {
select {
// write the message to the logger
case c <- fmt.Sprintf("this is log message %d", i):
default:
fmt.Printf("log channel buffer full: dropped message due to back pressure %d\n", i)
}
}package main
import (
"fmt"
"time"
)
func main() {
logChan := make(chan string, 5)
quit := make(chan struct{})
// Logger reading fro the logChan
go func() {
for msg := range logChan {
fmt.Println("delivered message: ", msg)
// create some back pressure
time.Sleep(10 * time.Millisecond)
}
close(quit)
}()
// simulate some load on the logger
go func() {
for i := 0; i < 20; i++ {
log(i, logChan)
}
// this will close the logger
close(logChan)
}()
<-quit
}
func log(i int, c chan string) {
select {
// write the message to the logger
case c <- fmt.Sprintf("this is log message %d", i):
default:
fmt.Printf("log channel buffer full: dropped message due to back pressure %d\n", i)
}
}Code Walkthrough (Optional)
Taking the example from the previous chapter to retrieve a list of websites, let’s re-write it using channels instead:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
args := []string{"http://youtube.com", "http://en.wikipedia.org", "http://facebook.com"}
now := time.Now()
for _, a := range args {
get(a)
}
fmt.Printf("It took %s for the entire process to finish\n", time.Since(now))
}
func get(s string) {
now := time.Now()
_, err := http.Get(s)
if err != nil {
log.Printf("failed to retrieve %s: %s\n", s, err)
return
}
log.Printf("Retrieved site %s in %s\n", s, time.Since(now))
}
// section: code
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
args := []string{"http://youtube.com", "http://en.wikipedia.org", "http://facebook.com"}
now := time.Now()
for _, a := range args {
get(a)
}
fmt.Printf("It took %s for the entire process to finish\n", time.Since(now))
}
func get(s string) {
now := time.Now()
_, err := http.Get(s)
if err != nil {
log.Printf("failed to retrieve %s: %s\n", s, err)
return
}
log.Printf("Retrieved site %s in %s\n", s, time.Since(now))
}
// section: code
/*
// section: output
2020/02/04 06:56:32 Retrieved site http://youtube.com in 366.257249ms
2020/02/04 06:56:32 Retrieved site http://en.wikipedia.org in 120.45097ms
2020/02/04 06:56:33 Retrieved site http://facebook.com in 371.748335ms
It took 858.755882ms for the entire process to finish
// section: output
*/Let's Add Channels
To give this basic concurrency, let’s rewrite the solution with channels:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// create a custom type to receive our responses
type resp struct {
site string
duration time.Duration
err error
}
func main() {
args := []string{"http://youtube.com", "http://en.wikipedia.org", "http://facebook.com"}
responses := make(chan resp, len(args))
now := time.Now()
for _, a := range args {
go get(a, responses)
}
// read back our responses
for i := 0; i < cap(responses); i++ {
r := <-responses
if r.err != nil {
log.Printf("failed to retrieve %s: %s\n", r.site, r.err)
continue
}
log.Printf("Retrieved site %s in %s\n", r.site, r.duration)
}
fmt.Printf("It took %s for the entire process to finish\n", time.Since(now))
}
func get(s string, responses chan<- resp) {
now := time.Now()
_, err := http.Get(s)
if err != nil {
responses <- resp{site: s, err: err}
return
}
responses <- resp{site: s, duration: time.Since(now)}
}
// section: code
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// create a custom type to receive our responses
type resp struct {
site string
duration time.Duration
err error
}
func main() {
args := []string{"http://youtube.com", "http://en.wikipedia.org", "http://facebook.com"}
responses := make(chan resp, len(args))
now := time.Now()
for _, a := range args {
go get(a, responses)
}
// read back our responses
for i := 0; i < cap(responses); i++ {
r := <-responses
if r.err != nil {
log.Printf("failed to retrieve %s: %s\n", r.site, r.err)
continue
}
log.Printf("Retrieved site %s in %s\n", r.site, r.duration)
}
fmt.Printf("It took %s for the entire process to finish\n", time.Since(now))
}
func get(s string, responses chan<- resp) {
now := time.Now()
_, err := http.Get(s)
if err != nil {
responses <- resp{site: s, err: err}
return
}
responses <- resp{site: s, duration: time.Since(now)}
}
// section: codeBetter With Channels
With the new code, we are now able to communicate the errors and responses back upstream to process were desired. However, we still need to address the following problems:
- Need to be able to scale and process millions of sites
- Need to set a reasonable timeout for each site retrieval
Addressing Timeout
To address the timeout, we can use time.NewTimer and another channel. We only need to change the get function to address the timeout.
func get(s string, responses chan<- resp) {
now := time.Now()
// pick a magic timeout duration
td := 2 * time.Second
// create a timeout.
timeout := time.NewTimer(td)
// this ensures we don't leak a go routine
defer timeout.Stop()
// create a channel to to receive the response on locally
// make sure it's buffered so you don't leak a goroutine
work := make(chan resp, 1)
// retrieve the site in a goroutine to avoid blocking
go func() {
// retrieve the website
_, err := http.Get(s)
work <- resp{site: s, duration: time.Since(now), err: err}
}()
select {
case r := <-work:
// retrieve the value from our local function and pass it through to our send channel
responses <- r
case <-timeout.C:
responses <- resp{site: s, err: fmt.Errorf("timed out after %s", td)}
}
}
// section: code
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// create a custom type to receive our responses
type resp struct {
site string
duration time.Duration
err error
}
func main() {
args := []string{"http://youtube.com", "http://en.wikipedia.org", "http://facebook.com"}
responses := make(chan resp, len(args))
now := time.Now()
for _, a := range args {
go get(a, responses)
}
// read back our responses
for i := 0; i < cap(responses); i++ {
r := <-responses
if r.err != nil {
log.Printf("failed to retrieve %s: %s\n", r.site, r.err)
continue
}
log.Printf("Retrieved site %s in %s\n", r.site, r.duration)
}
fmt.Printf("It took %s for the entire process to finish\n", time.Since(now))
}
// section: get
func get(s string, responses chan<- resp) {
now := time.Now()
// pick a magic timeout duration
td := 2 * time.Second
// create a timeout.
timeout := time.NewTimer(td)
// this ensures we don't leak a go routine
defer timeout.Stop()
// create a channel to to receive the response on locally
// make sure it's buffered so you don't leak a goroutine
work := make(chan resp, 1)
// retrieve the site in a goroutine to avoid blocking
go func() {
// retrieve the website
_, err := http.Get(s)
work <- resp{site: s, duration: time.Since(now), err: err}
}()
select {
case r := <-work:
// retrieve the value from our local function and pass it through to our send channel
responses <- r
case <-timeout.C:
responses <- resp{site: s, err: fmt.Errorf("timed out after %s", td)}
}
}
// section: get
// section: codeWe purposefully used our own timeout to show how we can control our operation. In production, you would set the timeout for the http client as even though we exited our goroutine, the client is still open in the background using up resources.
Scaling
Finally, we need to make this scale to millions of records. We can do this by using a buffered channel and control how large it is, thus throttling how many resources are in play at any given time
We can run the following code and pass in all the sites with the following command:
go run main.go "$(< sites.txt)"
package main
import (
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
)
// create a custom type to receive our responses
type resp struct {
site string
duration time.Duration
err error
}
func main() {
// Load sites from a file
args := os.Args[1:]
// Args might be a single string with line returns to delimit sites, break them apart
if len(args) == 1 {
args = strings.Split(args[0], "\n")
}
// Create a channel to send all sites to
// We'll create it buffered so that we throttle how much many go routines will be processing at any given time
sites := make(chan string, 30)
// create an unbuffered channel to receive on
// because we don't have contention or latency on the end of our
// process, there is no need to buffer this channel
responses := make(chan resp)
now := time.Now()
// launch the same number of go routines that we have a buffer size for
for i := 0; i < cap(sites); i++ {
go get(sites, responses)
}
// feed all of our data into the send channel
// this channel will block when it's full, and unblock
// when space becomes available
// We will do this in a goroutine so we don't block execution
go func() {
for _, s := range args {
sites <- s
}
// ensure that any open go routines close down
close(sites)
}()
// read back our responses
for i := 0; i < len(args); i++ {
r := <-responses
if r.err != nil {
log.Printf("failed to retrieve %s: %s\n", r.site, r.err)
continue
}
log.Printf("Retrieved site %s in %s\n", r.site, r.duration)
}
fmt.Printf("It took %s for the entire process to finish\n", time.Since(now))
}
func get(sites <-chan string, responses chan<- resp) {
// range through the receiving channel. When this channel is closed
// all go routines will terminate
for s := range sites {
now := time.Now()
// pick a magic timeout duration
td := 2 * time.Second
// create a timeout.
timeout := time.NewTimer(td)
// this ensures we don't leak a go routine
defer timeout.Stop()
// create a channel to to receive the response on locally
// make sure it's buffered so you don't leak a goroutine
work := make(chan resp, 1)
// retrieve the site in a goroutine to avoid blocking
go func() {
// retrieve the website
_, err := http.Get(s)
work <- resp{site: s, duration: time.Since(now), err: err}
}()
select {
case r := <-work:
// retrieve the value from our local function and pass it through to our send channel
responses <- r
case <-timeout.C:
responses <- resp{site: s, err: fmt.Errorf("timed out after %s", td)}
}
}
}
// section: code
package main
import (
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
)
// create a custom type to receive our responses
type resp struct {
site string
duration time.Duration
err error
}
func main() {
// Load sites from a file
args := os.Args[1:]
// Args might be a single string with line returns to delimit sites, break them apart
if len(args) == 1 {
args = strings.Split(args[0], "\n")
}
// Create a channel to send all sites to
// We'll create it buffered so that we throttle how much many go routines will be processing at any given time
sites := make(chan string, 30)
// create an unbuffered channel to receive on
// because we don't have contention or latency on the end of our
// process, there is no need to buffer this channel
responses := make(chan resp)
now := time.Now()
// launch the same number of go routines that we have a buffer size for
for i := 0; i < cap(sites); i++ {
go get(sites, responses)
}
// feed all of our data into the send channel
// this channel will block when it's full, and unblock
// when space becomes available
// We will do this in a goroutine so we don't block execution
go func() {
for _, s := range args {
sites <- s
}
// ensure that any open go routines close down
close(sites)
}()
// read back our responses
for i := 0; i < len(args); i++ {
r := <-responses
if r.err != nil {
log.Printf("failed to retrieve %s: %s\n", r.site, r.err)
continue
}
log.Printf("Retrieved site %s in %s\n", r.site, r.duration)
}
fmt.Printf("It took %s for the entire process to finish\n", time.Since(now))
}
func get(sites <-chan string, responses chan<- resp) {
// range through the receiving channel. When this channel is closed
// all go routines will terminate
for s := range sites {
now := time.Now()
// pick a magic timeout duration
td := 2 * time.Second
// create a timeout.
timeout := time.NewTimer(td)
// this ensures we don't leak a go routine
defer timeout.Stop()
// create a channel to to receive the response on locally
// make sure it's buffered so you don't leak a goroutine
work := make(chan resp, 1)
// retrieve the site in a goroutine to avoid blocking
go func() {
// retrieve the website
_, err := http.Get(s)
work <- resp{site: s, duration: time.Since(now), err: err}
}()
select {
case r := <-work:
// retrieve the value from our local function and pass it through to our send channel
responses <- r
case <-timeout.C:
responses <- resp{site: s, err: fmt.Errorf("timed out after %s", td)}
}
}
}
// section: codeHere is a sample of the output of all 100 sites:
2019/10/09 19:04:46 Retrieved site http://google.com in 271.050112ms
2019/10/09 19:04:47 Retrieved site http://nytimes.com in 475.879293ms
2019/10/09 19:04:47 Retrieved site http://forbes.com in 490.292424ms
2019/10/09 19:04:47 Retrieved site http://play.google.com in 495.126496ms
2019/10/09 19:04:47 Retrieved site http://apple.com in 500.157454ms2019/10/09 19:04:46 Retrieved site http://google.com in 271.050112ms
2019/10/09 19:04:47 Retrieved site http://nytimes.com in 475.879293ms
2019/10/09 19:04:47 Retrieved site http://forbes.com in 490.292424ms
2019/10/09 19:04:47 Retrieved site http://play.google.com in 495.126496ms
2019/10/09 19:04:47 Retrieved site http://apple.com in 500.157454ms
2019/10/09 19:04:47 Retrieved site http://target.com in 508.025298ms
2019/10/09 19:04:47 Retrieved site http://cnn.com in 522.319273ms
2019/10/09 19:04:47 Retrieved site http://quora.com in 534.739482ms
2019/10/09 19:04:47 Retrieved site http://espn.com in 570.186683ms
2019/10/09 19:04:47 Retrieved site http://twitter.com in 600.851295ms
2019/10/09 19:04:47 Retrieved site http://homedepot.com in 657.465788ms
2019/10/09 19:04:47 Retrieved site http://reddit.com in 749.390509ms
2019/10/09 19:04:47 Retrieved site http://indeed.com in 776.994401ms
2019/10/09 19:04:47 Retrieved site http://en.wikipedia.org in 780.251275ms
2019/10/09 19:04:47 Retrieved site http://linkedin.com in 790.79678ms
2019/10/09 19:04:47 Retrieved site http://ebay.com in 818.634474ms
2019/10/09 19:04:47 Retrieved site http://imdb.com in 893.051065ms
2019/10/09 19:04:47 Retrieved site http://walmart.com in 905.936567ms
2019/10/09 19:04:47 Retrieved site http://etsy.com in 931.219684ms
2019/10/09 19:04:47 Retrieved site http://youtube.com in 961.723023ms
2019/10/09 19:04:47 Retrieved site http://microsoft.com in 1.016002865s
2019/10/09 19:04:47 Retrieved site http://quizlet.com in 578.124315ms
2019/10/09 19:04:47 Retrieved site http://facebook.com in 1.278852068s
2019/10/09 19:04:47 Retrieved site http://webmd.com in 1.326975681s
2019/10/09 19:04:48 Retrieved site http://instagram.com in 1.403641652s
2019/10/09 19:04:48 Retrieved site http://netflix.com in 1.416308344s
2019/10/09 19:04:48 Retrieved site http://pinterest.com in 1.443917591s
2019/10/09 19:04:48 Retrieved site http://merriam-webster.com in 1.465617315s
2019/10/09 19:04:48 Retrieved site http://urbandictionary.com in 886.63105ms
2019/10/09 19:04:48 Retrieved site http://amazon.com in 1.563188195s
2019/10/09 19:04:48 Retrieved site http://rottentomatoes.com in 737.523189ms
2019/10/09 19:04:48 Retrieved site http://weather.com in 1.1133144s
2019/10/09 19:04:48 Retrieved site http://lowes.com in 743.009921ms
2019/10/09 19:04:48 Retrieved site http://mapquest.com in 1.447452857s
2019/10/09 19:04:48 Retrieved site http://msn.com in 828.323858ms
2019/10/09 19:04:48 Retrieved site http://dictionary.com in 727.93522ms
2019/10/09 19:04:48 Retrieved site http://usatoday.com in 946.987655ms
2019/10/09 19:04:48 Retrieved site http://businessinsider.com in 714.465284ms
2019/10/09 19:04:48 Retrieved site http://usnews.com in 556.635943ms
2019/10/09 19:04:48 Retrieved site http://finance.yahoo.com in 438.779929ms
2019/10/09 19:04:48 Retrieved site http://glassdoor.com in 1.032517869s
2019/10/09 19:04:48 Retrieved site http://allrecipes.com in 1.445943869s
2019/10/09 19:04:48 Retrieved site http://tripadvisor.com in 1.986160219s
2019/10/09 19:04:48 Retrieved site http://genius.com in 1.210331089s
2019/10/09 19:04:48 failed to retrieve http://craigslist.org: timed out after 2s
2019/10/09 19:04:48 Retrieved site http://zillow.com in 1.257154103s
2019/10/09 19:04:48 Retrieved site http://aol.com in 1.277668891s
2019/10/09 19:04:48 Retrieved site http://britannica.com in 660.832561ms
2019/10/09 19:04:48 Retrieved site http://foxnews.com in 1.585741651s
2019/10/09 19:04:48 Retrieved site http://go.com in 380.51193ms
2019/10/09 19:04:48 Retrieved site http://bestbuy.com in 1.567918995s
2019/10/09 19:04:48 Retrieved site http://yahoo.com in 1.71232326s
2019/10/09 19:04:48 Retrieved site http://mail.yahoo.com in 256.099184ms
2019/10/09 19:04:48 Retrieved site http://nih.gov in 1.784290781s
2019/10/09 19:04:48 Retrieved site http://healthline.com in 1.821099175s
2019/10/09 19:04:48 Retrieved site http://mayoclinic.org in 1.600111747s
2019/10/09 19:04:49 Retrieved site http://yellowpages.com in 869.179702ms
2019/10/09 19:04:49 Retrieved site http://gamepedia.com in 1.971832542s
2019/10/09 19:04:49 Retrieved site http://ign.com in 564.195881ms
2019/10/09 19:04:49 Retrieved site http://usps.com in 1.085420887s
2019/10/09 19:04:49 Retrieved site http://live.com in 813.39309ms
2019/10/09 19:04:49 Retrieved site http://retailmenot.com in 878.68093ms
2019/10/09 19:04:49 Retrieved site http://login.yahoo.com in 847.815834ms
2019/10/09 19:04:49 Retrieved site http://irs.gov in 1.101303547s
2019/10/09 19:04:49 Retrieved site http://chase.com in 984.761333ms
2019/10/09 19:04:49 Retrieved site http://medicalnewstoday.com in 1.346793151s
2019/10/09 19:04:49 Retrieved site http://cnet.com in 832.786537ms
2019/10/09 19:04:49 Retrieved site http://steamcommunity.com in 924.356448ms
2019/10/09 19:04:49 Retrieved site http://cbssports.com in 739.043697ms
2019/10/09 19:04:49 Retrieved site http://xfinity.com in 984.038122ms
2019/10/09 19:04:49 Retrieved site http://wikihow.com in 958.889753ms
2019/10/09 19:04:49 failed to retrieve http://macys.com: Get https://www.macys.com/: 302 response missing Location header
2019/10/09 19:04:49 Retrieved site http://twitch.tv in 754.990644ms
2019/10/09 19:04:49 Retrieved site http://expedia.com in 911.226691ms
2019/10/09 19:04:49 Retrieved site http://wiktionary.org in 1.091557509s
2019/10/09 19:04:49 Retrieved site http://wellsfargo.com in 1.023806832s
2019/10/09 19:04:49 Retrieved site http://steampowered.com in 1.305437019s
2019/10/09 19:04:49 Retrieved site http://bankofamerica.com in 1.222589082s
2019/10/09 19:04:49 Retrieved site http://office.com in 720.612609ms
2019/10/09 19:04:49 Retrieved site http://cnbc.com in 1.289999147s
2019/10/09 19:04:50 Retrieved site http://khanacademy.org in 1.118158166s
2019/10/09 19:04:50 Retrieved site http://mlb.com in 745.359047ms
2019/10/09 19:04:50 Retrieved site http://spotify.com in 970.132133ms
2019/10/09 19:04:50 failed to retrieve http://washingtonpost.com: timed out after 2s
2019/10/09 19:04:50 Retrieved site http://bbc.com in 716.820144ms
2019/10/09 19:04:50 Retrieved site http://att.com in 1.016356966s
2019/10/09 19:04:50 Retrieved site http://ufl.edu in 850.984022ms
2019/10/09 19:04:50 Retrieved site http://wayfair.com in 1.852581192s
2019/10/09 19:04:50 Retrieved site http://realtor.com in 1.088298352s
2019/10/09 19:04:50 Retrieved site http://nfl.com in 1.202048875s
2019/10/09 19:04:50 Retrieved site http://foodnetwork.com in 1.032741807s
2019/10/09 19:04:50 failed to retrieve http://accuweather.com: timed out after 2s
2019/10/09 19:04:50 Retrieved site http://ca.gov in 1.185209423s
2019/10/09 19:04:50 Retrieved site http://paypal.com in 1.408624689s
2019/10/09 19:04:50 Retrieved site http://npr.org in 987.585514ms
2019/10/09 19:04:50 Retrieved site http://goodreads.com in 1.254808047s
2019/10/09 19:04:50 Retrieved site http://theguardian.com in 1.499847207s
2019/10/09 19:04:50 Retrieved site http://wowhead.com in 1.154202684s
2019/10/09 19:04:50 Retrieved site http://groupon.com in 1.85877198s
2019/10/09 19:04:51 failed to retrieve http://apartments.com: timed out after 2s
It took 4.753549602s for the entire process to finish
2019/10/09 19:04:50 Retrieved site http://npr.org in 987.585514ms
2019/10/09 19:04:50 Retrieved site http://goodreads.com in 1.254808047s
2019/10/09 19:04:50 Retrieved site http://theguardian.com in 1.499847207s
2019/10/09 19:04:50 Retrieved site http://wowhead.com in 1.154202684s
2019/10/09 19:04:50 Retrieved site http://groupon.com in 1.85877198s
2019/10/09 19:04:51 failed to retrieve http://apartments.com: timed out after 2s
It took 4.753549602s for the entire process to finish2019/10/09 19:04:46 Retrieved site http://google.com in 271.050112ms
2019/10/09 19:04:47 Retrieved site http://nytimes.com in 475.879293ms
2019/10/09 19:04:47 Retrieved site http://forbes.com in 490.292424ms
2019/10/09 19:04:47 Retrieved site http://play.google.com in 495.126496ms
2019/10/09 19:04:47 Retrieved site http://apple.com in 500.157454ms
2019/10/09 19:04:47 Retrieved site http://target.com in 508.025298ms
2019/10/09 19:04:47 Retrieved site http://cnn.com in 522.319273ms
2019/10/09 19:04:47 Retrieved site http://quora.com in 534.739482ms
2019/10/09 19:04:47 Retrieved site http://espn.com in 570.186683ms
2019/10/09 19:04:47 Retrieved site http://twitter.com in 600.851295ms
2019/10/09 19:04:47 Retrieved site http://homedepot.com in 657.465788ms
2019/10/09 19:04:47 Retrieved site http://reddit.com in 749.390509ms
2019/10/09 19:04:47 Retrieved site http://indeed.com in 776.994401ms
2019/10/09 19:04:47 Retrieved site http://en.wikipedia.org in 780.251275ms
2019/10/09 19:04:47 Retrieved site http://linkedin.com in 790.79678ms
2019/10/09 19:04:47 Retrieved site http://ebay.com in 818.634474ms
2019/10/09 19:04:47 Retrieved site http://imdb.com in 893.051065ms
2019/10/09 19:04:47 Retrieved site http://walmart.com in 905.936567ms
2019/10/09 19:04:47 Retrieved site http://etsy.com in 931.219684ms
2019/10/09 19:04:47 Retrieved site http://youtube.com in 961.723023ms
2019/10/09 19:04:47 Retrieved site http://microsoft.com in 1.016002865s
2019/10/09 19:04:47 Retrieved site http://quizlet.com in 578.124315ms
2019/10/09 19:04:47 Retrieved site http://facebook.com in 1.278852068s
2019/10/09 19:04:47 Retrieved site http://webmd.com in 1.326975681s
2019/10/09 19:04:48 Retrieved site http://instagram.com in 1.403641652s
2019/10/09 19:04:48 Retrieved site http://netflix.com in 1.416308344s
2019/10/09 19:04:48 Retrieved site http://pinterest.com in 1.443917591s
2019/10/09 19:04:48 Retrieved site http://merriam-webster.com in 1.465617315s
2019/10/09 19:04:48 Retrieved site http://urbandictionary.com in 886.63105ms
2019/10/09 19:04:48 Retrieved site http://amazon.com in 1.563188195s
2019/10/09 19:04:48 Retrieved site http://rottentomatoes.com in 737.523189ms
2019/10/09 19:04:48 Retrieved site http://weather.com in 1.1133144s
2019/10/09 19:04:48 Retrieved site http://lowes.com in 743.009921ms
2019/10/09 19:04:48 Retrieved site http://mapquest.com in 1.447452857s
2019/10/09 19:04:48 Retrieved site http://msn.com in 828.323858ms
2019/10/09 19:04:48 Retrieved site http://dictionary.com in 727.93522ms
2019/10/09 19:04:48 Retrieved site http://usatoday.com in 946.987655ms
2019/10/09 19:04:48 Retrieved site http://businessinsider.com in 714.465284ms
2019/10/09 19:04:48 Retrieved site http://usnews.com in 556.635943ms
2019/10/09 19:04:48 Retrieved site http://finance.yahoo.com in 438.779929ms
2019/10/09 19:04:48 Retrieved site http://glassdoor.com in 1.032517869s
2019/10/09 19:04:48 Retrieved site http://allrecipes.com in 1.445943869s
2019/10/09 19:04:48 Retrieved site http://tripadvisor.com in 1.986160219s
2019/10/09 19:04:48 Retrieved site http://genius.com in 1.210331089s
2019/10/09 19:04:48 failed to retrieve http://craigslist.org: timed out after 2s
2019/10/09 19:04:48 Retrieved site http://zillow.com in 1.257154103s
2019/10/09 19:04:48 Retrieved site http://aol.com in 1.277668891s
2019/10/09 19:04:48 Retrieved site http://britannica.com in 660.832561ms
2019/10/09 19:04:48 Retrieved site http://foxnews.com in 1.585741651s
2019/10/09 19:04:48 Retrieved site http://go.com in 380.51193ms
2019/10/09 19:04:48 Retrieved site http://bestbuy.com in 1.567918995s
2019/10/09 19:04:48 Retrieved site http://yahoo.com in 1.71232326s
2019/10/09 19:04:48 Retrieved site http://mail.yahoo.com in 256.099184ms
2019/10/09 19:04:48 Retrieved site http://nih.gov in 1.784290781s
2019/10/09 19:04:48 Retrieved site http://healthline.com in 1.821099175s
2019/10/09 19:04:48 Retrieved site http://mayoclinic.org in 1.600111747s
2019/10/09 19:04:49 Retrieved site http://yellowpages.com in 869.179702ms
2019/10/09 19:04:49 Retrieved site http://gamepedia.com in 1.971832542s
2019/10/09 19:04:49 Retrieved site http://ign.com in 564.195881ms
2019/10/09 19:04:49 Retrieved site http://usps.com in 1.085420887s
2019/10/09 19:04:49 Retrieved site http://live.com in 813.39309ms
2019/10/09 19:04:49 Retrieved site http://retailmenot.com in 878.68093ms
2019/10/09 19:04:49 Retrieved site http://login.yahoo.com in 847.815834ms
2019/10/09 19:04:49 Retrieved site http://irs.gov in 1.101303547s
2019/10/09 19:04:49 Retrieved site http://chase.com in 984.761333ms
2019/10/09 19:04:49 Retrieved site http://medicalnewstoday.com in 1.346793151s
2019/10/09 19:04:49 Retrieved site http://cnet.com in 832.786537ms
2019/10/09 19:04:49 Retrieved site http://steamcommunity.com in 924.356448ms
2019/10/09 19:04:49 Retrieved site http://cbssports.com in 739.043697ms
2019/10/09 19:04:49 Retrieved site http://xfinity.com in 984.038122ms
2019/10/09 19:04:49 Retrieved site http://wikihow.com in 958.889753ms
2019/10/09 19:04:49 failed to retrieve http://macys.com: Get https://www.macys.com/: 302 response missing Location header
2019/10/09 19:04:49 Retrieved site http://twitch.tv in 754.990644ms
2019/10/09 19:04:49 Retrieved site http://expedia.com in 911.226691ms
2019/10/09 19:04:49 Retrieved site http://wiktionary.org in 1.091557509s
2019/10/09 19:04:49 Retrieved site http://wellsfargo.com in 1.023806832s
2019/10/09 19:04:49 Retrieved site http://steampowered.com in 1.305437019s
2019/10/09 19:04:49 Retrieved site http://bankofamerica.com in 1.222589082s
2019/10/09 19:04:49 Retrieved site http://office.com in 720.612609ms
2019/10/09 19:04:49 Retrieved site http://cnbc.com in 1.289999147s
2019/10/09 19:04:50 Retrieved site http://khanacademy.org in 1.118158166s
2019/10/09 19:04:50 Retrieved site http://mlb.com in 745.359047ms
2019/10/09 19:04:50 Retrieved site http://spotify.com in 970.132133ms
2019/10/09 19:04:50 failed to retrieve http://washingtonpost.com: timed out after 2s
2019/10/09 19:04:50 Retrieved site http://bbc.com in 716.820144ms
2019/10/09 19:04:50 Retrieved site http://att.com in 1.016356966s
2019/10/09 19:04:50 Retrieved site http://ufl.edu in 850.984022ms
2019/10/09 19:04:50 Retrieved site http://wayfair.com in 1.852581192s
2019/10/09 19:04:50 Retrieved site http://realtor.com in 1.088298352s
2019/10/09 19:04:50 Retrieved site http://nfl.com in 1.202048875s
2019/10/09 19:04:50 Retrieved site http://foodnetwork.com in 1.032741807s
2019/10/09 19:04:50 failed to retrieve http://accuweather.com: timed out after 2s
2019/10/09 19:04:50 Retrieved site http://ca.gov in 1.185209423s
2019/10/09 19:04:50 Retrieved site http://paypal.com in 1.408624689s
2019/10/09 19:04:50 Retrieved site http://npr.org in 987.585514ms
2019/10/09 19:04:50 Retrieved site http://goodreads.com in 1.254808047s
2019/10/09 19:04:50 Retrieved site http://theguardian.com in 1.499847207s
2019/10/09 19:04:50 Retrieved site http://wowhead.com in 1.154202684s
2019/10/09 19:04:50 Retrieved site http://groupon.com in 1.85877198s
2019/10/09 19:04:51 failed to retrieve http://apartments.com: timed out after 2s
It took 4.753549602s for the entire process to finishUnderstanding What We Just Built
The buffered channel pattern we used here is actually implementing a semaphore pattern!
sites := make(chan string, 30) // Buffer size throttles to 30 concurrent workers
for i := 0; i < cap(sites); i++ {
go get(sites, responses)
}
While this works great for worker pools, semaphores are more explicit when your primary goal is throttling concurrency rather than building a worker pool.
We’ll look at this throttle pattern on its own next. When you want explicit throttling without channels, the sync package module covers golang.org/x/sync/semaphore.
A Buffered Channel As A Semaphore
Here’s that throttle pattern on its own, without the rest of the fetcher. Send an empty struct into the channel to claim a slot before starting work, and receive from it to release the slot when you’re done. The buffer size is the concurrency limit:
func main() {
maxConcurrent := 3
// Create semaphore with max concurrency
sem := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// Acquire slot (blocks if full)
sem <- struct{}{}
fmt.Printf("worker %d running\n", id)
time.Sleep(100 * time.Millisecond)
// Release slot
<-sem
}(i)
}
wg.Wait()
fmt.Println("all workers done")
}
package main
import (
"fmt"
"sync"
"time"
)
// section: semaphore
func main() {
maxConcurrent := 3
// Create semaphore with max concurrency
sem := make(chan struct{}, maxConcurrent)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// Acquire slot (blocks if full)
sem <- struct{}{}
fmt.Printf("worker %d running\n", id)
time.Sleep(100 * time.Millisecond)
// Release slot
<-sem
}(i)
}
wg.Wait()
fmt.Println("all workers done")
}
// section: semaphore
// section: output
/*
worker 2 running
worker 0 running
worker 1 running
worker 4 running
worker 3 running
worker 5 running
worker 7 running
worker 6 running
worker 8 running
worker 9 running
all workers done
*/
// section: outputSending blocks once the buffer is full, so only cap(sem) goroutines ever run at the same time. struct{}{} carries no data and takes no memory - we only care about the slot itself.
Worker Pool Vs. Semaphore
Both bound concurrency, but they’re shaped differently:
| | Semaphore | Worker Pool | |–|———–|————-| | Goroutines | One per task | Fixed set of workers | | Shape | Launch all tasks, cap how many run | Workers pull tasks off a channel | | Best for | Short, one-off tasks | Long-running workers, steady streams |
For a one-time batch like fetching a list of sites, the semaphore (throttle) shape is usually the cleaner fit. A worker pool earns its keep when you have long-lived workers pulling from a queue.
Work Pools And Rate Limiting
Channels work great for worker pools. You can find examples of both here:
Channel Exercise (20 Min)
The following program currently works but could be improved by adding concurrency.
Make the following changes in the program:
- Launch
charCountin a go routine. - Use a buffered channel to retrieve back the results.
- Create a struct type to receive the count and error from the channel
- Make sure you don’t exit the program until each go routine has finished.
NOTE: You must download the files using the Download Source button on the left and run this from the src/exercises/02-concurrency directory to ensure you have the local files to work with
package main
import (
"bytes"
"fmt"
"io"
"log"
"os"
"path/filepath"
"time"
)
func main() {
start := time.Now()
paths := []string{}
root := "./files"
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
paths = append(paths, path)
}
return nil
})
if err != nil {
log.Fatal(err)
}
for _, p := range paths {
charCount(p)
}
fmt.Println("process took", time.Since(start))
}
func charCount(path string) {
f, err := os.Open(path)
if err != nil {
fmt.Printf("error opening %s: %s\n", path, err)
return
}
defer f.Close()
var count int
buff := make([]byte, bytes.MinRead)
for {
i, err := f.Read(buff)
count += i
if err == io.EOF {
fmt.Printf("%s has %d characters\n", path, count)
return
}
if err != nil {
fmt.Printf("error reading file %s: %s\n", path, err)
return
}
}
}package main
import (
"bytes"
"fmt"
"io"
"log"
"os"
"path/filepath"
"time"
)
func main() {
start := time.Now()
paths := []string{}
root := "./files"
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
paths = append(paths, path)
}
return nil
})
if err != nil {
log.Fatal(err)
}
for _, p := range paths {
charCount(p)
}
fmt.Println("process took", time.Since(start))
}
func charCount(path string) {
f, err := os.Open(path)
if err != nil {
fmt.Printf("error opening %s: %s\n", path, err)
return
}
defer f.Close()
var count int
buff := make([]byte, bytes.MinRead)
for {
i, err := f.Read(buff)
count += i
if err == io.EOF {
fmt.Printf("%s has %d characters\n", path, count)
return
}
if err != nil {
fmt.Printf("error reading file %s: %s\n", path, err)
return
}
}
}Channel Exercise - Extra
Using a mix of channels and waitgroups, design a solution that immediately launches a go routine from the filepath.Walk function.
Things to consider:
- How will you use the waitgroup to know when all go routines have finished processing.
- How do you know when you have processed everything on the channel (hint, use an unbuffered channel)
- You can start processing the channel in a go routine before you start putting items in the channel…
Channel Solution
package main
import (
"bytes"
"fmt"
"io"
"log"
"os"
"path/filepath"
"time"
)
// <-- add a struct type to send responses back on the channel
type result struct {
path string
count int
err error
}
func main() {
start := time.Now()
paths := []string{}
root := "./files"
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
paths = append(paths, path)
}
return nil
})
if err != nil {
log.Fatal(err)
}
ch := make(chan result, len(paths)) // <-- create a buffered channel to start execution right away.
// <-- launch all the processes, use an anonymous go routine to not block execution
go func() {
for _, p := range paths {
go charCount(p, ch) // <-- Launch in a go routine, pass the channel
}
}()
// <-- Create a loop to read back the size of the buffered channel
for i := 0; i < cap(ch); i++ {
r := <-ch // <-- get the next available result from the channel
if r.err != nil {
fmt.Println(r.err)
continue
}
fmt.Printf("%s has %d characters\n", r.path, r.count)
}
fmt.Println("process took", time.Since(start))
}
func charCount(path string, ch chan result) { // <-- Accept the channel as an argument
f, err := os.Open(path)
if err != nil {
// <-- send back the error on a channel
ch <- result{path: path, count: 0, err: fmt.Errorf("error opening %s: %s\n", path, err)}
return
}
defer f.Close()
var count int
buff := make([]byte, bytes.MinRead)
for {
i, err := f.Read(buff)
count += i
if err == io.EOF {
// <-- send back the result on a channel
ch <- result{path: path, count: count}
return
}
if err != nil {
ch <- result{path: path, count: 0, err: fmt.Errorf("error reading file %s: %s\n", path, err)}
return
}
}
}package main
import (
"bytes"
"fmt"
"io"
"log"
"os"
"path/filepath"
"time"
)
// <-- add a struct type to send responses back on the channel
type result struct {
path string
count int
err error
}
func main() {
start := time.Now()
paths := []string{}
root := "./files"
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
paths = append(paths, path)
}
return nil
})
if err != nil {
log.Fatal(err)
}
ch := make(chan result, len(paths)) // <-- create a buffered channel to start execution right away.
// <-- launch all the processes, use an anonymous go routine to not block execution
go func() {
for _, p := range paths {
go charCount(p, ch) // <-- Launch in a go routine, pass the channel
}
}()
// <-- Create a loop to read back the size of the buffered channel
for i := 0; i < cap(ch); i++ {
r := <-ch // <-- get the next available result from the channel
if r.err != nil {
fmt.Println(r.err)
continue
}
fmt.Printf("%s has %d characters\n", r.path, r.count)
}
fmt.Println("process took", time.Since(start))
}
func charCount(path string, ch chan result) { // <-- Accept the channel as an argument
f, err := os.Open(path)
if err != nil {
// <-- send back the error on a channel
ch <- result{path: path, count: 0, err: fmt.Errorf("error opening %s: %s\n", path, err)}
return
}
defer f.Close()
var count int
buff := make([]byte, bytes.MinRead)
for {
i, err := f.Read(buff)
count += i
if err == io.EOF {
// <-- send back the result on a channel
ch <- result{path: path, count: count}
return
}
if err != nil {
ch <- result{path: path, count: 0, err: fmt.Errorf("error reading file %s: %s\n", path, err)}
return
}
}
}Channel Solution - Extra
package main
import (
"bytes"
"fmt"
"io"
"log"
"os"
"path/filepath"
"sync"
"time"
)
// <-- add a struct type to send responses back on the channel
type result struct {
path string
count int
err error
}
func main() {
start := time.Now()
wg := sync.WaitGroup{}
ch := make(chan result)
// begin watching the channel to process results.
// Do this in a go routine so it doesn't block program execution
go func() {
for r := range ch {
func() {
defer wg.Done()
if r.err != nil {
fmt.Println(r.err)
return
}
fmt.Printf("%s has %d characters\n", r.path, r.count)
}()
}
}()
root := "./files"
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
wg.Add(1) // make sure to track each go routine you launch in the wait group
go charCount(path, ch)
}
return nil
})
if err != nil {
log.Fatal(err)
}
wg.Wait() // wait for all go routines to finish
close(ch) // close the channel to exit the range go routine
fmt.Println("process took", time.Since(start))
}
func charCount(path string, ch chan result) { // <-- Accept the channel as an argument
f, err := os.Open(path)
if err != nil {
// <-- send back the error on a channel
ch <- result{path: path, count: 0, err: fmt.Errorf("error opening %s: %s\n", path, err)}
return
}
defer f.Close()
var count int
buff := make([]byte, bytes.MinRead)
for {
i, err := f.Read(buff)
count += i
if err == io.EOF {
// <-- send back the result on a channel
ch <- result{path: path, count: count}
return
}
if err != nil {
ch <- result{path: path, count: 0, err: fmt.Errorf("error reading file %s: %s\n", path, err)}
return
}
}
}package main
import (
"bytes"
"fmt"
"io"
"log"
"os"
"path/filepath"
"sync"
"time"
)
// <-- add a struct type to send responses back on the channel
type result struct {
path string
count int
err error
}
func main() {
start := time.Now()
wg := sync.WaitGroup{}
ch := make(chan result)
// begin watching the channel to process results.
// Do this in a go routine so it doesn't block program execution
go func() {
for r := range ch {
func() {
defer wg.Done()
if r.err != nil {
fmt.Println(r.err)
return
}
fmt.Printf("%s has %d characters\n", r.path, r.count)
}()
}
}()
root := "./files"
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() {
wg.Add(1) // make sure to track each go routine you launch in the wait group
go charCount(path, ch)
}
return nil
})
if err != nil {
log.Fatal(err)
}
wg.Wait() // wait for all go routines to finish
close(ch) // close the channel to exit the range go routine
fmt.Println("process took", time.Since(start))
}
func charCount(path string, ch chan result) { // <-- Accept the channel as an argument
f, err := os.Open(path)
if err != nil {
// <-- send back the error on a channel
ch <- result{path: path, count: 0, err: fmt.Errorf("error opening %s: %s\n", path, err)}
return
}
defer f.Close()
var count int
buff := make([]byte, bytes.MinRead)
for {
i, err := f.Read(buff)
count += i
if err == io.EOF {
// <-- send back the result on a channel
ch <- result{path: path, count: count}
return
}
if err != nil {
ch <- result{path: path, count: 0, err: fmt.Errorf("error reading file %s: %s\n", path, err)}
return
}
}
}Graceful Shutdown
All programs should attempt a graceful shutdown. Graceful shutdown includes:
- Detecting that the program was requested to shut down
- Shut down all internal processes, including long running go routines
- Have a reasonable timeout in the event that internal processes are taking to long to shut down or are deadlocked
- Respond to an actual user request for immediate hard shutdown
- Record the result of the shutdown (success, timeout, user intervention)
The `os/signals` Package
You can use channels and the os/signal package to shut down your program:
func main() {
// Set up channel on which to send signal notifications.
// We must use a buffered channel or risk missing the signal
// if we're not ready to receive when the signal is sent.
c := make(chan os.Signal, 1)
// Wire up the channel to an `os.Signal`
signal.Notify(c, os.Interrupt)
fmt.Println("awaiting signal...")
// Block until a signal is received.
s := <-c
fmt.Println("Got signal:", s)
// do my code shutdown....
// then exit
}
package main
import (
"fmt"
"os"
"os/signal"
)
// section: main
func main() {
// Set up channel on which to send signal notifications.
// We must use a buffered channel or risk missing the signal
// if we're not ready to receive when the signal is sent.
c := make(chan os.Signal, 1)
// Wire up the channel to an `os.Signal`
signal.Notify(c, os.Interrupt)
fmt.Println("awaiting signal...")
// Block until a signal is received.
s := <-c
fmt.Println("Got signal:", s)
// do my code shutdown....
// then exit
}
// section: mainProgram Shutdown
Given an existing basic startup:
func main() {
c := cmd.New()
if err := c.Open(); err != nil {
log.Fatal(c)
}
wait := make(chan struct{})
<-wait
if err := c.Close(); err != nil {
log.Fatal(err)
}
log.Println("successfuly shutdown")
}
package main
import (
"log"
"training/cmd"
)
// section: main
func main() {
c := cmd.New()
if err := c.Open(); err != nil {
log.Fatal(c)
}
wait := make(chan struct{})
<-wait
if err := c.Close(); err != nil {
log.Fatal(err)
}
log.Println("successfuly shutdown")
}
// section: main
/*
// section: output
2020/04/20 11:09:09 monitor check
2020/04/20 11:09:10 monitor check
2020/04/20 11:09:11 monitor check
^Csignal: interrupt
// section: output
*/There is no way to gracefully shutdown this program. Any interrupt will result in an immediate exit, and not actually finish proper shutdown.
Output:
2020/04/20 11:09:09 monitor check
2020/04/20 11:09:10 monitor check
2020/04/20 11:09:11 monitor check
^Csignal: interruptpackage main
import (
"log"
"training/cmd"
)
// section: main
func main() {
c := cmd.New()
if err := c.Open(); err != nil {
log.Fatal(c)
}
wait := make(chan struct{})
<-wait
if err := c.Close(); err != nil {
log.Fatal(err)
}
log.Println("successfuly shutdown")
}
// section: main
/*
// section: output
2020/04/20 11:09:09 monitor check
2020/04/20 11:09:10 monitor check
2020/04/20 11:09:11 monitor check
^Csignal: interrupt
// section: output
*/Notice that we don’t see any output from the actual shutdown. This is because once the interrupt was detected, it immediately exited the program.
Add Shutdown Detection
Using the os.Signals package, we can re-write this code to allow for a better shutdown:
func main() {
c := cmd.New()
if err := c.Open(); err != nil {
log.Fatal(c)
}
quit := make(chan os.Signal, 1)
// Wire up the channel to an `os.Signal`
signal.Notify(quit, os.Interrupt)
log.Println("program started")
// Block until a signal is received.
s := <-quit
log.Printf("received %s signal for shutdown", s)
if err := c.Close(); err != nil {
log.Fatal(err)
}
log.Println("successfuly shutdown")
}
package main
import (
"log"
"os"
"os/signal"
"training/cmd"
)
// section: main
func main() {
c := cmd.New()
if err := c.Open(); err != nil {
log.Fatal(c)
}
quit := make(chan os.Signal, 1)
// Wire up the channel to an `os.Signal`
signal.Notify(quit, os.Interrupt)
log.Println("program started")
// Block until a signal is received.
s := <-quit
log.Printf("received %s signal for shutdown", s)
if err := c.Close(); err != nil {
log.Fatal(err)
}
log.Println("successfuly shutdown")
}
// section: main
/*
// section: output
2020/04/20 11:17:03 program started
2020/04/20 11:17:04 monitor check
2020/04/20 11:17:05 monitor check
2020/04/20 11:17:06 monitor check
^C2020/04/20 11:17:06 received interrupt signal for shutdown
2020/04/20 11:17:06 shutting down monitor
2020/04/20 11:17:06 successfuly shutdown
// section: output
*/Now when we send in an interrupt, the program will intercept it and allow us to shut down gracefully
2020/04/20 11:17:03 program started
2020/04/20 11:17:04 monitor check
2020/04/20 11:17:05 monitor check
2020/04/20 11:17:06 monitor check
^C2020/04/20 11:17:06 received interrupt signal for shutdown
2020/04/20 11:17:06 shutting down monitor
2020/04/20 11:17:06 successfuly shutdownpackage main
import (
"log"
"os"
"os/signal"
"training/cmd"
)
// section: main
func main() {
c := cmd.New()
if err := c.Open(); err != nil {
log.Fatal(c)
}
quit := make(chan os.Signal, 1)
// Wire up the channel to an `os.Signal`
signal.Notify(quit, os.Interrupt)
log.Println("program started")
// Block until a signal is received.
s := <-quit
log.Printf("received %s signal for shutdown", s)
if err := c.Close(); err != nil {
log.Fatal(err)
}
log.Println("successfuly shutdown")
}
// section: main
/*
// section: output
2020/04/20 11:17:03 program started
2020/04/20 11:17:04 monitor check
2020/04/20 11:17:05 monitor check
2020/04/20 11:17:06 monitor check
^C2020/04/20 11:17:06 received interrupt signal for shutdown
2020/04/20 11:17:06 shutting down monitor
2020/04/20 11:17:06 successfuly shutdown
// section: output
*/Note: This program will hang indefinitely if the Close routine deadlocks for any reason. To solve this, we need to add a timeout to our shutdown routine.
Shutdown With Timeout
A well behaved program should attempt to shut down gracefully. The following code sample is one possible template to do so.
func main() {
c := cmd.New()
if err := c.Open(); err != nil {
log.Fatal(c)
}
sigCh := make(chan os.Signal, 1)
// Wire up the channel to an `os.Signal`
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
log.Println("Listening for signals")
// launch all program code...
// Block until one of the signals above is received
<-sigCh
log.Println("Signal received, initializing clean shutdown...")
// do graceful shutdown
// make sure not to block for the timeout added later
closed := make(chan struct{})
go func() {
// do all graceful shutdown of program
if err := c.Close(); err != nil {
log.Fatal(err)
}
// signal you've completed your shutdown
close(closed)
}()
// Block again until another signal is received, a shutdown timeout elapses,
// or the Command is gracefully closed
log.Println("Waiting for clean shutdown...")
select {
case <-sigCh:
log.Println("second signal received, initializing hard shutdown")
case <-time.After(time.Second * 30):
log.Println("time limit reached, initializing hard shutdown")
case <-closed:
log.Println("server shutdown completed")
}
// goodbye.
}
package main
import (
"log"
"os"
"os/signal"
"syscall"
"time"
"training/cmd"
)
// section: main
func main() {
c := cmd.New()
if err := c.Open(); err != nil {
log.Fatal(c)
}
sigCh := make(chan os.Signal, 1)
// Wire up the channel to an `os.Signal`
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
log.Println("Listening for signals")
// launch all program code...
// Block until one of the signals above is received
<-sigCh
log.Println("Signal received, initializing clean shutdown...")
// do graceful shutdown
// make sure not to block for the timeout added later
closed := make(chan struct{})
go func() {
// do all graceful shutdown of program
if err := c.Close(); err != nil {
log.Fatal(err)
}
// signal you've completed your shutdown
close(closed)
}()
// Block again until another signal is received, a shutdown timeout elapses,
// or the Command is gracefully closed
log.Println("Waiting for clean shutdown...")
select {
case <-sigCh:
log.Println("second signal received, initializing hard shutdown")
case <-time.After(time.Second * 30):
log.Println("time limit reached, initializing hard shutdown")
case <-closed:
log.Println("server shutdown completed")
}
// goodbye.
}
// section: main
/*
// section: output
2020/04/20 11:22:35 Listening for signals
2020/04/20 11:22:36 monitor check
2020/04/20 11:22:37 monitor check
^C2020/04/20 11:22:37 Signal received, initializing clean shutdown...
2020/04/20 11:22:37 Waiting for clean shutdown...
2020/04/20 11:22:37 shutting down monitor
2020/04/20 11:22:42 server shutdown completed
// section: output
*/Output:
2020/04/20 11:22:35 Listening for signals
2020/04/20 11:22:36 monitor check
2020/04/20 11:22:37 monitor check
^C2020/04/20 11:22:37 Signal received, initializing clean shutdown...
2020/04/20 11:22:37 Waiting for clean shutdown...
2020/04/20 11:22:37 shutting down monitor
2020/04/20 11:22:42 server shutdown completedpackage main
import (
"log"
"os"
"os/signal"
"syscall"
"time"
"training/cmd"
)
// section: main
func main() {
c := cmd.New()
if err := c.Open(); err != nil {
log.Fatal(c)
}
sigCh := make(chan os.Signal, 1)
// Wire up the channel to an `os.Signal`
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
log.Println("Listening for signals")
// launch all program code...
// Block until one of the signals above is received
<-sigCh
log.Println("Signal received, initializing clean shutdown...")
// do graceful shutdown
// make sure not to block for the timeout added later
closed := make(chan struct{})
go func() {
// do all graceful shutdown of program
if err := c.Close(); err != nil {
log.Fatal(err)
}
// signal you've completed your shutdown
close(closed)
}()
// Block again until another signal is received, a shutdown timeout elapses,
// or the Command is gracefully closed
log.Println("Waiting for clean shutdown...")
select {
case <-sigCh:
log.Println("second signal received, initializing hard shutdown")
case <-time.After(time.Second * 30):
log.Println("time limit reached, initializing hard shutdown")
case <-closed:
log.Println("server shutdown completed")
}
// goodbye.
}
// section: main
/*
// section: output
2020/04/20 11:22:35 Listening for signals
2020/04/20 11:22:36 monitor check
2020/04/20 11:22:37 monitor check
^C2020/04/20 11:22:37 Signal received, initializing clean shutdown...
2020/04/20 11:22:37 Waiting for clean shutdown...
2020/04/20 11:22:37 shutting down monitor
2020/04/20 11:22:42 server shutdown completed
// section: output
*/Hot Reload (HUP)
You can also use signals to perform live reloads. Here is a psuedo example of how to accomplish that task:
func main() {
// wire up a quit channel to monitor
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
// create a hup channel
hup := make(chan os.Signal, 1)
signal.Notify(hup, os.Interrupt, syscall.SIGHUP)
wg := &sync.WaitGroup{}
wg.Add(1)
go hotReload(hup, wg)
<-quit
fmt.Println("waiting for clean shutdown")
wg.Wait()
fmt.Println("clean shutdown")
}
func hotReload(hup chan os.Signal, wg *sync.WaitGroup) {
defer wg.Done()
for {
s := <-hup
switch s {
case syscall.SIGHUP:
fmt.Println("reloading config...")
case os.Interrupt:
fmt.Println("closing config reload")
return
}
}
}
package main
import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
)
// section: code
func main() {
// wire up a quit channel to monitor
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
// create a hup channel
hup := make(chan os.Signal, 1)
signal.Notify(hup, os.Interrupt, syscall.SIGHUP)
wg := &sync.WaitGroup{}
wg.Add(1)
go hotReload(hup, wg)
<-quit
fmt.Println("waiting for clean shutdown")
wg.Wait()
fmt.Println("clean shutdown")
}
func hotReload(hup chan os.Signal, wg *sync.WaitGroup) {
defer wg.Done()
for {
s := <-hup
switch s {
case syscall.SIGHUP:
fmt.Println("reloading config...")
case os.Interrupt:
fmt.Println("closing config reload")
return
}
}
}
// section: code
/*
// section: commands
$ ps | grep hup
46490 ttys006 0:00.01 ./hup
$ kill -s SIGHUP 46490
$ kill -s SIGINT 46490
// section: commands
// section: output
$ ./hup
reloading config...
waiting for clean shutdown
closing config reload
clean shutdown
// section: output
*/Because we are using the kill command, we need to compile and run the binary. Using go run will intercept the SIGHUP and the program won’t perform as expected.
Commands:
package main
import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
)
// section: code
func main() {
// wire up a quit channel to monitor
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
// create a hup channel
hup := make(chan os.Signal, 1)
signal.Notify(hup, os.Interrupt, syscall.SIGHUP)
wg := &sync.WaitGroup{}
wg.Add(1)
go hotReload(hup, wg)
<-quit
fmt.Println("waiting for clean shutdown")
wg.Wait()
fmt.Println("clean shutdown")
}
func hotReload(hup chan os.Signal, wg *sync.WaitGroup) {
defer wg.Done()
for {
s := <-hup
switch s {
case syscall.SIGHUP:
fmt.Println("reloading config...")
case os.Interrupt:
fmt.Println("closing config reload")
return
}
}
}
// section: code
/*
// section: commands
$ ps | grep hup
46490 ttys006 0:00.01 ./hup
$ kill -s SIGHUP 46490
$ kill -s SIGINT 46490
// section: commands
// section: output
$ ./hup
reloading config...
waiting for clean shutdown
closing config reload
clean shutdown
// section: output
*/Output:
package main
import (
"fmt"
"os"
"os/signal"
"sync"
"syscall"
)
// section: code
func main() {
// wire up a quit channel to monitor
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
// create a hup channel
hup := make(chan os.Signal, 1)
signal.Notify(hup, os.Interrupt, syscall.SIGHUP)
wg := &sync.WaitGroup{}
wg.Add(1)
go hotReload(hup, wg)
<-quit
fmt.Println("waiting for clean shutdown")
wg.Wait()
fmt.Println("clean shutdown")
}
func hotReload(hup chan os.Signal, wg *sync.WaitGroup) {
defer wg.Done()
for {
s := <-hup
switch s {
case syscall.SIGHUP:
fmt.Println("reloading config...")
case os.Interrupt:
fmt.Println("closing config reload")
return
}
}
}
// section: code
/*
// section: commands
$ ps | grep hup
46490 ttys006 0:00.01 ./hup
$ kill -s SIGHUP 46490
$ kill -s SIGINT 46490
// section: commands
// section: output
$ ./hup
reloading config...
waiting for clean shutdown
closing config reload
clean shutdown
// section: output
*/Channels - Some Wisdom
I can’t tell you how many times I start with channels, and by the time I’m done, I’ve completely optimized them out. – Cory LaNou
When I first learned about channels, I wanted use them everywhere. Now, I rarely use them at all. – Mat Ryer
These quotes are not intended to steer you away from using channels, but more to think about do you in fact need a channel? It is common for developers new to Go to overuse channels, which leads to unecessary code complexity with no benefit to program performance.
Exercise (15 Min)
Given the following program, add the ability for intercepting a os.Interrupt signal and wait for the program to shut down gracefully.
Note: You only have to make modifications to the main function.
For extra credit, add in a timeout for shutdown as well, as well as monitor for a user sending in a second shutdown request.
package main
import (
"log"
"sync"
"time"
)
func main() {
c := NewCommand()
c.Open()
wait := make(chan struct{})
<-wait
c.Close()
}
func NewCommand() *command {
wg := sync.WaitGroup{}
return &command{
quit: make(chan struct{}),
wg: &wg,
}
}
type command struct {
quit chan struct{}
wg *sync.WaitGroup
}
func (c *command) Open() {
c.wg.Add(1)
go c.monitor()
}
func (c *command) Close() {
close(c.quit)
c.wg.Wait()
}
func (c *command) monitor() {
defer c.wg.Done()
ticker := time.NewTicker(time.Second)
for {
select {
case <-c.quit:
log.Println("shutting down monitor")
return
case <-ticker.C:
log.Println("monitor check")
}
}
}package main
import (
"log"
"sync"
"time"
)
func main() {
c := NewCommand()
c.Open()
wait := make(chan struct{})
<-wait
c.Close()
}
func NewCommand() *command {
wg := sync.WaitGroup{}
return &command{
quit: make(chan struct{}),
wg: &wg,
}
}
type command struct {
quit chan struct{}
wg *sync.WaitGroup
}
func (c *command) Open() {
c.wg.Add(1)
go c.monitor()
}
func (c *command) Close() {
close(c.quit)
c.wg.Wait()
}
func (c *command) monitor() {
defer c.wg.Done()
ticker := time.NewTicker(time.Second)
for {
select {
case <-c.quit:
log.Println("shutting down monitor")
return
case <-ticker.C:
log.Println("monitor check")
}
}
}Solution
package main
import (
"log"
"os"
"os/signal"
"sync"
"time"
)
func main() {
c := NewCommand()
c.Open()
sigCh := make(chan os.Signal, 1)
// Wire up the channel to an `os.Signal`
signal.Notify(sigCh, os.Interrupt)
log.Println("Listening for signals")
<-sigCh
log.Println("Signal received, initializing clean shutdown...")
// do graceful shutdown
// make sure not to block for the timeout added later
closed := make(chan struct{})
go func() {
// do all graceful shutdown of program
c.Close()
// signal you've completed your shutdown
close(closed)
}()
log.Println("Waiting for clean shutdown...")
<-closed
log.Println("server shutdown completed")
}
func NewCommand() *command {
wg := sync.WaitGroup{}
return &command{
quit: make(chan struct{}),
wg: &wg,
}
}
type command struct {
quit chan struct{}
wg *sync.WaitGroup
}
func (c *command) Open() {
c.wg.Add(1)
go c.monitor()
}
func (c *command) Close() {
// add artificial time to simulate the real world
time.Sleep(time.Second * 5)
close(c.quit)
c.wg.Wait()
}
func (c *command) monitor() {
defer c.wg.Done()
ticker := time.NewTicker(time.Second)
for {
select {
case <-c.quit:
log.Println("shutting down monitor")
return
case <-ticker.C:
log.Println("monitor check")
}
}
}package main
import (
"log"
"os"
"os/signal"
"sync"
"time"
)
func main() {
c := NewCommand()
c.Open()
sigCh := make(chan os.Signal, 1)
// Wire up the channel to an `os.Signal`
signal.Notify(sigCh, os.Interrupt)
log.Println("Listening for signals")
<-sigCh
log.Println("Signal received, initializing clean shutdown...")
// do graceful shutdown
// make sure not to block for the timeout added later
closed := make(chan struct{})
go func() {
// do all graceful shutdown of program
c.Close()
// signal you've completed your shutdown
close(closed)
}()
log.Println("Waiting for clean shutdown...")
<-closed
log.Println("server shutdown completed")
}
func NewCommand() *command {
wg := sync.WaitGroup{}
return &command{
quit: make(chan struct{}),
wg: &wg,
}
}
type command struct {
quit chan struct{}
wg *sync.WaitGroup
}
func (c *command) Open() {
c.wg.Add(1)
go c.monitor()
}
func (c *command) Close() {
// add artificial time to simulate the real world
time.Sleep(time.Second * 5)
close(c.quit)
c.wg.Wait()
}
func (c *command) monitor() {
defer c.wg.Done()
ticker := time.NewTicker(time.Second)
for {
select {
case <-c.quit:
log.Println("shutting down monitor")
return
case <-ticker.C:
log.Println("monitor check")
}
}
}Solution (Extra Credit)
package main
import (
"log"
"os"
"os/signal"
"sync"
"time"
)
func main() {
c := NewCommand()
c.Open()
sigCh := make(chan os.Signal, 1)
// Wire up the channel to an `os.Signal`
signal.Notify(sigCh, os.Interrupt)
log.Println("Listening for signals")
<-sigCh
log.Println("Signal received, initializing clean shutdown...")
// do graceful shutdown
// make sure not to block for the timeout added later
closed := make(chan struct{})
go func() {
// do all graceful shutdown of program
c.Close()
// signal you've completed your shutdown
close(closed)
}()
log.Println("Waiting for clean shutdown...")
select {
case <-sigCh:
log.Println("second signal received, initializing hard shutdown")
case <-time.After(time.Second * 30):
log.Println("time limit reached, initializing hard shutdown")
case <-closed:
log.Println("server shutdown completed")
}
}
func NewCommand() *command {
wg := sync.WaitGroup{}
return &command{
quit: make(chan struct{}),
wg: &wg,
}
}
type command struct {
quit chan struct{}
wg *sync.WaitGroup
}
func (c *command) Open() {
c.wg.Add(1)
go c.monitor()
}
func (c *command) Close() {
// add artificial time to simulate the real world
time.Sleep(time.Second * 5)
close(c.quit)
c.wg.Wait()
}
func (c *command) monitor() {
defer c.wg.Done()
ticker := time.NewTicker(time.Second)
for {
select {
case <-c.quit:
log.Println("shutting down monitor")
return
case <-ticker.C:
log.Println("monitor check")
}
}
}package main
import (
"log"
"os"
"os/signal"
"sync"
"time"
)
func main() {
c := NewCommand()
c.Open()
sigCh := make(chan os.Signal, 1)
// Wire up the channel to an `os.Signal`
signal.Notify(sigCh, os.Interrupt)
log.Println("Listening for signals")
<-sigCh
log.Println("Signal received, initializing clean shutdown...")
// do graceful shutdown
// make sure not to block for the timeout added later
closed := make(chan struct{})
go func() {
// do all graceful shutdown of program
c.Close()
// signal you've completed your shutdown
close(closed)
}()
log.Println("Waiting for clean shutdown...")
select {
case <-sigCh:
log.Println("second signal received, initializing hard shutdown")
case <-time.After(time.Second * 30):
log.Println("time limit reached, initializing hard shutdown")
case <-closed:
log.Println("server shutdown completed")
}
}
func NewCommand() *command {
wg := sync.WaitGroup{}
return &command{
quit: make(chan struct{}),
wg: &wg,
}
}
type command struct {
quit chan struct{}
wg *sync.WaitGroup
}
func (c *command) Open() {
c.wg.Add(1)
go c.monitor()
}
func (c *command) Close() {
// add artificial time to simulate the real world
time.Sleep(time.Second * 5)
close(c.quit)
c.wg.Wait()
}
func (c *command) monitor() {
defer c.wg.Done()
ticker := time.NewTicker(time.Second)
for {
select {
case <-c.quit:
log.Println("shutting down monitor")
return
case <-ticker.C:
log.Println("monitor check")
}
}
}