One of the key features that sets Go apart from other languages is its support for concurrency through goroutines — a way of creating lightweight threads that can run simultaneously. 1
Known for its simplicity and ease-of-use, Go has quickly become a go-to for developers looking to create concurrent programs.
Golang concurrency is no stranger to the world of programming, but what makes it such a hot topic? With the rise of multithreading and parallel processing, mastering concurrency in Golang has become more relevant than ever.
The lightweight threads provide numerous advantages over traditional threads, such as lower memory overhead and better scalability.
Goroutines are lightweight threads that run concurrently within the same address space.
Synchronization with channels in Golang refers to controlling the flow of data between goroutines by establishing channels or data pipelines.
Channel
Using channels for synchronization and data exchange
Channels in Golang allow goroutines to synchronize and exchange data, providing a simple and safe way to coordinate concurrent work.
In this example, a buffered channel done signals when the doWork function has completed execution. The main function waits for both workers to finish before exiting.
package main
import (
  "fmt"
  "time"
)
func main() {
  done := make(chan bool, 2)
  go doWork(1, done)
  go doWork(2, done)
  <-done
  <-done
  fmt.Println("vim-go")
}
func doWork(id int, done chan bool) {
  fmt.Printf("Worker %d started\n", id)
  time.Sleep(time.Second)
  fmt.Printf("Worker %d finished\n", id)
  done <- true
}
Context
Using context.Context for cancelation and deadlines
The context.Context package can be used to handle cancelation and deadlines in concurrent programs.
The context.Context manages cancelation for the doWorkWithContext function.
package main
import (
  "context"
  "fmt"
  "time"
)
func main() {
  ctx, cancel := context.WithCancel(context.Background())
  defer cancel()
  go doWorkWithContext(ctx, 1)
  go doWorkWithContext(ctx, 2)
  time.Sleep(2000 * time.Millisecond)
  cancel()
  time.Sleep(2 * time.Second)
  fmt.Println("vim-go")
}
func doWorkWithContext(ctx context.Context, id int) {
  select {
  case <-time.After(2 * time.Duration(id) * time.Second):
    fmt.Printf("Worker %d completed\n", id)
  case <-ctx.Done():
    fmt.Printf("Worker %d canceled\n", id)
  }
}
Mutex
Using Mutexes to protect shared data
Mutexes can be used to protect shared data from concurrent access, preventing race conditions.
This example demonstrates using a mutex to protect the value field in the Counter struct. The Increment method locks the mutex before modifying the shared data and unlocks it afterward.
package main
import (
  "fmt"
  "sync"
)
func main() {
  counter := &Counter{}
  // without the WaitGroup, it ends without counting till 1000
  var wg sync.WaitGroup
  for i := 0; i < 1000; i++ {
    wg.Add(1)
    go func() {
      counter.Increment()
      wg.Done()
    }()
  }
  wg.Wait()
  fmt.Printf("Counter value: %d\n", counter.value)
  fmt.Println("vim-go")
}
type Counter struct {
  value int
  mu    sync.Mutex
}
func (c *Counter) Increment() {
  c.mu.Lock()
  defer c.mu.Unlock()
  c.value++
}
Select
Using select for multiple channel operations
The select statement allows a goroutine to perform multiple channel operations, choosing the first one that is ready.
In this example, the select statement waits for either a timeout or a tick. The main function exits when the timeout is reached, and ticks are printed every 200 milliseconds until then.
package main
import (
  "fmt"
  "time"
)
func main() {
  timeout := time.After(1 * time.Second)
  tick := time.Tick(200 * time.Millisecond)
  for {
    select {
    case <-timeout:
      fmt.Println("Timeout reached")
      return
    case <-tick:
      fmt.Println("Tick")
    }
  }
  fmt.Println("vim-go")
}