Building a Concurrent Go Web Server with Worker Pools

Building a Concurrent Go Web Server with Worker Pools

Go's implementation of web server already uses go routines on every request. So isn't it the best performance you can get ? Yes it is. But then what is the purpose of this article ?

This article provides a way for the web server to provide instant response but process the heavy work in the background using go routines. It is mostly useful, for resource heavy task to be done.

This repository contains a Golang implementation of a web server that utilizes concurrent workers to handle incoming requests. The server responds to a "/hello" endpoint with an "Accepted" status code and the message "Welcome."

You can use my Github project to check the code directly : github.com/go-workerpool-webserver

Features

  1. Concurrent Workers: The server employs a pool of worker goroutines (goroutines are lightweight threads in Go) to handle incoming requests concurrently. The number of worker go routines is defined by the workers variable.

  2. Request Handling: The "/hello" endpoint is defined in the handleHello function. When a request is received on this endpoint, the server responds with an "Accepted" status code and the message "Welcome." Additionally, a timestamp (in nanoseconds) is sent to a channel (ch1) to be processed by one of the worker goroutines.

  3. Worker Functionality: The workerFxn function represents the behaviour of the worker goroutines. Each worker waits for data from the ch1 channel, logs the received timestamp, and then simulates processing by sleeping for five seconds.


If you are new to Go and doesn't know how to run this project, then You can use my Github project to check the code directly : github.com/go-workerpool-webserver

Server

This will be responsible for handling requests

package main

import (
    "log"
    "net/http"
    "time"
)

var (
    // ch1 is a channel to send request IDs to workers
    // Type of channel is int 
    // This channel can only send and receive integers
    // If you want to send and receive any data type, use interface{}
    ch1=make(chan int)
    // workers is the number of concurrent workers to spawn
    workers=50    
)

// workerFxn is a function that runs in a goroutine and processes
func workerFxn(){
    // range over the channel to receive request IDs
    for i:=range ch1{
        log.Println(i)
        // Simulate some work by sleeping for 5 seconds
        time.Sleep(time.Second * 5)
    }

}

func handleHello(w http.ResponseWriter, r * http.Request){
    // Send a response back to the client    
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Welcome"))

    // Send the request ID to the channel
    go func(){    
        // Sending the current time through the channel to process
        // You can send any data type through the channel as long as the
        // receiving end is expecting the same data type
        // In this case, we are sending the current time in nanoseconds

        ch1<-time.Now().Nanosecond()
    }()
}


func main(){

    // Spawn the workers
    for i:=0;i<workers;i++{
        go workerFxn()
    }

    // Register the handler function
    http.HandleFunc("/hello",handleHello)

    // Start the server
    log.Println("Starting server in 8080..")
    err:=http.ListenAndServe(":8080",nil)
    if err!=nil{
        log.Println("Error running on port 8080")
    }

}

Client

Use the client to generate 10K Get requests to the endpoint to test the server

package main

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

const (
    // url is the URL of the server to send requests to
    url = "http://localhost:8080/hello"
    // totalRequests is the number of requests to send
    totalRequests = 10000
)

func main() {
    // Create a WaitGroup to wait for all requests to finish
    var wg sync.WaitGroup

    // Send totalRequests number of requests
    for i := 1; i <= totalRequests; i++ {
        wg.Add(1)
        go sendRequest(i, &wg)
    }

    // Wait for all requests to finish
    wg.Wait()

    fmt.Println("All requests completed.")
}

func sendRequest(requestID int, wg *sync.WaitGroup) {
    // Defer the call to Done to mark this function as finished
    defer wg.Done()

    // Send a GET request to the server
    resp, err := http.Get(url)
    if err != nil {
        fmt.Printf("Request %d failed: %v\n", requestID, err)
        return
    }

    fmt.Printf("Request %d completed %s\n", requestID,resp.Status)
}

Did you find this article valuable?

Support Aswin Benny by becoming a sponsor. Any amount is appreciated!