Go Routines and Channels
Concurrency is the computer science term for breaking up a single process into inde‐ pendent
components and specifying how these components safely share data. Most languages provide concurrency
via a library using operating system–level threads that share data by attempting to acquire locks. Go is
different. Its main concurrency model, arguably Go’s most famous feature, is based on Communicating
Sequential Processes (CSP).
Broadly speaking, all programs follow the same three-step process: they take data, transform it, and then
output the result. Whether you should use concurrency in your program depends on how data flows
through the steps in your program. Some‐ times two steps can be concurrent because the data from one is
not required for the other to proceed, and at other times two steps must happen in series because one
depends on the other’s output. Use concurrency when you want to combine data from multiple operations
that can operate independently.
Another important thing to note is that concurrency isn’t worth using if the process that’s running
concurrently doesn’t take a lot of time. Concurrency isn’t free; many common in-memory algorithms are
so fast that the overhead of passing values via concurrency overwhelms any potential time savings you’d
gain by running concur‐ rent code in parallel.
A goroutine as a lightweight thread, managed by the Go runtime system.
This program consists of two goroutines. The first goroutine is implicit and is the main function
itself. The second goroutine is created when we call go f(0). Normally when we invoke a
function our program will execute all the statements in a function and then return to the next line
following the invocation. With a goroutine we return immediately to the next line and don't wait
for the function to complete. This is why the call to the Scanln function has been included;
without it the program would exit before being given the opportunity to print all the numbers.
Goroutines are lightweight and we can easily create thousands of them.
1. program to run 10 goroutines
package main
import (
"fmt"
"math/rand"
"time"
)
func f(n int) {
for i := 0; i < 10; i++ {
fmt.Println(n, ":", i)
amt := time.Duration(rand.Intn(250))
time.Sleep(time.Millisecond * amt)
}
}
func main() {
for i := 0; i < 10; i++ {
go f(i)
}
var input string
fmt.Scanln(&input)
}
Output:
6:0
8:0
1:0
7:0
2:0
9:0
0:0
3:0
3:1
4:0
5:0
3:2
7:1
4:1
8:1
5:1
3:3
7:2
1:1
2:1
0:1
9:1
4:2
8:2
6:1
5:2
4:3
3:4
7:3
9:2
8:3
0:2
3:5
1:2
1:3
6:2
1:4
2:2
3:6
4:4
8:4
7:4
1:5
0:3
5:3
3:7
6:3
0:4
9:3
5:4
4:5
2:3
8:5
6:4
7:5
3:8
1:6
8:6
9:4
6:5
5:5
1:7
0:5
4:6
5:6
3:9
7:6
2:4
8:7
1:8
1:9
6:6
7:7
6:7
9:5
4:7
0:6
8:8
5:7
2:5
9:6
4:8
6:8
7:8
4:9
8:9
0:7
9:7
2:6
5:8
9:8
2:7
9:9
0:8
2:8
6:9
7:9
0:9
5:9
2:9
time.Duration is used to convert an int to type time.Duration
This is because time.Millisecond * amt involves multiplying a time.Duration (which
time.Millisecondis) by an int, and Go doesn’t allow this without explicit conversion.
Channels
Channels provide a way for two goroutines to communicate with one another and synchronize their
execution. Here is an example program using channels:
2. GO channels
package main
import (
"fmt"
"time"
)
func pinger(c chan string) {
for i := 0; ; i++ {
c <- "ping"
}
}
func printer(c chan string) {
for {
msg := <-c
fmt.Println(msg)
time.Sleep(time.Second * 1)
}
}
func main() {
var c chan string = make(chan string)
//fmt.Println(len(c), cap(c))
go pinger(c)
go printer(c)
var input string
fmt.Scanln(&input)
}
This program will print “ping” forever (hit enter to stop it). A channel type is represented with the
keyword chan followed by the type of the things that are passed on the channel (in this case we are
passing strings). The <- (left arrow) operator is used to send and receive messages on the channel. c
<- "ping" means send "ping". msg := <- c means receive a message and store it in msg. The
fmt line could also have been written like this: fmt.Println(<-c) in which case we could remove
the previous line.
Using a channel like this synchronizes the two goroutines. When pinger attempts to send a message
on the channel it will wait until printer is ready to receive the message. (this is known as blocking)
Let's add another sender to the program and see what happens. Add this function:
func ponger(c chan string) {
for i := 0; ; i++ {
c <- "pong"
}
}
The program will now take turns printing “ping” and “pong”.
3. Program with two senders
package main
import (
"fmt"
"time"
)
func pinger(c chan string) {
for i := 0; ; i++ {
c <- "ping"
}
}
func printer(c chan string) {
for {
msg := <-c
fmt.Println(msg)
time.Sleep(time.Second * 1)
}
}
func ponger(c chan string) {
for i := 0; ; i++ {
c <- "pong"
}
}
func main() {
var c chan string = make(chan string)
go pinger(c)
go ponger(c)
go printer(c)
var input string
fmt.Scanln(&input)
}
How the Go Scheduler Works:
1. Preemptive Scheduling:
○ Go's scheduler is preemptive, meaning it can interrupt
long-running goroutines to ensure that other goroutines
get a chance to run. This helps prevent a single goroutine
from monopolizing the CPU.
2. Work Stealing:
○ Go uses a work-stealing algorithm to balance the load across
"processors”. If one processor's queue of runnable goroutines becomes
empty, it can "steal" goroutines from the queue of another processor.
3. Goroutine Prioritization:
○ The scheduler may prioritize goroutines that are ready to run
(i.e., those that are not blocked on I/O or synchronization
operations). However, there’s no strict prioritization or fairness
guarantee.
4. System Calls and Blocking Operations:
○ When a goroutine makes a system call or performs a blocking
operation (e.g., waiting on a channel, sleeping, etc.), the scheduler
can put that goroutine to sleep and switch to another one that’s ready
to run.
5. Scheduling Policies:
○ While Go’s scheduling is not purely round-robin, it does attempt
to give each runnable goroutine a fair share of CPU time. However,
this fairness is balanced with efficiency, and the scheduler may favor
certain goroutines based on context (e.g., if a goroutine is quickly
yielding or completing its work).
Channel Direction
We can specify a direction on a channel type thus restricting it to either sending or receiving. For
example pinger's function signature can be changed to this:
func pinger(c chan<- string)
Now c can only be sent to. Attempting to receive from c will result in a compiler error. Similarly we
can change printer to this:
func printer(c <-chan string)
A channel that doesn't have these restrictions is known as bi-directional. A bi-directional channel can
be passed to a function that takes send-only or receive-only channels, but the reverse is not true.
4. Using Select
Go has a special statement called select which works like a switch but for channels:
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
for {
c1 <- "from 1"
time.Sleep(time.Second * 2)
}
}()
go func() {
for {
c2 <- "from 2"
time.Sleep(time.Second * 3)
}
}()
go func() {
for {
select {
case msg1 := <-c1:
fmt.Println(msg1)
case msg2 := <-c2:
fmt.Println(msg2)
}
}
}()
var input string
fmt.Scanln(&input)
}
Output:
from 1
from 2
from 1
from 2
from 1
from 2
from 1
from 1
from 2
from 1
from 2
from 1
from 1
from 2
from 1
from 2
from 1
from 1
from 2
Buffered Channels
It's also possible to pass a second parameter to the make function when creating a channel:
c := make(chan int, 1)
This creates a buffered channel with a capacity of 1. Normally channels are synchronous; both sides
of the channel will wait until the other side is ready. A buffered channel is asynchronous; sending or
receiving a message will not wait unless the channel is already full.
5. Buffered Channels
package main
import (
"fmt"
"time"
)
func main() {
// Create a buffered channel with a capacity of 2
c := make(chan int, 2)
// Start a goroutine that sends data into the channel
go func() {
for i := 1; i <= 5; i++ {
//fmt.Printf("Sending %d\n", i)
c <- i
fmt.Printf("Sent %d\n", i)
time.Sleep(time.Second) // Simulate some delay
}
}()
// Start a goroutine that receives data from the channel
go func() {
for i := 1; i <= 5; i++ {
msg := <-c
fmt.Printf("Received %d\n", msg)
time.Sleep(4 * time.Second) // Simulate longer
processing delay
}
}()
// Give goroutines time to complete
time.Sleep(20 * time.Second)
}
Output:
Sent 1
Received 1
Sent 2
Sent 3
Sent 4
Received 2
Received 3
Sent 5
Received 4
Received 5
6. Range and Close
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}