main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
package main

import (
	"encoding/json"
	"log"
	"net/http"
	"os"
	"strconv"
	"sync"
	"time"

	"github.com/gorilla/mux"
)

// User represents a user resource
type User struct {
	ID       int    `json:"id"`
	Username string `json:"username"`
	Email    string `json:"email"`
	Age      int    `json:"age"`
}

// UserStore manages user data with thread-safe operations
type UserStore struct {
	mu    sync.RWMutex
	users map[int]User
	nextID int
}

// NewUserStore creates a new UserStore with initial data
func NewUserStore() *UserStore {
	return &UserStore{
		users: map[int]User{
			1: {ID: 1, Username: "john_doe", Email: "[email protected]", Age: 30},
			2: {ID: 2, Username: "jane_smith", Email: "[email protected]", Age: 28},
		},
		nextID: 3,
	}
}

// GetAllUsers returns all users (read-locked for thread safety)
func (us *UserStore) GetAllUsers() []User {
	us.mu.RLock()
	defer us.mu.RUnlock()

	users := make([]User, 0, len(us.users))
	for _, user := range us.users {
		users = append(users, user)
	}
	return users
}

// GetUserByID returns a single user by ID
func (us *UserStore) GetUserByID(id int) (User, bool) {
	us.mu.RLock()
	defer us.mu.RUnlock()

	user, exists := us.users[id]
	return user, exists
}

// CreateUser adds a new user to the store
func (us *UserStore) CreateUser(user User) User {
	us.mu.Lock()
	defer us.mu.Unlock()

	user.ID = us.nextID
	us.users[user.ID] = user
	us.nextID++
	return user
}

// UpdateUser updates an existing user
func (us *UserStore) UpdateUser(id int, updatedUser User) (User, bool) {
	us.mu.Lock()
	defer us.mu.Unlock()

	user, exists := us.users[id]
	if !exists {
		return User{}, false
	}

	// Preserve original ID and update other fields
	updatedUser.ID = id
	us.users[id] = updatedUser
	return updatedUser, true
}

// DeleteUser removes a user from the store
func (us *UserStore) DeleteUser(id int) bool {
	us.mu.Lock()
	defer us.mu.Unlock()

	_, exists := us.users[id]
	if !exists {
		return false
	}

	delete(us.users, id)
	return true
}

// Middleware: LoggingMiddleware logs incoming HTTP requests
func LoggingMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		log.Printf(
			"START: %s %s %s",
			r.Method,
			r.URL.Path,
			r.RemoteAddr,
		)

		// Call the next handler
		next.ServeHTTP(w, r)

		// Log after request is processed
		log.Printf(
			"END: %s %s %s %s",
			r.Method,
			r.URL.Path,
			r.RemoteAddr,
			time.Since(start),
		)
	})
}

// Middleware: RecoveryMiddleware recovers from panics and returns 500 error
func RecoveryMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if err := recover(); err != nil {
				log.Printf("PANIC: %v", err)
				http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

// Helper: respondJSON sends a JSON response with proper status code
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	if data != nil {
		if err := json.NewEncoder(w).Encode(data); err != nil {
			log.Printf("Failed to encode JSON response: %v", err)
		}
	}
}

// Handler: GetAllUsersHandler returns all users
func GetAllUsersHandler(us *UserStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		users := us.GetAllUsers()
		respondJSON(w, http.StatusOK, users)
	}
}

// Handler: GetUserHandler returns a single user by ID
func GetUserHandler(us *UserStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		idStr := vars["id"]
		id, err := strconv.Atoi(idStr)
		if err != nil {
			respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid user ID"})
			return
		}

		user, exists := us.GetUserByID(id)
		if !exists {
			respondJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"})
			return
		}

		respondJSON(w, http.StatusOK, user)
	}
}

// Handler: CreateUserHandler creates a new user
func CreateUserHandler(us *UserStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var user User
		if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
			respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
			return
		}

		// Validate required fields
		if user.Username == "" || user.Email == "" {
			respondJSON(w, http.StatusBadRequest, map[string]string{"error": "username and email are required"})
			return
		}

		createdUser := us.CreateUser(user)
		respondJSON(w, http.StatusCreated, createdUser)
	}
}

// Handler: UpdateUserHandler updates an existing user
func UpdateUserHandler(us *UserStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		idStr := vars["id"]
		id, err := strconv.Atoi(idStr)
		if err != nil {
			respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid user ID"})
			return
		}

		var updatedUser User
		if err := json.NewDecoder(r.Body).Decode(&updatedUser); err != nil {
			respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
			return
		}

		user, exists := us.UpdateUser(id, updatedUser)
		if !exists {
			respondJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"})
			return
		}

		respondJSON(w, http.StatusOK, user)
	}
}

// Handler: DeleteUserHandler deletes a user
func DeleteUserHandler(us *UserStore) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		idStr := vars["id"]
		id, err := strconv.Atoi(idStr)
		if err != nil {
			respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid user ID"})
			return
		}

		success := us.DeleteUser(id)
		if !success {
			respondJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"})
			return
		}

		// Return 204 No Content on successful deletion
		w.WriteHeader(http.StatusNoContent)
	}
}

func main() {
	// Initialize user store
	userStore := NewUserStore()

	// Create router with gorilla/mux (industry standard)
	r := mux.NewRouter()

	// Apply global middleware
	r.Use(LoggingMiddleware)
	r.Use(RecoveryMiddleware)

	// Define API routes
	api := r.PathPrefix("/api/v1").Subrouter()
	api.HandleFunc("/users", GetAllUsersHandler(userStore)).Methods(http.MethodGet)
	api.HandleFunc("/users/{id}", GetUserHandler(userStore)).Methods(http.MethodGet)
	api.HandleFunc("/users", CreateUserHandler(userStore)).Methods(http.MethodPost)
	api.HandleFunc("/users/{id}", UpdateUserHandler(userStore)).Methods(http.MethodPut)
	api.HandleFunc("/users/{id}", DeleteUserHandler(userStore)).Methods(http.MethodDelete)

	// Get port from environment variable (default to 8080)
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	// Start server
	log.Printf("Starting server on :%s", port)
	if err := http.ListenAndServe(":"+port, r); err != nil {
		log.Fatalf("Server failed to start: %v", err)
	}
}

How It Works

Implements a small user API with middleware, structured routing, and guarded state access.

UserStore holds users behind an RWMutex with auto-incremented IDs; handlers decode and validate JSON bodies, then perform CRUD operations; router groups endpoints under /api/v1; middleware logs method, path, and duration and converts panics into 500 responses; server reads the PORT environment variable before ListenAndServe.

Key Concepts

  • 1Thread-safe map storage with read/write locks and incremental ID tracking.
  • 2JSON decoding and encoding with validation of required fields and path parameters.
  • 3Reusable logging and recovery middleware around all routes.
  • 4Versioned subrouter and explicit HTTP methods for each handler.

When to Use This Pattern

  • Bootstrap template for CRUD microservices or prototypes.
  • Mock API for frontend integration tests.
  • Teaching Gorilla Mux routing and middleware layering.
  • Temporary admin APIs before adding a database.

Best Practices

  • Return clear 4xx errors for validation failures instead of panicking.
  • Add context timeouts for database or downstream calls when extending handlers.
  • Keep route registration together and versioned for future migrations.
  • Log request duration and status codes for observability.
Go Version1.18+
Difficultyadvanced
Production ReadyYes
Lines of Code280