Skip to content
Home
Go Web Development: Building Modern Web Applications

Go Web Development: Building Modern Web Applications

Go Go 8 min read 1663 words Beginner ExcellentWiki Editorial Team

Go’s standard library provides everything you need to build production-quality web applications. The net/http package includes a robust HTTP server, request routing, static file serving, and cookie management. This guide covers the full spectrum of Go web development, from basic serving to advanced patterns.

The HTTP Server

Go’s http.Server is production-ready out of the box, but requires explicit configuration for production use.

Basic Server

package main

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

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
---

func main() {
    http.HandleFunc("/", helloHandler)
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
---

Production Server Configuration

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", helloHandler)

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

    // Graceful shutdown
    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("server error: %v", err)
        }
    }()

    // Wait for interrupt signal
    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("shutdown error: %v", err)
    }
---

The ReadTimeout and WriteTimeout are critical for production. Without them, slow clients can hold connections open indefinitely, exhausting server resources. Graceful shutdown ensures in-flight requests complete before the process exits.

Routing

Go 1.22 introduced method-based routing in the standard ServeMux:

mux := http.NewServeMux()

// Method-specific handlers
mux.HandleFunc("GET /api/items", listItems)
mux.HandleFunc("POST /api/items", createItem)
mux.HandleFunc("PUT /api/items/{id}", updateItem)
mux.HandleFunc("DELETE /api/items/{id}", deleteItem)

// Path value extraction
func updateItem(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // ...
---

For more complex routing requirements (subdomain routing, regex patterns, middleware chaining), third-party routers like chi or gorilla/mux remain popular. Chi is particularly well-regarded for its lightweight nature and compatibility with net/http.

Templating

Go’s html/template package generates HTML safely with automatic escaping.

Template Files

Create templates/base.html:

<!DOCTYPE html>
<html>
<head>
    <title>{{block "title" .}}Default Title{{end}}</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <header>
        <nav>
            <a href="/">Home</a>
            <a href="/items">Items</a>
        </nav>
    </header>
    <main>
        {{block "content" .}}{{end}}
    </main>
    <footer>
        <p>&copy; 2025 ExcellentWiki</p>
    </footer>
</body>
</html>

Create templates/items.html:

{{define "title"}}Items{{end}}

{{define "content"}}
<h1>Items</h1>
<ul>
    {{range .Items}}
    <li>{{.Name}} - ${{.Price}}</li>
    {{else}}
    <li>No items found</li>
    {{end}}
</ul>
{{end}}

Template Rendering

type Item struct {
    Name  string
    Price float64
---

type PageData struct {
    Items []Item
---

func renderTemplate(w http.ResponseWriter, tmpl string, data any) {
    templates := template.Must(template.ParseGlob("templates/*.html"))
    err := templates.ExecuteTemplate(w, tmpl, data)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
---

func itemsHandler(w http.ResponseWriter, r *http.Request) {
    data := PageData{
        Items: []Item{
            {Name: "Widget", Price: 9.99},
            {Name: "Gadget", Price: 24.99},
        },
    }
    renderTemplate(w, "items.html", data)
---

Template Functions

Add custom functions to templates:

func formatCurrency(amount float64) string {
    return fmt.Sprintf("$%.2f", amount)
---

func myFuncs() template.FuncMap {
    return template.FuncMap{
        "currency": formatCurrency,
    }
---

func init() {
    templates = template.New("").Funcs(myFuncs()).ParseGlob("templates/*.html")
---

Static Files

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

    // Serve static files with caching
    fileServer := http.FileServer(http.Dir("./static"))
    mux.Handle("GET /static/", cacheMiddleware(fileServer))

    // Specific static file
    mux.Handle("GET /favicon.ico", http.FileServer(http.Dir("./static")))

    log.Fatal(http.ListenAndServe(":8080", mux))
---

func cacheMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Cache-Control", "public, max-age=86400")
        w.Header().Set("ETag", `"v1"`)
        next.ServeHTTP(w, r)
    })
---

Session Management

Use secure, server-side sessions:

import "github.com/gorilla/sessions"

var store = sessions.NewCookieStore([]byte("super-secret-key-32-bytes!"))

func loginHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session-name")
    session.Values["user_id"] = 123
    session.Values["role"] = "admin"
    session.Save(r, w)
---

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        session, err := store.Get(r, "session-name")
        if err != nil || session.Values["user_id"] == nil {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
---

Sessions are stored in a signed cookie on the client. For distributed deployments, use a shared session store backed by Redis or PostgreSQL.

Middleware Patterns

Composable middleware via http.Handler wrapping:

func Chain(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middleware) - 1; i >= 0; i-- {
        h = middleware[i](h)
    }
    return h
---

// Usage
handler := Chain(
    itemsHandler,
    middleware.Recovery,
    middleware.Logging,
    middleware.Auth,
)

WebSockets and Real-Time Features

import "github.com/gorilla/websocket"

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    CheckOrigin: func(r *http.Request) bool {
        return true // restrict in production
    },
---

func wsHandler(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
        return
    }
    defer conn.Close()

    for {
        messageType, p, err := conn.ReadMessage()
        if err != nil {
            return
        }
        if err := conn.WriteMessage(messageType, p); err != nil {
            return
        }
    }
---

Deployment

Behind a Reverse Proxy

Go HTTP servers are designed to run behind Nginx, Caddy, or a cloud load balancer:

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
---

Health Checks

Expose a health endpoint for load balancers:

func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{
        "status": "healthy",
    })
---

Database Integration

Connect to databases for dynamic web applications:

import (
    "database/sql"
    _ "github.com/lib/pq"
    "net/http"
)

type App struct {
    db *sql.DB
---

func (app *App) userHandler(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    var name string
    err := app.db.QueryRow("SELECT name FROM users WHERE id = $1", id).Scan(&name)
    if err == sql.ErrNoRows {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    fmt.Fprintf(w, "User: %s", name)
---

func main() {
    db, _ := sql.Open("postgres", "postgres://user:pass@localhost/dbname")
    app := &App{db: db}
    mux := http.NewServeMux()
    mux.HandleFunc("GET /users/{id}", app.userHandler)
    http.ListenAndServe(":8080", mux)
---

Connection Pool Configuration

db.SetMaxOpenConns(25)       // max open connections
db.SetMaxIdleConns(10)       // max idle connections
db.SetConnMaxLifetime(5 * time.Minute)  // recycle connections

Form Handling

func formHandler(w http.ResponseWriter, r *http.Request) {
    if err := r.ParseForm(); err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }

    name := r.FormValue("name")
    email := r.FormValue("email")

    // Validate
    if name == "" {
        fmt.Fprintf(w, "Name is required")
        return
    }

    fmt.Fprintf(w, "Received: %s (%s)", name, email)
---

File Uploads

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    r.Body = http.MaxBytesReader(w, r.Body, 10<<20) // 10 MB limit

    if err := r.ParseMultipartForm(5 << 20); err != nil {
        http.Error(w, "File too large", http.StatusBadRequest)
        return
    }

    file, header, err := r.FormFile("file")
    if err != nil {
        http.Error(w, "No file provided", http.StatusBadRequest)
        return
    }
    defer file.Close()

    dst, _ := os.Create("/uploads/" + header.Filename)
    defer dst.Close()
    io.Copy(dst, file)

    fmt.Fprintf(w, "Uploaded: %s (%d bytes)", header.Filename, header.Size)
---

Advanced Patterns in Go

Context Propagation

Go’s context package is essential for managing timeouts, cancellations, and request-scoped values across API boundaries and goroutines:

func handler(ctx context.Context) error {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    result, err := queryDatabase(ctx)
    if err != nil {
        return fmt.Errorf("query failed: %w", err)
    }
    return result
---

Memory Management

Go’s garbage collector has been optimized significantly since version 1.5. The GC is concurrent and generational, with typical pause times under 1ms. For latency-critical applications, you can tune GC behavior with GOGC (target heap growth percentage) and GOMEMLIMIT (soft memory cap).

# Aggressive GC for latency-sensitive apps
export GOGC=50    # trigger GC when heap grows 50%
export GOMEMLIMIT=2GiB  # soft memory limit

Build Tags and Conditional Compilation

Build tags let you compile platform-specific or feature-flag code:

//go:build linux
// +build linux

package main

func getOS() string {
    return "linux"
---
go build -tags=debug,linux .

Pprof Profiling

import _ "net/http/pprof"

// Access profiles at /debug/pprof/
go tool pprof http://localhost:8080/debug/pprof/heap
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=30

FAQ

Q: Should I use a web framework in Go? A: The standard library suffices for most applications. Frameworks like Gin, Echo, or Fiber add convenience but are unnecessary for straightforward APIs.

Q: How do I handle file uploads? A: Use r.ParseMultipartForm and r.FormFile. Set MaxBytesReader on the request body to limit upload size.

Q: How do I serve Single Page Applications (SPAs)? A: Serve the static build directory and add a catch-all route that returns index.html for non-API paths.

Q: How do I run HTTPS in development? A: Use http.ListenAndServeTLS with self-signed certificates or tools like mkcert. For production, use a reverse proxy like Caddy that handles TLS automatically.

Q: How do I add request-scoped context? A: Use context.WithValue in middleware and pass it through the request’s r.Context(). Access it inside handlers with r.Context().Value(key).

Q: What ORM should I use? A: Go prefers SQL directly. Use database/sql with pgx or sqlx for convenience. Popular ORM-like tools include gorm and ent. Consider the trade-off: ORMs add abstraction but reduce control over query performance.

Q: How do I structure a larger web application? A: Follow the standard layout: separate handlers, models, services, middleware, and configuration into packages. Use dependency injection through constructor functions to wire them together.

For a comprehensive overview, read our article on Getting Started With Go.

For a comprehensive overview, read our article on Go Concurrency Guide.

Related Concepts and Further Reading

Understanding go web development requires familiarity with several interconnected ideas and principles that together form a complete picture. Exploring these related concepts deepens your knowledge and provides context that makes the core material more meaningful and applicable. Each concept builds on the others, creating a web of understanding that supports deeper learning and practical application. Taking time to explore how these elements connect reveals patterns that accelerate comprehension and retention of new information.

The relationship between go web development and adjacent fields is worth particular attention. Many of the most important insights emerge at the boundaries between disciplines, where ideas from different areas combine to create new approaches and solutions that neither field could produce alone. Exploring these connections pays dividends in both breadth and depth of understanding, revealing patterns and principles that might otherwise remain hidden from view. Cross-disciplinary knowledge is increasingly valued as problems become more complex and interconnected.

For those looking to go beyond introductory material, several excellent resources provide deeper treatment of specific aspects of go web development. Academic journals, industry publications, authoritative reference works, and online courses each offer different perspectives and levels of detail. The key is to match your reading to your current learning goals and build knowledge progressively, focusing on quality over quantity in your study materials. A well-chosen resource that matches your current level is worth more than dozens of resources that are too basic or too advanced.

Section: Go 1663 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top