back

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")
}