A goroutine is a lightweight thread of execution. Unlike traditional threads, which are managed by the operating system and have a relatively large memory footprint, goroutines are managed by the Go runtime. They are extremely lightweight, allowing you to create thousands or even millions of goroutines in a single program.
Here is a simple example to demonstrate the creation of a goroutine:
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("Hello from goroutine!")
}
func main() {
go hello()
time.Sleep(1 * time.Second)
fmt.Println("Hello from main function!")
}
In this example, the hello
function is executed as a goroutine using the go
keyword. The main function continues its execution while the goroutine runs concurrently. We use time.Sleep
to ensure that the main function doesn’t exit before the goroutine has a chance to execute.
Channels are used for communication and synchronization between goroutines. They provide a typed conduit through which you can send and receive values with the type - safety guarantee. Channels are declared using the chan
keyword.
Here is a basic example of using a channel:
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c
fmt.Println(x, y, x+y)
}
In this code, we create a channel c
of type int
. Two goroutines are launched to calculate the sum of different parts of the slice s
. The results are sent to the channel using the <-
operator in the sum
function. In the main function, we receive the results from the channel and print them.
To create a goroutine, simply prefix a function call with the go
keyword. You can also pass arguments to the function just like a normal function call.
package main
import (
"fmt"
"time"
)
func printNumbers(n int) {
for i := 0; i < n; i++ {
fmt.Println(i)
time.Sleep(100 * time.Millisecond)
}
}
func main() {
go printNumbers(5)
time.Sleep(1 * time.Second)
fmt.Println("Main function is done.")
}
In this example, the printNumbers
function is executed as a goroutine. The main function continues its execution while the goroutine prints numbers.
As shown in the previous example, channels can be used to send and receive data between goroutines. You can also create buffered channels, which can hold a certain number of values without blocking the sender.
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
Here, we create a buffered channel with a capacity of 2. We can send two values to the channel without blocking. Then we receive and print the values from the channel.
The producer - consumer pattern is a common design pattern in concurrent programming. The producer goroutine generates data and sends it to a channel, while the consumer goroutine receives the data from the channel and processes it.
package main
import (
"fmt"
"time"
)
func producer(ch chan int) {
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(200 * time.Millisecond)
}
close(ch)
}
func consumer(ch chan int) {
for num := range ch {
fmt.Println("Received:", num)
}
}
func main() {
ch := make(chan int)
go producer(ch)
go consumer(ch)
time.Sleep(2 * time.Second)
}
In this example, the producer
function sends integers to the channel, and the consumer
function receives and prints them. The close
function is used to signal that no more values will be sent on the channel.
The fan - out pattern involves multiple goroutines reading from a single channel and processing the data independently. The fan - in pattern combines the results from multiple channels into a single channel.
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d started job %d\n", id, j)
time.Sleep(time.Second)
fmt.Printf("Worker %d finished job %d\n", id, j)
results <- j * 2
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= numJobs; a++ {
<-results
}
close(results)
}
In this code, multiple worker
goroutines (fan - out) receive jobs from the jobs
channel and send the results to the results
channel. The main function distributes the jobs and collects the results.
When working with concurrent code, error handling becomes more complex. One approach is to use channels to send errors along with the data.
package main
import (
"fmt"
)
func divide(a, b int, resultChan chan int, errChan chan error) {
if b == 0 {
errChan <- fmt.Errorf("division by zero")
return
}
resultChan <- a / b
}
func main() {
resultChan := make(chan int)
errChan := make(chan error)
go divide(10, 2, resultChan, errChan)
select {
case result := <-resultChan:
fmt.Println("Result:", result)
case err := <-errChan:
fmt.Println("Error:", err)
}
}
Here, we use two channels: one for the result and one for the error. The select
statement is used to handle either the result or the error.
A race condition occurs when two or more goroutines access shared data concurrently, and at least one of them modifies the data. To avoid race conditions, you can use synchronization primitives like mutexes.
package main
import (
"fmt"
"sync"
)
var (
counter int
mutex sync.Mutex
)
func increment() {
mutex.Lock()
counter++
mutex.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
increment()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
In this example, we use a sync.Mutex
to ensure that only one goroutine can access and modify the counter
variable at a time.
Golang’s concurrency model provides a powerful and elegant way to write concurrent programs. Goroutines and channels simplify the process of handling multiple tasks concurrently, and the built - in synchronization primitives help in avoiding common pitfalls like race conditions. By understanding the fundamental concepts, usage methods, common practices, and best practices, you can start writing efficient and reliable concurrent Go programs.