The sleep
in the previous section has many issues: It doesn't accurately describe the goal of waiting for all goroutines to exit before exiting, and it is inherently a race condition. Channels allow goroutines to be coordinated.
Channels allow you to send data between goroutines in a first-in first-out fashion.
First let's explore channels with the smallest goroutine possible, before jumping into more complex examples:
package main
import (
"fmt"
"time"
)
func main() {
codes := make(chan int)
func() {
time.Sleep(4 * time.Second)
codes <- 1
}()
code := <-codes
fmt.Println(code)
}
Breaking this down line by line:
codes := make(chan int)
- initializes
codes
as a unbuffered channel for ints. - We could not write
var codes chan int
because the zero value would be nil.
func() { ... }()
- This is an anonymous closure.
codes <- 1
sends 1 tocodes
.- sends block until the receiver is ready to receive.
code := <-codes
- This operation waits until
codes
has a value to give. <-codes
receives 1 fromcodes
and initalizescode
with it.- receives block until the sender is ready to send.
Stop and remove the anonymous function the pushes 1 to codes
from program and re-run it. What happens?
fatal error: all goroutines are asleep - deadlock!
This tells us that code := <-codes
is deadlocked because there are no goroutines to push data into codes
.
The difference between buffered channels and unbuffered channels is:
- There is no blocking on sends unless the buffer is completely full.
- There is no blocking on receives unless the buffer is empty.
- Buffered channels are initialized with
make(chan type, N)
with type being the data type of the channel, and N being the size of the buffer.
Let's recreate the previous example from 5.1 using buffered channels:
package main
import "fmt"
var saidHi chan bool = make(chan bool, 3)
func main() {
for _, name := range []string{"Andrew", "Beavis", "Cory"} {
go sayHi(name)
}
for i := 0; i < 3; i++ {
<-saidHi
}
}
func sayHi(n string) {
fmt.Println(n)
saidHi <- true
}
func someFunction(success chan bool, other string) {
This function signature takes a chan bool
and a string
, but doesn't describe how the channel is going to be used. We could make this function signature more descriptive by saying that success
will only be sent to and never received from:
func someFunction(success chan<- bool, name string) {
To describe a channel that is only received from in a function signature, we could write <-chan bool
.
Select statements can be used to provide non-blocking channel operations:
select {
case code := <-codes:
fmt.Println(code)
default:
fmt.Println("No code received. Continuing")
}s
What happens when we would like to receive from multiple channels at once? You could create a goroutine for each one, but sometimes that causes explosive complexity. You can also use select
for this:
select {
case code := <-codes:
fmt.Println(code + 1)
case msg := <-messages:
fmt.Println(msg)
}
The above example is blocking until one of codes or messages sends a value. If default
is added, it would be non-blocking.
Closing a channel indicates no more data is going to be sent over it:
package main
import "fmt"
var data = make(chan bool, 2)
func main() {
go AddValues()
for {
element, more := <-data
if more {
fmt.Println(element)
} else {
break
}
}
fmt.Println("complete")
}
func AddValues() {
data <- true
data <- false
close(data)
}
The important pieces from above:
element, more := <-data
- Similar to type casting or map indexing, we can include a second variable
more
to determine if the channel is closed.
close(data)
- This indicates that no more data will be sent.
The last thing that close
empowers you to do is use channels with range
:
package main
import "fmt"
func main() {
x := make(chan bool, 5)
x <- false
x <- true
close(x)
for el := range x {
fmt.Println(el)
}
}
While important, goroutines and channels add complexity and make debugging much harder. Ensure that the concurrency fits the problem before diving into a solution.
Memory leaks? Totally possible with goroutines and channels.