Shoulder.dev Logo Shoulder.dev

Concurrency in Go: Building Parallel and Concurrent Systems

Scenario: In today's world, building efficient and concurrent systems is crucial for handling large workloads and improving application performance. In this scenario, we will explore how to use Goroutines, channels, and synchronization primitives to build concurrent systems using the Go programming language.

Solution:

First, let's understand the basics of concurrency in Go. Concurrency is the ability of a system to handle multiple tasks at the same time. Go provides several features to support concurrency, including Goroutines, channels, and synchronization primitives.

  1. Goroutines: Goroutines are lightweight threads managed by the Go runtime. They are created using the go keyword followed by the function name. For example:
func main() {
    go func() {
        // Your code here
    }()
}
  1. Channels: Channels are used to communicate between Goroutines. They provide a way to send and receive data. Channels are bidirectional, meaning they can be used for both sending and receiving data. For example:
func main() {
    msg := make(chan string)

    go func() {
        msg <- "Hello, World!"
        close(msg)
    }()

    println(<-msg)
}
  1. Synchronization primitives: Synchronization primitives are used to control access to shared resources. Go provides several synchronization primitives, including mutexes, RWMutexes, and condition variables. For example:
type Counter struct {
    v   int
    mux sync.Mutex
}

func (c *Counter) Inc() {
    c.mux.Lock()
    defer c.mux.Unlock()
    c.v++
}

Now, let's build a simple concurrent system using these concepts. We will create a web server that can handle multiple requests concurrently.

First, let's create the project structure:

concurrency-go/
├── api/
│   ├── next/
│   │   ├── 42888.txt
│   │   ├── 61696.txt
│   │   └── ...
│   ├── README
│   ├── except.txt
│   ├── go1.1.txt
│   ├── go1.10.txt
│   ├── go1.11.txt
│   ├── go1.12.txt
│   ├── go1.13.txt
│   ├── go1.14.txt
│   ├── go1.15.txt
│   ├── go1.16.txt
│   ├── go1.17.txt
│   ├── go1.18.txt
│   ├── go1.19.txt
│   ├── go1.2.txt
│   ├── go1.20.txt
│   ├── go1.21.txt
│   ├── go1.22.txt
│   ├── go1.3.txt
│   ├── go1.4.txt
│   ├── go1.5.txt
│   ├── go1.6.txt
│   ├── go1.7.txt
│   ├── go1.8.txt
│   ├── go1.9.txt
│   └── go1.txt
│   └── main.go
│   └── server.go
│   └── test/
│       └── ...
└── doc/
    ├── initial/
    │   ├── 1-intro.md
    │   ├── 2-language.md
    │   ├── 3-tools.md
    │   ├── 4-runtime.md
    │   ├── 5-toolchain.md
    │   ├── 6-stdlib/
    │   │   ├── asm.html
    │   │   ├── go1.17_spec.html
    │   │   ├── go_mem.html
    │   │   ├── go_spec.html
    │   │   ├── godebug.md
    │   │   └── ...
    │   └── ...
    ├── next/
    │   ├── 1-intro.md
    │   ├── 2-language.md
    │   ├── 3-tools.md
    │   ├── 4-runtime.md
    │   ├── 5-toolchain.md
    │   ├── 6-stdlib/
    │   │   ├── asm.html
    │   │   ├── go1.17_spec.html
    │   │   ├── go_mem.html
    │   │   ├── go_spec.html
    │   │   ├── godebug.md
    │   │   └── ...
    │   └── ...
    ├── README.md
    └── ...
└── go.mod
└── go.sum
└── main.go
└── server.go
└── test/
    └── ...

Next, let's create the main.go file to start the server:

package main

import (
    "fmt"
    "net/http"
    "sync"
)

type Counter struct {
    v   int
    mux sync.Mutex
}

func (c *Counter) Inc() {
    c.mux.Lock()
    defer c.mux.Unlock()
    c.v++
}

func handler(w http.ResponseWriter, r *http.Request) {
    counter.Inc()
    fmt.Fprintf(w, "Hello, World!")
}

var counter Counter
var wg sync.WaitGroup

func main() {
    http.HandleFunc("/", handler)

    wg.Add(1)
    go func() {
        err := http.ListenAndServe(":8080", nil)
        if err != nil {
            panic(err)
        }
        wg.Done()
    }()

    wg.Wait()
}

Finally, let's create the server.go file to handle multiple requests concurrently:

package main

import (
    "fmt"
    "net/http"
    "sync"
)

type Counter struct {
    v   int
    mux sync.Mutex
}

func (c *Counter) Inc() {
    c.mux.Lock()
    defer c.mux.Unlock()
    c.v++
}

type Response struct {
    w   http.ResponseWriter
    err error
}

func handler(w http.ResponseWriter, r *http.Request) {
    counter.Inc()
    fmt.Fprintf(w, "Hello, World!")
}

var counter Counter
var wg sync.WaitGroup

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        wg.Add(1)
        go func() {
            handler(w, r)
            wg.Done()
        }()
    })

    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        panic(err)
    }

    wg.Wait()
}

Now, let's write some tests to verify the answer:

  1. Test the server can handle multiple requests concurrently:
func TestConcurrency(t *testing.T) {
    client := &http.Client{}

    for i := 0; i < 10; i++ {
        go func() {
            resp, err := client.Get("http://localhost:8080")
            if err != nil {
                t.Fatalf("Failed to get response: %v", err)
            }
            body, err := ioutil.ReadAll(resp.Body)
            if err != nil {
                t.Fatalf("Failed to read response body: %v", err)
            }
            if string(body) != "Hello, World!" {
                t.Fatalf("Unexpected response: %s", string(body))
            }
            resp.Body.Close()
        }