A goroutine is a lightweight thread of execution. Unlike traditional threads, goroutines are managed by the Go runtime rather than the operating system. This makes them extremely efficient in terms of memory and context - switching overhead. Goroutines allow multiple functions to execute concurrently within the same address space.
package main
import (
"fmt"
"time"
)
func printNumbers() {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println("Number:", i)
}
}
func printLetters() {
for i := 'a'; i < 'e'; i++ {
time.Sleep(150 * time.Millisecond)
fmt.Println("Letter:", string(i))
}
}
func main() {
go printNumbers()
go printLetters()
// Wait for a while to let the goroutines finish
time.Sleep(1 * time.Second)
fmt.Println("Main function exiting")
}
In this example, printNumbers
and printLetters
are two functions that are executed concurrently as goroutines. The go
keyword is used to start a new goroutine.
Channels are a typed conduit through which you can send and receive values with the type - safe <-
operator. They provide a way to communicate and synchronize between goroutines.
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to 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)
}
Here, we use a channel c
to send the sum calculated by each goroutine back to the main function.
To create a goroutine, simply prefix a function call with the go
keyword. Managing goroutines often involves waiting for them to finish. One simple way, as shown in the previous example, is to use time.Sleep
. However, this is not a reliable method as it may cause the main function to exit before the goroutines are done or wait longer than necessary.
Channels can be created using the make
function. There are two types of channels: unbuffered and buffered.
// Unbuffered channel
unbuffered := make(chan int)
// Buffered channel with a capacity of 2
buffered := make(chan int, 2)
An unbuffered channel blocks the sending goroutine until there is a receiving goroutine ready to accept the value. A buffered channel allows you to send a certain number of values without blocking, up to its capacity.
The sync.WaitGroup
type is used to wait for a collection of goroutines to finish.
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("All workers finished")
}
In this example, we use a WaitGroup
to wait for all the worker goroutines to finish.
The select
statement is used to choose from multiple channel operations. It blocks until one of its cases can proceed.
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
}
The select
statement allows the main function to receive messages from either c1
or c2
as soon as they are available.
A race condition occurs when two or more goroutines access shared data concurrently, and at least one of the accesses is a write operation. To avoid race conditions, use synchronization mechanisms such as mutexes or channels.
package main
import (
"fmt"
"sync"
)
var (
counter int
mutex sync.Mutex
)
func increment(wg *sync.WaitGroup) {
defer wg.Done()
mutex.Lock()
counter++
mutex.Unlock()
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go increment(&wg)
}
wg.Wait()
fmt.Println("Counter:", counter)
}
In this example, we use a sync.Mutex
to ensure that only one goroutine can access the counter
variable at a time.
When working with concurrent applications, proper error handling is crucial. One way is to use channels to send errors back to the main goroutine.
package main
import (
"errors"
"fmt"
)
func doWork(id int, c chan error) {
if id%2 == 0 {
c <- nil
} else {
c <- errors.New("Error occurred in goroutine")
}
}
func main() {
c := make(chan error)
for i := 0; i < 5; i++ {
go doWork(i, c)
}
for i := 0; i < 5; i++ {
if err := <-c; err != nil {
fmt.Println(err)
}
}
}
Here, each goroutine sends an error (or nil
if no error) through the channel, and the main goroutine handles them.
Golang provides powerful and easy - to - use features for writing concurrent applications. Goroutines and channels are the building blocks that allow you to write efficient and scalable concurrent code. By understanding the fundamental concepts, usage methods, common practices, and best practices, you can write high - performance concurrent applications in Golang with confidence.