A goroutine is a lightweight thread of execution managed by the Go runtime. Unlike traditional threads, goroutines have a very small memory footprint (typically 2KB of stack space initially) and can be created in large numbers without exhausting system resources. Goroutines are multiplexed onto a smaller number of operating system threads, which allows the Go runtime to efficiently manage concurrent execution.
Channels are typed conduits through which you can send and receive values with the type - safe <-
operator. They provide a way for goroutines to communicate and synchronize with each other. Channels can be used to pass data between goroutines, ensuring that data is safely shared and preventing race conditions.
To create a goroutine, you simply prefix a function call with the go
keyword. Here is a simple example:
package main
import (
"fmt"
"time"
)
func printNumbers() {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(i)
}
}
func main() {
go printNumbers()
// Do other work in the main goroutine
time.Sleep(1 * time.Second)
fmt.Println("Main goroutine exiting")
}
In this example, the printNumbers
function is run as a goroutine. The main goroutine continues to execute while the printNumbers
goroutine runs in the background.
To create a channel, you use the make
function. Here is an example of creating a channel and sending and receiving values:
package main
import "fmt"
func main() {
// Create a channel of integers
ch := make(chan int)
// Send a value to the channel
go func() {
ch <- 42
}()
// Receive a value from the channel
num := <-ch
fmt.Println(num)
}
In this example, we create an integer channel ch
. A goroutine sends the value 42
to the channel, and the main goroutine receives the value from the channel.
The producer - consumer pattern is a common concurrency pattern where one or more goroutines (producers) generate data and send it to a channel, and one or more other goroutines (consumers) receive the data from the channel and process it.
package main
import (
"fmt"
"time"
)
// Producer function
func producer(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(200 * time.Millisecond)
}
close(ch)
}
// Consumer function
func consumer(ch chan int) {
for num := range ch {
fmt.Println("Received:", num)
}
}
func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}
In this example, the producer
goroutine generates numbers and sends them to the channel, and the consumer
goroutine receives the numbers from the channel and prints them.
The fan - out pattern involves multiple goroutines reading from a single channel, while the fan - in pattern involves multiple channels sending data to a single channel.
package main
import (
"fmt"
)
// Fan - Out: Multiple goroutines read from a single channel
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d started job %d\n", id, j)
results <- j * 2
fmt.Printf("Worker %d finished job %d\n", id, j)
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
// Start up 3 workers
const numWorkers = 3
for w := 1; w <= numWorkers; w++ {
go worker(w, jobs, results)
}
// Send jobs to the jobs channel
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// Collect the results
for a := 1; a <= numJobs; a++ {
<-results
}
close(results)
}
When working with goroutines, it’s important to handle errors properly. One way to do this is to pass an error channel between goroutines.
package main
import (
"fmt"
"time"
)
func doWork(id int, errChan chan<- error) {
time.Sleep(200 * time.Millisecond)
err := fmt.Errorf("error from worker %d", id)
errChan <- err
}
func main() {
errChan := make(chan error)
go doWork(1, errChan)
err := <-errChan
fmt.Println(err)
}
Closing a channel indicates that no more values will be sent on it. You can use the range
keyword to iterate over the values in a channel until it is closed.
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 3; i++ {
ch <- i
}
close(ch)
}()
for num := range ch {
fmt.Println(num)
}
}
Goroutines and channels are powerful features in Go that make it easy to write concurrent programs. By understanding the fundamental concepts, usage methods, common practices, and best practices, you can leverage these features to build efficient and scalable applications. Goroutines allow you to run multiple tasks concurrently with minimal overhead, and channels provide a safe and efficient way to communicate and synchronize between goroutines.