Go, also known as Golang, is a statically typed, compiled, and concurrent programming language designed at Google. One of its key features is built-in support for concurrency. In this article, we will explore the basics of concurrency and synchronization in Go.
Goroutines
In Go, concurrency is achieved through goroutines, which are lightweight threads managed by the Go runtime. They are extremely easy to work with compared to traditional threads.
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 3; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world") // running in a goroutine
say("hello") // running in the main thread
}In the example above, the function say is called as a goroutine by using the go keyword. While say("world") runs concurrently, the main function continues to execute and calls say("hello") concurrently.
Channels
Channels are the conduits through which goroutines communicate. You can send a value into a channel from one goroutine and receive that value in another goroutine.
package main
import (
"fmt"
)
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to channel c
}
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 // receive from c
fmt.Println(x, y, x+y)
}In this example, the sum function calculates the sum of integers in a slice and sends it to a channel c. The main function runs two goroutines that compute the sum concurrently and then receives their results from the channel.
Buffered Channels
Channels can be buffered, allowing sending and receiving to proceed independently up to a set maximum. These are declared by passing an additional capacity argument to the channel instantiation.
package main
import "fmt"
func main() {
c := make(chan int, 2) // buffered channel with a capacity of 2
c <- 1
c <- 2
fmt.Println(<-c)
fmt.Println(<-c)
}In a buffered channel, the sends do not block until the buffer is full. In the code example, the channel c can hold two values. The main goroutine sends value 1 and 2 to the channel before reading them.
Synchronization and WaitGroups
Go's sync package provides synchronization primitives, which include the WaitGroup to wait for a collection of goroutines to finish executing.
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
// simulate work
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait() // wait for all workers to finish
}In this example, the main function launches three worker goroutines and waits until they have completed using a WaitGroup.
Conclusion
Concurrency in Go makes it straightforward to run multiple tasks independently, leveraging goroutines and channels for efficient execution and communication. Synchronization can be effectively managed using channels and the sync package.