Skip to main content
โšก Calmops

Building HTTP Handlers in Go

Building HTTP Handlers in Go

HTTP handlers are the core of web applications in Go. The standard library provides everything you need to build robust web services. This guide covers everything about building effective HTTP handlers.

Basic HTTP Handlers

Simple Handler

package main

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

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

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

Handler with Response Headers

package main

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

func jsonHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, `{"message": "Hello, World!"}`)
}

func main() {
	http.HandleFunc("/json", jsonHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Handler with Status Codes

package main

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

func notFoundHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusNotFound)
	fmt.Fprintf(w, "Page not found")
}

func createdHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusCreated)
	fmt.Fprintf(w, "Resource created")
}

func main() {
	http.HandleFunc("/notfound", notFoundHandler)
	http.HandleFunc("/created", createdHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Request Handling

Reading Request Data

package main

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

func echoHandler(w http.ResponseWriter, r *http.Request) {
	// Read request body
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Error reading body", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	w.Header().Set("Content-Type", "text/plain")
	fmt.Fprintf(w, "You sent: %s", string(body))
}

func main() {
	http.HandleFunc("/echo", echoHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Query Parameters

package main

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

func searchHandler(w http.ResponseWriter, r *http.Request) {
	// Parse query parameters
	query := r.URL.Query()

	// Get single parameter
	search := query.Get("q")
	fmt.Fprintf(w, "Search query: %s\n", search)

	// Get all parameters
	for key, values := range query {
		fmt.Fprintf(w, "%s: %v\n", key, values)
	}
}

func main() {
	http.HandleFunc("/search", searchHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Form Data

package main

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

func formHandler(w http.ResponseWriter, r *http.Request) {
	// Parse form data
	err := r.ParseForm()
	if err != nil {
		http.Error(w, "Error parsing form", http.StatusBadRequest)
		return
	}

	// Get form values
	name := r.FormValue("name")
	email := r.FormValue("email")

	fmt.Fprintf(w, "Name: %s\nEmail: %s\n", name, email)
}

func main() {
	http.HandleFunc("/form", formHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Request Headers

package main

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

func headerHandler(w http.ResponseWriter, r *http.Request) {
	// Get specific header
	userAgent := r.Header.Get("User-Agent")
	fmt.Fprintf(w, "User-Agent: %s\n", userAgent)

	// Get all headers
	fmt.Fprintf(w, "\nAll headers:\n")
	for key, values := range r.Header {
		for _, value := range values {
			fmt.Fprintf(w, "%s: %s\n", key, value)
		}
	}
}

func main() {
	http.HandleFunc("/headers", headerHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Handler Types and Patterns

Handler Interface

package main

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

// Custom handler type
type MyHandler struct {
	message string
}

// Implement http.Handler interface
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, h.message)
}

func main() {
	handler := &MyHandler{message: "Custom handler"}
	http.Handle("/custom", handler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Handler Chaining

package main

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

// Middleware function
func loggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Printf("Request: %s %s\n", r.Method, r.URL.Path)
		next.ServeHTTP(w, r)
	})
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello!")
}

func main() {
	handler := http.HandlerFunc(helloHandler)
	http.Handle("/", loggingMiddleware(handler))
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Middleware

Authentication Middleware

package main

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

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

		if token != "Bearer valid-token" {
			http.Error(w, "Invalid token", http.StatusForbidden)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func protectedHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "This is protected content")
}

func main() {
	handler := http.HandlerFunc(protectedHandler)
	http.Handle("/protected", authMiddleware(handler))
	log.Fatal(http.ListenAndServe(":8080", nil))
}

CORS Middleware

package main

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

func corsMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type")

		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusOK)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func apiHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, `{"status": "ok"}`)
}

func main() {
	handler := http.HandlerFunc(apiHandler)
	http.Handle("/api", corsMiddleware(handler))
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Practical Examples

RESTful API Handler

package main

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

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

var users = map[int]User{
	1: {ID: 1, Name: "Alice", Email: "[email protected]"},
	2: {ID: 2, Name: "Bob", Email: "[email protected]"},
}

func userHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	// Extract ID from path
	parts := strings.Split(r.URL.Path, "/")
	if len(parts) < 3 {
		http.Error(w, "Invalid path", http.StatusBadRequest)
		return
	}

	switch r.Method {
	case http.MethodGet:
		// Get all users
		json.NewEncoder(w).Encode(users)

	case http.MethodPost:
		// Create user
		var user User
		json.NewDecoder(r.Body).Decode(&user)
		users[user.ID] = user
		w.WriteHeader(http.StatusCreated)
		json.NewEncoder(w).Encode(user)

	default:
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	}
}

func main() {
	http.HandleFunc("/users", userHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

File Upload Handler

package main

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

func uploadHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	// Parse multipart form
	err := r.ParseMultipartForm(10 << 20) // 10MB max
	if err != nil {
		http.Error(w, "Error parsing form", http.StatusBadRequest)
		return
	}

	// Get file from form
	file, handler, err := r.FormFile("file")
	if err != nil {
		http.Error(w, "Error retrieving file", http.StatusBadRequest)
		return
	}
	defer file.Close()

	// Create destination file
	dst, err := os.Create(handler.Filename)
	if err != nil {
		http.Error(w, "Error creating file", http.StatusInternalServerError)
		return
	}
	defer dst.Close()

	// Copy file
	_, err = io.Copy(dst, file)
	if err != nil {
		http.Error(w, "Error saving file", http.StatusInternalServerError)
		return
	}

	fmt.Fprintf(w, "File uploaded: %s", handler.Filename)
}

func main() {
	http.HandleFunc("/upload", uploadHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Best Practices

โœ… Good Practices

// Set appropriate status codes
w.WriteHeader(http.StatusOK)

// Set Content-Type header
w.Header().Set("Content-Type", "application/json")

// Handle errors properly
if err != nil {
	http.Error(w, "Error message", http.StatusInternalServerError)
	return
}

// Use middleware for cross-cutting concerns
func middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Pre-processing
		next.ServeHTTP(w, r)
		// Post-processing
	})
}

// Validate input
if r.Method != http.MethodPost {
	http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
	return
}

โŒ Anti-Patterns

// Don't ignore errors
body, _ := io.ReadAll(r.Body)

// Don't forget to set status code
fmt.Fprintf(w, "Error") // Returns 200 OK!

// Don't mix concerns
// Keep handlers focused on single responsibility

// Don't hardcode values
// Use configuration for ports, timeouts, etc.

Resources

Summary

Building HTTP handlers in Go is straightforward with the standard library:

  • Use http.HandleFunc() for simple handlers
  • Implement http.Handler interface for complex handlers
  • Use middleware for cross-cutting concerns
  • Always set appropriate status codes and headers
  • Validate and sanitize input
  • Handle errors gracefully

With these patterns, you can build robust and maintainable web services in Go.

Comments