Go: HTTP servers, context and concurrency
Seven Go snippets covering the standard-library way to build a service: a minimal HTTP server, graceful shutdown, request timeouts via context, concurrent work with goroutines and WaitGroup, UUID generation and line-by-line file reading.
Go's standard library covers more of a web service than most languages', which is why these snippets import almost nothing. The interesting part is not the syntax — it is the three or four decisions the library forces you to make explicitly.
The default server has no timeouts
http.ListenAndServe(":8080", nil) starts a server with ReadTimeout, WriteTimeout and IdleTimeout all set to zero, meaning no limit. A client that opens a connection and sends nothing holds a goroutine and a file descriptor indefinitely; enough of them and the process runs out of both. Constructing an http.Server explicitly so you can set the timeouts is not a refinement, it is the difference between a service that survives contact with the open internet and one that does not.
Graceful shutdown is why context exists
When the orchestrator sends SIGTERM, the naive response is to exit, which drops every in-flight request. Server.Shutdown instead stops accepting new connections and waits for the active ones to finish, bounded by a context deadline so a stuck handler cannot delay the shutdown forever.
That same context threads through everything else. An outbound HTTP call made with http.NewRequestWithContext inherits the deadline of the request that triggered it, so when the client disconnects the downstream call is cancelled too instead of continuing to burn a connection for an answer nobody will read. Context is not a request-scoped bag of values; it is the cancellation tree, and using it as the former is the most common misuse.
WaitGroup answers "when", not "what happened"
sync.WaitGroup tells you when a set of goroutines has finished. It does not collect their results and it does not surface their errors — a goroutine that panics takes down the whole process regardless of how carefully the caller waits. Results need a channel or a slice indexed by position, and errors need somewhere to go. The snippet here does both explicitly, because the version that ignores errors is the one that gets copied and then quietly loses failures.
The Go blog on context remains the clearest explanation of how cancellation is meant to propagate.
Reading files without loading them
bufio.Scanner reads line by line with a fixed buffer, so memory use is independent of file size. It has one sharp edge worth knowing: the default buffer is 64 KB, and a longer line makes the scan stop with an error rather than continue. For log processing that limit is usually fine; for anything with embedded JSON it is not.
HTTP Request with Timeout via Context (Go)
Sending HTTP requests in Go using context.WithTimeout to enforce network deadline limits and prevent dangling goroutines.
A pattern for executing external HTTP requests in Go safely using context.WithTimeout. Enforcing timeouts on all outbound requests prevents goroutines from hanging forever when external endpoints are slow or unresponsive.
package main
import (
"context"
"io"
"log"
"net/http"
"time"
)
func fetchExternalAPI() ([]byte, error) {
// 1. Initialize a context with a 3-second timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel() // Always defer cancel to release context resources
// 2. Build the HTTP request attached to the timeout context
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.github.com/users/octocat", nil)
if err != nil {
return nil, err
}
client := &http.Client{}
// 3. Dispatch the request
resp, err := client.Do(req)
if err != nil {
// If timeout deadline is reached, Do returns a context error
if ctx.Err() == context.DeadlineExceeded {
log.Println("HTTP request timed out after 3 seconds")
}
return nil, err
}
defer resp.Body.Close()
// 4. Read body content
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
func main() {
data, err := fetchExternalAPI()
if err != nil {
log.Fatalf("Failed to fetch API: %v", err)
}
log.Printf("Successfully fetched %d bytes", len(data))
}
Generate a UUID
Creates a random v4 UUID using only the standard library's crypto/rand — no external uuid package required.
How it works: 16 random bytes are read from crypto/rand, then the version (4) and variant bits are forced per RFC 4122 before formatting the bytes as the standard 8-4-4-4-12 hex groups.
package main
import (
"crypto/rand"
"fmt"
)
func generateUUID() (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]), nil
}
// Example usage
id, _ := generateUUID() // "3f9a1c2e-8b3d-4c1a-9f2e-6d8b1a4c7e9f"
Run tasks concurrently with goroutines
The standard pattern for launching multiple goroutines and waiting for all of them to finish, using sync.WaitGroup.
How it works: wg.Add(1) before launching each goroutine and defer wg.Done() inside it keep the counter balanced; passing url as the parameter u (rather than closing over the loop variable directly) avoids the classic Go bug where every goroutine ends up seeing the same, final value of the loop variable.
package main
import (
"fmt"
"sync"
)
func processURLs(urls []string) {
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
fmt.Println("fetched:", u)
}(url)
}
wg.Wait() // blocks until every goroutine has called wg.Done()
}
HTTP Server with Graceful Shutdown (Go)
Production-ready Go HTTP server template that intercepts SIGINT/SIGTERM signals to shut down cleanly without interrupting active client requests.
An implementation of a Go HTTP server implementing graceful shutdown. It catches operating system termination signals (such as Ctrl+C or Docker/Kubernetes termination commands) and wraps up all ongoing HTTP requests within a 10-second timeout window.
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Create channel to listen for OS signals
shutdownChan := make(chan os.Signal, 1)
signal.Notify(shutdownChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
// Start server in a background goroutine
go func() {
log.Printf("Starting server on %s", server.Addr)
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("ListenAndServe error: %v", err)
}
}()
// Block until a signal is received
sig := <-shutdownChan
log.Printf("Received signal: %v. Initiating graceful shutdown...", sig)
// Allocate 10 seconds timeout window for remaining requests
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Shutdown the server cleanly
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server stopped cleanly. Closing DB connections...")
// Free database connections or key-value store handlers here
}
Make an HTTP GET request
The standard-library way to fetch and parse JSON from an API — no third-party HTTP client needed.
How it works: http.Get performs the request, io.ReadAll drains the response body into a byte slice, and json.Unmarshal decodes it straight into a typed struct passed by pointer.
package main
import (
"encoding/json"
"io"
"net/http"
)
func httpGetJSON(url string, target interface{}) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
return json.Unmarshal(body, target)
}
// Example usage
type User struct {
Name string `json:"name"`
}
var user User
err := httpGetJSON("https://api.example.com/users/1", &user)
Read a file line by line
Stream a text file line by line without loading the whole file into memory.
How it works: bufio.Scanner reads the file in chunks and exposes one line at a time via Scan()/Text(), so memory use stays flat regardless of file size.
package main
import (
"bufio"
"fmt"
"os"
)
func readLines(path string) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
return scanner.Err()
}
Create a simple HTTP server
The standard-library way to serve HTTP routes in Go — no framework needed for a small API.
How it works: http.HandleFunc registers a handler for a path, and http.ListenAndServe starts the server and blocks — passing nil as the second argument tells it to use the default request multiplexer that HandleFunc registered routes on.
package main
import (
"encoding/json"
"log"
"net/http"
)
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}
func main() {
http.HandleFunc("/health", healthHandler)
log.Println("Listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
More snippet collections
Contact Information
Submit a request or write to me directly on Telegram. I'll reply within 24 hours.