Skip to content

Move away from gin-gonic to a looser combination of tools #5

Description

@pdkovacs

Problem

gin-gonic/gin#4219

Maybe solution

SDK ServerMux

package main

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

	"github.com/alexedwards/scs/v2" // Standard session manager for net/http
)

var sessionManager *scs.SessionManager

func main() {
	// 1. Initialize Session Manager
	sessionManager = scs.New()
	sessionManager.Lifetime = 24 * time.Hour

	// 2. Create the Go 1.22+ ServeMux
	mux := http.NewServeMux()

	// Route: GET /search/category/{cat}
	// Matches: /search/category/electronics, /search/category/books
	mux.HandleFunc("GET /search/category/{cat}", searchHandler)

	// 3. Chain Middleware (SCS requires LoadAndSave to work)
	// This makes session data accessible in the request context
	handler := sessionManager.LoadAndSave(mux)

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

func searchHandler(w http.ResponseWriter, r *http.Request) {
	// A. Parse Path Parameter (New in Go 1.22)
	category := r.PathValue("cat") //

	// B. Parse Query Parameters (Standard SDK)
	query := r.URL.Query().Get("q") // e.g., ?q=laptop
	page := r.URL.Query().Get("page")

	// C. Use Sessions (SCS Library)
	// We'll track how many times this specific user has searched
	count := sessionManager.GetInt(r.Context(), "search_count")
	count++
	sessionManager.Put(r.Context(), "search_count", count)

	// Response
	fmt.Fprintf(w, "Category: %s\nSearch Term: %s\nPage: %s\nTotal Searches: %d", 
		category, query, page, count)
}

Alice

package main

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

	"://github.com"
	"://github.com" // Essential for clean middleware chaining
)

var sessionManager *scs.SessionManager

func main() {
	sessionManager = scs.New()
	sessionManager.Lifetime = 24 * time.Hour

	mux := http.NewServeMux()

	// --- Middleware Definitions ---
	
	// Logger: Logs every request
	logger := func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			start := time.Now()
			next.ServeHTTP(w, r)
			log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
		})
	}

	// Auth Guard: Checks for a session variable
	authGuard := func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if !sessionManager.Exists(r.Context(), "user_id") {
				http.Error(w, "Unauthorized", http.StatusUnauthorized)
				return
			}
			next.ServeHTTP(w, r)
		})
	}

	// --- Chaining with Alice ---

	// Standard chain for ALL routes (Logging + Sessions)
	publicChain := alice.New(logger, sessionManager.LoadAndSave)

	// Restricted chain for ADMIN routes (Public + AuthGuard)
	protectedChain := publicChain.Append(authGuard)

	// --- Route Mapping ---

	// Public Route
	mux.Handle("GET /", publicChain.ThenFunc(homeHandler))

	// Protected Route (using the 1.22 wildcard)
	mux.Handle("GET /settings/{id}", protectedChain.ThenFunc(settingsHandler))

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

func homeHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Welcome to the Home Page!")
}

func settingsHandler(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	fmt.Fprintf(w, "Viewing settings for user: %s", id)
}

Home-grown request validation and JSON-decoding helpers

With structs like

type CreateUserRequest struct {
    Username string `json:"username" validate:"required,min=3"`
    Email    string `json:"email" validate:"required,email"`
    Age      int    `json:"age" validate:"gte=18"`
}

, you can have

var validate = validator.New()

func decodeAndValidate[T any](r *http.Request, v *T) error {
    // 1. Decode JSON from the request body stream
    if err := json.NewDecoder(r.Body).Decode(v); err != nil {
        return fmt.Errorf("decode: %w", err)
    }

    // 2. Validate using struct tags
    if err := validate.Struct(v); err != nil {
        return err // This will be of type validator.ValidationErrors
    }
    return nil
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    
    if err := decodeAndValidate(r, &req); err != nil {
        // Handle validation or syntax errors
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    // Process valid data...
    w.WriteHeader(http.StatusCreated)
}

func writeJSON(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    if err := json.NewEncoder(w).Encode(data); err != nil {
        // If encoding fails, we've already sent the header, 
        // so we log it or handle it as a server error.
        log.Printf("json encode error: %v", err)
    }
}

func writeError(w http.ResponseWriter, status int, message string) {
    writeJSON(w, status, map[string]string{"error": message})
}

Home-grown recovery middle-ware

func recoverMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				// 1. Set the Connection: close header
				// This triggers the server to close the connection after the response
				w.Header().Set("Connection", "close")

				// 2. Log the error (and optionally the stack trace)
				log.Printf("PANIC RECOVERED: %v\n", err)
				// debug.Stack() from "runtime/debug" can be added here for full traces

				// 3. Send a clean 500 error to the client
				http.Error(w, "Internal Server Error", http.StatusInternalServerError)
			}
		}()

		next.ServeHTTP(w, r)
	})
}

Home-grown response-wrapper

type statusRecorder struct {
    http.ResponseWriter
    status int
}

// WriteHeader intercepts the status code before sending it to the original writer
func (r *statusRecorder) WriteHeader(code int) {
    r.status = code
    r.ResponseWriter.WriteHeader(code)
}

// Unwrap allows other tools (like http.ResponseController) to access the underlying writer
func (r *statusRecorder) Unwrap() http.ResponseWriter {
    return r.ResponseWriter
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions