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, andIdleTimeouton your server - Implement a
/healthendpoint for load balancer checks - Use structured logging (
log/slogin 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.