Building Web Servers in Go

Building Web Servers in Go

Go is an excellent choice for building web servers due to its built-in net/http package and impressive performance. This guide will take you from creating a basic HTTP server to building a complete web application with middleware, routing, and database integration.

Why Go for Web Development?

  • Performance: Compiled to native code, excellent concurrency support
  • Simplicity: Clean syntax and standard library batteries
  • Concurrency: Goroutines make handling many connections easy
  • Static Binaries: Single executable with no external dependencies
  • Cross-platform: Build for any platform from any machine

Basic HTTP Server

Let’s start with the simplest possible web server:

package main

import (
    "fmt"
    "net/http"
)

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

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

Save this as main.go and run it:

go run main.go

Now visit http://localhost:8080 in your browser. You’ll see “Hello, World!”.

Understanding Handlers

In Go, web servers use handlers to process HTTP requests. A handler is any type that implements the http.Handler interface:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Handler Functions

The most common way to create handlers is using functions with the signature func(http.ResponseWriter, *http.Request):

package main

import (
    "fmt"
    "net/http"
)

// Handler function for the home page
func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome to the Home Page!")
}

// Handler function for about page
func aboutHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "This is the About Page")
}

func main() {
    // Register handlers
    http.HandleFunc("/", homeHandler)
    http.HandleFunc("/about", aboutHandler)

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

Struct-Based Handlers

For more complex applications, struct-based handlers provide better organization:

package main

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

// Server represents our web server
type Server struct {
    Port   string
    Counter int
}

// ServeHTTP implements the http.Handler interface
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.URL.Path {
    case "/":
        s.homeHandler(w, r)
    case "/count":
        s.countHandler(w, r)
    case "/increment":
        s.incrementHandler(w, r)
    default:
        http.NotFound(w, r)
    }
}

func (s *Server) homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Counter Value: %d", s.Counter)
    fmt.Fprintf(w, "\nVisit /count to see the counter")
    fmt.Fprintf(w, "\nVisit /increment to increment it")
}

func (s *Server) countHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Current count: %d", s.Counter)
}

func (s *Server) incrementHandler(w http.ResponseWriter, r *http.Request) {
    s.Counter++
    fmt.Fprintf(w, "Count incremented to: %d", s.Counter)
}

func main() {
    server := &Server{Port: ":8080", Counter: 0}
    
    fmt.Printf("Server starting on port %s...\n", server.Port)
    http.ListenAndServe(server.Port, server)
}

Working with HTTP Methods

Web applications need to handle different HTTP methods (GET, POST, PUT, DELETE, etc.):

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "strings"
)

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

var users []User
var nextID int = 1

func userHandler(w http.ResponseWriter, r *http.Request) {
    // Extract user ID from URL path
    path := strings.TrimPrefix(r.URL.Path, "/users/")
    
    if path == "" {
        // No ID specified - list users or create new user
        switch r.Method {
        case http.MethodGet:
            getUsers(w, r)
        case http.MethodPost:
            createUser(w, r)
        default:
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        }
    } else {
        // ID specified - get, update, or delete specific user
        switch r.Method {
        case http.MethodGet:
            getUser(w, r, path)
        case http.MethodPut:
            updateUser(w, r, path)
        case http.MethodDelete:
            deleteUser(w, r, path)
        default:
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        }
    }
}

func getUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var user User
    
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    user.ID = nextID
    nextID++
    users = append(users, user)
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func getUser(w http.ResponseWriter, r *http.Request, idStr string) {
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid user ID", http.StatusBadRequest)
        return
    }
    
    for _, user := range users {
        if user.ID == id {
            w.Header().Set("Content-Type", "application/json")
            json.NewEncoder(w).Encode(user)
            return
        }
    }
    
    http.Error(w, "User not found", http.StatusNotFound)
}

func updateUser(w http.ResponseWriter, r *http.Request, idStr string) {
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid user ID", http.StatusBadRequest)
        return
    }
    
    var updatedUser User
    if err := json.NewDecoder(r.Body).Decode(&updatedUser); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    for i, user := range users {
        if user.ID == id {
            updatedUser.ID = id
            users[i] = updatedUser
            w.Header().Set("Content-Type", "application/json")
            json.NewEncoder(w).Encode(updatedUser)
            return
        }
    }
    
    http.Error(w, "User not found", http.StatusNotFound)
}

func deleteUser(w http.ResponseWriter, r *http.Request, idStr string) {
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid user ID", http.StatusBadRequest)
        return
    }
    
    for i, user := range users {
        if user.ID == id {
            users = append(users[:i], users[i+1:]...)
            w.WriteHeader(http.StatusNoContent)
            return
        }
    }
    
    http.Error(w, "User not found", http.StatusNotFound)
}

func main() {
    // Initialize with sample data
    users = append(users, User{ID: nextID, Name: "John Doe", Email: "[email protected]"})
    nextID++
    
    http.HandleFunc("/users", userHandler)
    http.HandleFunc("/users/", userHandler)
    
    fmt.Println("Server starting on port 8080...")
    fmt.Println("Available endpoints:")
    fmt.Println("  GET    /users        - List all users")
    fmt.Println("  POST   /users        - Create new user")
    fmt.Println("  GET    /users/{id}   - Get user by ID")
    fmt.Println("  PUT    /users/{id}   - Update user")
    fmt.Println("  DELETE /users/{id}   - Delete user")
    
    http.ListenAndServe(":8080", nil)
}

Adding Middleware

Middleware wraps handlers to add cross-cutting concerns like logging, authentication, and CORS.

Basic Middleware Pattern

package main

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

// Logging middleware
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Call the next handler
        next.ServeHTTP(w, r)
        
        // Log the request
        log.Printf(
            "%s %s %s %v",
            r.Method,
            r.RequestURI,
            r.RemoteAddr,
            time.Since(start),
        )
    })
}

// CORS middleware
func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Set CORS headers
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        
        // Handle preflight requests
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }
        
        next.ServeHTTP(w, r)
    })
}

// Authentication middleware
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Skip authentication for health check
        if r.URL.Path == "/health" {
            next.ServeHTTP(w, r)
            return
        }
        
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Authorization header required", http.StatusUnauthorized)
            return
        }
        
        // Simple token validation (in production, use proper JWT validation)
        if token != "Bearer valid-token" {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }
        
        next.ServeHTTP(w, r)
    })
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome to the protected home page!")
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "OK")
}

func main() {
    // Create routes
    mux := http.NewServeMux()
    mux.HandleFunc("/", homeHandler)
    mux.HandleFunc("/health", healthHandler)
    
    // Apply middleware
    handler := loggingMiddleware(corsMiddleware(authMiddleware(mux)))
    
    fmt.Println("Server starting on port 8080...")
    http.ListenAndServe(":8080", handler)
}

Custom Middleware Chain

For more complex applications, you might want a middleware chain system:

package main

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

// Middleware type
type Middleware func(http.Handler) http.Handler

// Chain creates a middleware chain
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

// Request ID middleware
func requestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = fmt.Sprintf("%d", time.Now().UnixNano())
        }
        
        w.Header().Set("X-Request-ID", requestID)
        next.ServeHTTP(w, r)
    })
}

// Content type middleware
func jsonMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        next.ServeHTTP(w, r)
    })
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
    response := map[string]interface{}{
        "message": "Hello from API",
        "method":  r.Method,
        "path":    r.URL.Path,
    }
    
    fmt.Fprintf(w, `{"message":"Hello from API","method":"%s","path":"%s"}`, r.Method, r.URL.Path)
}

func main() {
    handler := Chain(
        http.HandlerFunc(apiHandler),
        requestIDMiddleware,
        jsonMiddleware,
    )
    
    fmt.Println("Server starting on port 8080...")
    http.ListenAndServe(":8080", handler)
}

Template Rendering

Go includes a powerful template engine for generating HTML responses.

Basic Template Usage

package main

import (
    "html/template"
    "net/http"
)

type PageData struct {
    Title   string
    Message string
    Items   []string
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    data := PageData{
        Title:   "Welcome Page",
        Message: "Welcome to our website!",
        Items:   []string{"Go", "Web Development", "Templates"},
    }
    
    tmpl, err := template.ParseFiles("templates/layout.html", "templates/home.html")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    err = tmpl.ExecuteTemplate(w, "layout", data)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

func main() {
    http.HandleFunc("/", homeHandler)
    http.ListenAndServe(":8080", nil)
}

Create template files:

templates/layout.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{.Title}}</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; }
        .container { max-width: 800px; margin: 0 auto; }
        .nav { background: #f4f4f4; padding: 10px; margin-bottom: 20px; }
        .nav a { margin-right: 20px; text-decoration: none; }
    </style>
</head>
<body>
    <div class="container">
        <nav class="nav">
            <a href="/">Home</a>
            <a href="/about">About</a>
        </nav>
        {{template "content" .}}
    </div>
</body>
</html>

templates/home.html:

{{define "content"}}
    <h1>{{.Message}}</h1>
    <h2>Featured Topics:</h2>
    <ul>
        {{range .Items}}
            <li>{{.}}</li>
        {{end}}
    </ul>
{{end}}

Template Functions

You can add custom functions to your templates:

package main

import (
    "html/template"
    "net/http"
    "strings"
    "time"
)

// Custom template functions
var templateFunctions = template.FuncMap{
    "upper": strings.ToUpper,
    "lower": strings.ToLower,
    "formatDate": func(t time.Time) string {
        return t.Format("January 2, 2006")
    },
}

type Article struct {
    Title     string
    Content   string
    Author    string
    Published time.Time
    Tags      []string
}

func loadTemplates() *template.Template {
    return template.Must(
        template.New("").Funcs(templateFunctions).ParseGlob("templates/*.html"),
    )
}

func articleHandler(w http.ResponseWriter, r *http.Request) {
    article := Article{
        Title:     "Getting Started with Go Web Development",
        Content:   "Go is a powerful language for building web applications...",
        Author:    "John Doe",
        Published: time.Now(),
        Tags:      []string{"go", "web", "tutorial"},
    }
    
    tmpl := loadTemplates()
    err := tmpl.ExecuteTemplate(w, "article", article)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

func main() {
    http.HandleFunc("/article", articleHandler)
    http.ListenAndServe(":8080", nil)
}

templates/article.html:

{{define "content"}}
    <article>
        <h1>{{.Title | upper}}</h1>
        <div class="meta">
            By {{.Author}} on {{formatDate .Published}}
        </div>
        <div class="tags">
            Tags: 
            {{range .Tags}}
                <span class="tag">{{. | lower}}</span>
            {{end}}
        </div>
        <div class="content">
            <p>{{.Content}}</p>
        </div>
    </article>
{{end}}

Database Integration

Let’s integrate a database using a simple in-memory store pattern.

Repository Pattern

package main

import (
    "encoding/json"
    "net/http"
    "sync"
    "time"
)

type Post struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Content   string    `json:"content"`
    Author    string    `json:"author"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

type PostRepository struct {
    posts []Post
    mu    sync.RWMutex
    nextID int
}

func NewPostRepository() *PostRepository {
    return &PostRepository{
        posts: make([]Post, 0),
        nextID: 1,
    }
}

func (r *PostRepository) GetAll() []Post {
    r.mu.RLock()
    defer r.mu.RUnlock()
    return r.posts
}

func (r *PostRepository) GetByID(id int) (*Post, bool) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    
    for _, post := range r.posts {
        if post.ID == id {
            return &post, true
        }
    }
    return nil, false
}

func (r *PostRepository) Create(post *Post) *Post {
    r.mu.Lock()
    defer r.mu.Unlock()
    
    post.ID = r.nextID
    post.CreatedAt = time.Now()
    post.UpdatedAt = time.Now()
    r.posts = append(r.posts, *post)
    r.nextID++
    
    return post
}

func (r *PostRepository) Update(id int, updated *Post) (*Post, bool) {
    r.mu.Lock()
    defer r.mu.Unlock()
    
    for i, post := range r.posts {
        if post.ID == id {
            updated.ID = id
            updated.CreatedAt = post.CreatedAt
            updated.UpdatedAt = time.Now()
            r.posts[i] = *updated
            return updated, true
        }
    }
    return nil, false
}

func (r *PostRepository) Delete(id int) bool {
    r.mu.Lock()
    defer r.mu.Unlock()
    
    for i, post := range r.posts {
        if post.ID == id {
            r.posts = append(r.posts[:i], r.posts[i+1:]...)
            return true
        }
    }
    return false
}

// HTTP Handlers
func (r *PostRepository) getPostsHandler(w http.ResponseWriter, r *http.Request) {
    posts := r.GetAll()
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(posts)
}

func (r *PostRepository) createPostHandler(w http.ResponseWriter, r *http.Request) {
    var post Post
    if err := json.NewDecoder(r.Body).Decode(&post); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    
    created := r.Create(&post)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(created)
}

func (r *PostRepository) getPostHandler(w http.ResponseWriter, r *http.Request) {
    id := extractIDFromURL(r.URL.Path)
    if id == -1 {
        http.Error(w, "Invalid post ID", http.StatusBadRequest)
        return
    }
    
    post, found := r.GetByID(id)
    if !found {
        http.Error(w, "Post not found", http.StatusNotFound)
        return
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(post)
}

func extractIDFromURL(path string) int {
    // Simple extraction logic - in production, use proper URL parsing
    // This assumes URL format: /posts/123
    parts := strings.Split(path, "/")
    if len(parts) < 3 {
        return -1
    }
    
    id, err := strconv.Atoi(parts[2])
    if err != nil {
        return -1
    }
    
    return id
}

func main() {
    repo := NewPostRepository()
    
    // Create some initial data
    repo.Create(&Post{
        Title:   "Getting Started with Go",
        Content: "Go is a powerful programming language...",
        Author:  "Alice",
    })
    repo.Create(&Post{
        Title:   "Web Development in Go",
        Content: "Building web applications with Go is straightforward...",
        Author:  "Bob",
    })
    
    mux := http.NewServeMux()
    mux.HandleFunc("/posts", func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            repo.getPostsHandler(w, r)
        case http.MethodPost:
            repo.createPostHandler(w, r)
        default:
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        }
    })
    
    mux.HandleFunc("/posts/", func(w http.ResponseWriter, r *http.Request) {
        if r.Method == http.MethodGet {
            repo.getPostHandler(w, r)
        } else {
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        }
    })
    
    fmt.Println("Blog API starting on port 8080...")
    fmt.Println("Available endpoints:")
    fmt.Println("  GET    /posts        - List all posts")
    fmt.Println("  POST   /posts        - Create new post")
    fmt.Println("  GET    /posts/{id}   - Get specific post")
    
    http.ListenAndServe(":8080", mux)
}

Configuration and Environment

Environment Variables

package main

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

type Config struct {
    Port        string
    DatabaseURL string
    LogLevel    string
    Debug       bool
}

func loadConfig() *Config {
    config := &Config{
        Port:        getEnv("PORT", "8080"),
        DatabaseURL: getEnv("DATABASE_URL", "sqlite:app.db"),
        LogLevel:    getEnv("LOG_LEVEL", "info"),
        Debug:       getEnvBool("DEBUG", false),
    }
    
    return config
}

func getEnv(key, defaultValue string) string {
    if value, exists := os.LookupEnv(key); exists {
        return value
    }
    return defaultValue
}

func getEnvBool(key string, defaultValue bool) bool {
    if value, exists := os.LookupEnv(key); exists {
        if parsed, err := strconv.ParseBool(value); err == nil {
            return parsed
        }
    }
    return defaultValue
}

func main() {
    config := loadConfig()
    
    if config.Debug {
        log.Printf("Debug mode enabled")
        log.Printf("Port: %s", config.Port)
        log.Printf("Database: %s", config.DatabaseURL)
    }
    
    handler := setupRoutes(config)
    
    addr := ":" + config.Port
    log.Printf("Server starting on %s", addr)
    if err := http.ListenAndServe(addr, handler); err != nil {
        log.Fatalf("Server failed to start: %v", err)
    }
}

func setupRoutes(config *Config) http.Handler {
    mux := http.NewServeMux()
    
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Server running on port %s", config.Port)
    })
    
    mux.HandleFunc("/config", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Port: %s\n", config.Port)
        fmt.Fprintf(w, "Database: %s\n", config.DatabaseURL)
        fmt.Fprintf(w, "Log Level: %s\n", config.LogLevel)
        fmt.Fprintf(w, "Debug: %t\n", config.Debug)
    })
    
    return mux
}

Graceful Shutdown

Handle server shutdown gracefully to close connections properly:

package main

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

func main() {
    server := &http.Server{
        Addr:    ":8080",
        Handler: setupRoutes(),
    }
    
    // Start server in a goroutine
    go func() {
        log.Printf("Server starting on %s", server.Addr)
        if err := server.ListenAndServe(); 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
    
    log.Println("Shutting down server...")
    
    // Create deadline for shutdown
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    // Attempt graceful shutdown
    if err := server.Shutdown(ctx); err != nil {
        log.Printf("Server forced to shutdown: %v", err)
    }
    
    log.Println("Server exited")
}

func setupRoutes() http.Handler {
    mux := http.NewServeMux()
    
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Server running... Try stopping with Ctrl+C")
    })
    
    return mux
}

Testing Your Web Server

Go’s testing package makes it easy to test HTTP handlers:

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestHomeHandler(t *testing.T) {
    // Create a request to pass to our handler
    req, err := http.NewRequest("GET", "/", nil)
    if err != nil {
        t.Fatal(err)
    }
    
    // Create a ResponseRecorder to record the response
    rr := httptest.NewRecorder()
    
    // Create handler and serve the request
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("Hello, World!"))
    })
    
    handler.ServeHTTP(rr, req)
    
    // Check the status code
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
    }
    
    // Check the response body
    expected := "Hello, World!"
    if rr.Body.String() != expected {
        t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
    }
}

func TestAPIEndpoint(t *testing.T) {
    req, err := http.NewRequest("GET", "/api/users", nil)
    if err != nil {
        t.Fatal(err)
    }
    
    rr := httptest.NewRecorder()
    
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`[{"id":1,"name":"John"}]`))
    })
    
    handler.ServeHTTP(rr, req)
    
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("API returned wrong status code: got %v want %v", status, http.StatusOK)
    }
    
    contentType := rr.Header().Get("Content-Type")
    expectedContentType := "application/json"
    if contentType != expectedContentType {
        t.Errorf("API returned wrong content type: got %v want %v", contentType, expectedContentType)
    }
}

Best Practices

  1. Use Interfaces: Define interfaces for your services to enable testing and flexibility
  2. Handle Errors: Always handle errors appropriately
  3. Validate Input: Validate all incoming data
  4. Use Context: Pass context through your request chain
  5. Implement Logging: Add comprehensive logging
  6. Secure Your Server: Use HTTPS, authentication, and input sanitization
  7. Test Your Code: Write unit tests for handlers and middleware

External Resources:

Related Tutorials:

Last updated on