How to Build a Web Server in Go: A Practical Guide

We earn commissions when you shop through the links below.

If you’ve been curious about Go’s reputation for performance and simplicity, learning how to build a web server in Go is the perfect starting point. Go’s standard library ships with a powerful net/http package that lets you spin up a production-ready HTTP server in under 20 lines — no frameworks required. In this guide, I’ll walk you through everything from a basic hello-world server to structured routing, middleware, and JSON APIs.

Why Go for Web Servers?

Go compiles to a single binary, handles concurrency natively with goroutines, and has a tiny memory footprint compared to Node.js or Java. Benchmarks consistently show Go servers outperforming interpreted languages by a significant margin under load. If you’re building microservices, internal tooling, or a lean REST API, Go is a smart choice.

Prerequisites

  • Go installed (1.21 or later — check with go version)
  • Basic familiarity with Go syntax
  • A terminal and any text editor (I use Cursor for Go development — the AI completions handle boilerplate fast)

Step 1: The Minimal Web Server

Here’s the simplest possible Go web server:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, World!")
    })

    fmt.Println("Server running on :8080")
    http.ListenAndServe(":8080", nil)
}

Run it with go run main.go and hit http://localhost:8080. That’s it — no third-party dependencies, no config files. This is the foundation of how to build a web server in Go.

Step 2: Structured Routing with a ServeMux

Using the default mux works for demos, but you’ll want explicit routing for real apps. Go 1.22 improved the built-in ServeMux to support method-based routing and path parameters without a library:

package main

import (
    "encoding/json"
    "net/http"
    "log"
)

type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("GET /users", listUsers)
    mux.HandleFunc("GET /users/{id}", getUser)
    mux.HandleFunc("POST /users", createUser)

    log.Println("Listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

func listUsers(w http.ResponseWriter, r *http.Request) {
    users := []User{
        {ID: "1", Name: "Alice"},
        {ID: "2", Name: "Bob"},
    }
    writeJSON(w, http.StatusOK, users)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    user := User{ID: id, Name: "Alice"}
    writeJSON(w, http.StatusOK, user)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var user User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "bad request", http.StatusBadRequest)
        return
    }
    writeJSON(w, http.StatusCreated, user)
}

func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}

The r.PathValue("id") call is native Go — no Gorilla Mux or Chi needed for basic path parameters anymore.

Step 3: Adding Middleware

Middleware in Go is just a function that wraps an http.Handler. Here’s a logging middleware pattern that works with any handler:

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
    })
}

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token != "Bearer secret" {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Wrap the mux when starting the server:
// http.ListenAndServe(":8080", loggingMiddleware(authMiddleware(mux)))

Stack middleware by wrapping handlers. This pattern is idiomatic Go — simple, composable, and doesn’t require a framework.

Step 4: Handling Configuration and Graceful Shutdown

Production servers need graceful shutdown so in-flight requests aren’t dropped when you redeploy:

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("ok"))
    })

    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    go func() {
        log.Println("Starting server on :8080")
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("Server error: %v", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("Graceful shutdown failed: %v", err)
    }
    log.Println("Server stopped")
}

Setting explicit timeouts on your http.Server is non-negotiable in production — the defaults are zero, meaning no timeout at all.

Should You Use a Framework?

For most projects, the standard library is enough. But if you're building something large, a few lightweight routers are worth knowing:

  • Chi — idiomatic, middleware-friendly, zero dependencies
  • Gin — fast, batteries-included, great for REST APIs
  • Echo — clean API, good OpenAPI support

I'd still recommend starting without a framework. Once you understand how to build a web server in Go from scratch, picking up any of these takes about an hour.

Deploying Your Go Server

Go compiles to a static binary, which makes deployment straightforward. Build for Linux with:

GOOS=linux GOARCH=amd64 go build -o server ./cmd/server

Copy the binary to your server and run it. For managed hosting, Railway supports Go out of the box — push your code and it detects the Go project, builds it, and handles TLS automatically. For a VPS where you want full control, DigitalOcean Droplets are a solid choice — spin up a $6/month instance, copy your binary, run it behind Nginx, and you're done.

Quick Production Checklist

  • Set ReadTimeout, WriteTimeout, and IdleTimeout on your server
  • Implement a /health endpoint for load balancer checks
  • Use structured logging (log/slog in Go 1.21+)
  • Implement graceful shutdown
  • Run behind a reverse proxy (Nginx or Caddy) for TLS termination
  • Use environment variables for config — never hardcode secrets

Wrapping Up

Knowing how to build a web server in Go is one of those skills that pays dividends across many projects. The standard library gives you everything you need for a production server — fast, concurrent, and easy to reason about. Start with the minimal example, layer in routing and middleware as your app grows, and deploy a single binary with no runtime dependencies. Go's simplicity is the feature.