Understanding channels and goroutines
Channels are made for communicating, which is why Go's mantra is as follows:
A channel is made for sharing data, and it usually connects two or more execution threads in an application, which makes it possible to send and receive data without worrying about data safety. Go has a lightweight implementation of a thread that is managed by the runtime instead of the operating system, and the best way to make them communicate is through the use of channels.
Creating a new goroutine is pretty easy – you just need to use the go operator, followed by a function execution. This includes method calls and closures. If the function has any arguments, they will be evaluated before the routine starts. Once it starts, there is no guarantee that changes to variables from an outer scope will be synchronized if you don't use channels:
a := myType{}
go doSomething(a) // function call
go func() { // closure call
// ...
}() // note that the closure is executed
go a.someMethod() // method call
We already saw how to create a new channel with the make function. If a channel is unbuffered (0 capacity), sending to the channel is a blocking operation that will wait for another goroutine to receive data from the same channel in order to unlock it. The capacity shows how many messages the channel is capable of holding before sending the next becomes a blocking operation:
unbuf := make(chan int) // unbuffered channel
buf := make(chan int, 3) // channel with size 3
In order to send to a channel, we can use the <- operator. If the channel is on the left of the operator, it is a send operation, and if it's on the right, it is a receive operation. The value that's received from a channel can be assigned to a variable, as follows:
var ch = make(chan int)
go func() {
b := <-ch // receive and assign
fmt.Println(b)
}()
ch <- 10 // send to channel
Closing a channel can be done with the close() function. This operation means that no more values can be sent to the channel. This is why it is usually the responsibility of the sender. Sending to a close channel will cause a panic anyway, which is why it should be done by the receiver. Also, when receiving from a channel, a second Boolean variable can be specified in the assignment. This will be true if the channel is still open so that the receiver knows when a channel has been closed:
var ch = make(chan int)
go func() {
b, ok := <-ch // channel open, ok is true
b, ok = <-ch // channel closed, ok is false
b <- ch // channel close, b will be a zero value
}()
ch <- 10 // send to channel
close(ch) // close the channel
There is a special control statement called select which works exactly like switch, but with only operations on channels:
var ch1 = make(chan int)
var ch2 = make(chan int)
go func() { ch1 <- 10 }
go func() { <-ch2 }
switch { // the first operation that completes is selected
case a := <-ch1:
fmt.Println(a)
case ch2 <- 20:
fmt.Println(b)
}