MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr πŸ”₯ tagline

Hey there πŸ‘‹ I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 β€” present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 β€” Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms β€” earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Introduction to Go: Building a REST API

Learn Go fundamentals and build a REST API with the standard library and Gorilla Mux.

GoREST APIBackendGetting Started

By MinhVo

Introduction

Go (Golang) has become one of the most popular languages for building backend services, APIs, and distributed systems. Created at Google in 2007 by Robert Griesemer, Rob Pike, and Ken Thompson, Go was designed to address the challenges of building large-scale software: fast compilation, efficient concurrency, and simplicity. Its standard library includes a production-ready HTTP server, making it possible to build a REST API without any external dependencies.

Go's simplicity is its superpower. The language has a small specβ€”no generics until Go 1.18, no exceptions (just explicit error returns), no classes (just structs with methods), and no inheritance (just interfaces). This simplicity leads to code that is easy to read, easy to maintain, and easy to onboard new developers. Companies like Docker, Kubernetes, Terraform, CockroachDB, and Uber's microservices are all built in Go, proving its suitability for production systems.

The Go standard library's net/http package provides everything needed for HTTP servers: request parsing, response writing, routing (basic), middleware, TLS support, and graceful shutdown. For more advanced routing, the community has created excellent libraries like Gorilla Mux, Chi, and Echo. This guide covers Go fundamentals and builds a production-ready REST API using both the standard library and Gorilla Mux.

Go programming language and backend development

Understanding Go: Core Concepts

Types and Structs

Go is statically typed with a clean type system. Structs are Go's primary data structureβ€”they're like classes without inheritance:

package main
 
import "time"
 
// User represents a user in the system
type User struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    Age       int       `json:"age,omitempty"`
    CreatedAt time.Time `json:"created_at"`
}
 
// Method on User struct
func (u *User) Validate() error {
    if u.Name == "" {
        return fmt.Errorf("name is required")
    }
    if u.Email == "" {
        return fmt.Errorf("email is required")
    }
    return nil
}

Error Handling

Go uses explicit error returns instead of exceptions. This forces developers to handle errors at each call site:

func GetUser(id string) (*User, error) {
    row := db.QueryRow("SELECT id, name, email FROM users WHERE id = $1", id)
    
    var user User
    err := row.Scan(&user.ID, &user.Name, &user.Email)
    if err != nil {
        if err == sql.ErrNoRows {
            return nil, fmt.Errorf("user %s not found", id)
        }
        return nil, fmt.Errorf("failed to query user: %w", err) // Wrap error
    }
    
    return &user, nil
}

Interfaces

Go interfaces are satisfied implicitlyβ€”any type that implements the required methods satisfies the interface without explicit declaration:

// Any type with a ServeHTTP method satisfies this interface
type Handler interface {
    ServeHTTP(w http.ResponseWriter, r *http.Request)
}
 
// UserRepository defines data access operations
type UserRepository interface {
    FindByID(ctx context.Context, id string) (*User, error)
    FindAll(ctx context.Context) ([]*User, error)
    Create(ctx context.Context, user *User) error
    Update(ctx context.Context, user *User) error
    Delete(ctx context.Context, id string) error
}

Goroutines and Channels

Go's concurrency model is built around goroutines (lightweight threads) and channels (typed communication pipes):

// Goroutine: lightweight concurrent execution
go func() {
    result := expensiveComputation()
    ch <- result // Send result to channel
}()
 
// Channel: typed communication between goroutines
ch := make(chan Result, 10) // Buffered channel
result := <-ch              // Receive from channel

Building REST APIs and microservices

Architecture and Design Patterns

The Handler Pattern

Go HTTP handlers follow the http.HandlerFunc signature. Handlers are composed using middleware:

type Middleware func(http.Handler) http.Handler
 
func LoggingMiddleware(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 %v", r.Method, r.URL.Path, r.RemoteAddr, time.Since(start))
    })
}

The Repository Pattern

Separate data access from business logic using the repository pattern. Define an interface for data operations and provide implementations that can be swapped for testing:

// In-memory implementation for testing
type InMemoryUserRepo struct {
    users map[string]*User
    mu    sync.RWMutex
}
 
func (r *InMemoryUserRepo) FindByID(ctx context.Context, id string) (*User, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    user, ok := r.users[id]
    if !ok {
        return nil, ErrUserNotFound
    }
    return user, nil
}

The Service Layer

Business logic lives in the service layer, which depends on repository interfaces:

type UserService struct {
    repo UserRepository
}
 
func (s *UserService) GetUser(ctx context.Context, id string) (*User, error) {
    user, err := s.repo.FindByID(ctx, id)
    if err != nil {
        return nil, err
    }
    return user, nil
}

Step-by-Step Implementation

Let's build a complete REST API for managing users.

Project Structure

my-api/
β”œβ”€β”€ main.go
β”œβ”€β”€ go.mod
β”œβ”€β”€ go.sum
β”œβ”€β”€ handlers/
β”‚   └── user.go
β”œβ”€β”€ models/
β”‚   └── user.go
β”œβ”€β”€ repository/
β”‚   β”œβ”€β”€ repository.go
β”‚   └── memory.go
β”œβ”€β”€ middleware/
β”‚   └── middleware.go
└── main_test.go

Models

// models/user.go
package models
 
import (
    "fmt"
    "time"
)
 
type User struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    Age       int       `json:"age,omitempty"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}
 
type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age,omitempty"`
}
 
type UpdateUserRequest struct {
    Name  *string `json:"name,omitempty"`
    Email *string `json:"email,omitempty"`
    Age   *int    `json:"age,omitempty"`
}
 
func (r *CreateUserRequest) Validate() error {
    if r.Name == "" {
        return fmt.Errorf("name is required")
    }
    if r.Email == "" {
        return fmt.Errorf("email is required")
    }
    if r.Age < 0 || r.Age > 150 {
        return fmt.Errorf("age must be between 0 and 150")
    }
    return nil
}

Repository Interface and Implementation

// repository/repository.go
package repository
 
import (
    "context"
    "errors"
    "my-api/models"
)
 
var (
    ErrNotFound = errors.New("not found")
    ErrConflict = errors.New("conflict")
)
 
type UserRepository interface {
    FindByID(ctx context.Context, id string) (*models.User, error)
    FindAll(ctx context.Context) ([]*models.User, error)
    Create(ctx context.Context, user *models.User) error
    Update(ctx context.Context, user *models.User) error
    Delete(ctx context.Context, id string) error
}
// repository/memory.go
package repository
 
import (
    "context"
    "sync"
    "my-api/models"
)
 
type MemoryUserRepo struct {
    users map[string]*models.User
    mu    sync.RWMutex
}
 
func NewMemoryUserRepo() *MemoryUserRepo {
    return &MemoryUserRepo{
        users: make(map[string]*models.User),
    }
}
 
func (r *MemoryUserRepo) FindByID(ctx context.Context, id string) (*models.User, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    user, ok := r.users[id]
    if !ok {
        return nil, ErrNotFound
    }
    return user, nil
}
 
func (r *MemoryUserRepo) FindAll(ctx context.Context) ([]*models.User, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    users := make([]*models.User, 0, len(r.users))
    for _, user := range r.users {
        users = append(users, user)
    }
    return users, nil
}
 
func (r *MemoryUserRepo) Create(ctx context.Context, user *models.User) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    // Check for duplicate email
    for _, existing := range r.users {
        if existing.Email == user.Email {
            return ErrConflict
        }
    }
    r.users[user.ID] = user
    return nil
}
 
func (r *MemoryUserRepo) Update(ctx context.Context, user *models.User) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    if _, ok := r.users[user.ID]; !ok {
        return ErrNotFound
    }
    r.users[user.ID] = user
    return nil
}
 
func (r *MemoryUserRepo) Delete(ctx context.Context, id string) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    if _, ok := r.users[id]; !ok {
        return ErrNotFound
    }
    delete(r.users, id)
    return nil
}

HTTP Handlers

// handlers/user.go
package handlers
 
import (
    "encoding/json"
    "errors"
    "net/http"
    "strings"
    "time"
    "my-api/models"
    "my-api/repository"
)
 
type UserHandler struct {
    repo repository.UserRepository
}
 
func NewUserHandler(repo repository.UserRepository) *UserHandler {
    return &UserHandler{repo: repo}
}
 
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Basic routing
    path := strings.TrimPrefix(r.URL.Path, "/users")
    path = strings.TrimPrefix(path, "/")
 
    switch {
    case path == "" && r.Method == http.MethodGet:
        h.ListUsers(w, r)
    case path == "" && r.Method == http.MethodPost:
        h.CreateUser(w, r)
    case path != "" && r.Method == http.MethodGet:
        h.GetUser(w, r, path)
    case path != "" && r.Method == http.MethodPut:
        h.UpdateUser(w, r, path)
    case path != "" && r.Method == http.MethodDelete:
        h.DeleteUser(w, r, path)
    default:
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
    }
}
 
func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
    users, err := h.repo.FindAll(r.Context())
    if err != nil {
        writeError(w, http.StatusInternalServerError, "Failed to fetch users")
        return
    }
    writeJSON(w, http.StatusOK, map[string]interface{}{"data": users, "total": len(users)})
}
 
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request, id string) {
    user, err := h.repo.FindByID(r.Context(), id)
    if err != nil {
        if errors.Is(err, repository.ErrNotFound) {
            writeError(w, http.StatusNotFound, "User not found")
            return
        }
        writeError(w, http.StatusInternalServerError, "Failed to fetch user")
        return
    }
    writeJSON(w, http.StatusOK, map[string]interface{}{"data": user})
}
 
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
    var req models.CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "Invalid request body")
        return
    }
    if err := req.Validate(); err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }
 
    user := &models.User{
        ID:        generateID(),
        Name:      req.Name,
        Email:     req.Email,
        Age:       req.Age,
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    }
 
    if err := h.repo.Create(r.Context(), user); err != nil {
        if errors.Is(err, repository.ErrConflict) {
            writeError(w, http.StatusConflict, "User with this email already exists")
            return
        }
        writeError(w, http.StatusInternalServerError, "Failed to create user")
        return
    }
 
    writeJSON(w, http.StatusCreated, map[string]interface{}{"data": user})
}
 
func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request, id string) {
    user, err := h.repo.FindByID(r.Context(), id)
    if err != nil {
        if errors.Is(err, repository.ErrNotFound) {
            writeError(w, http.StatusNotFound, "User not found")
            return
        }
        writeError(w, http.StatusInternalServerError, "Failed to fetch user")
        return
    }
 
    var req models.UpdateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "Invalid request body")
        return
    }
 
    if req.Name != nil {
        user.Name = *req.Name
    }
    if req.Email != nil {
        user.Email = *req.Email
    }
    if req.Age != nil {
        user.Age = *req.Age
    }
    user.UpdatedAt = time.Now()
 
    if err := h.repo.Update(r.Context(), user); err != nil {
        writeError(w, http.StatusInternalServerError, "Failed to update user")
        return
    }
 
    writeJSON(w, http.StatusOK, map[string]interface{}{"data": user})
}
 
func (h *UserHandler) DeleteUser(w http.ResponseWriter, r *http.Request, id string) {
    if err := h.repo.Delete(r.Context(), id); err != nil {
        if errors.Is(err, repository.ErrNotFound) {
            writeError(w, http.StatusNotFound, "User not found")
            return
        }
        writeError(w, http.StatusInternalServerError, "Failed to delete user")
        return
    }
    w.WriteHeader(http.StatusNoContent)
}
 
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}
 
func writeError(w http.ResponseWriter, status int, message string) {
    writeJSON(w, status, map[string]string{"error": message})
}
 
func generateID() string {
    // Simple UUID-like ID generation
    return fmt.Sprintf("%x", time.Now().UnixNano())
}

Middleware

// middleware/middleware.go
package middleware
 
import (
    "log"
    "net/http"
    "time"
)
 
func Logger(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 %v", r.Method, r.URL.Path, time.Since(start))
    })
}
 
func CORS(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, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusNoContent)
            return
        }
        next.ServeHTTP(w, r)
    })
}
 
func Recovery(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 recovered: %v", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

Main Entry Point

// main.go
package main
 
import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
    "my-api/handlers"
    "my-api/middleware"
    "my-api/repository"
)
 
func main() {
    // Initialize repository
    repo := repository.NewMemoryUserRepo()
 
    // Initialize handlers
    userHandler := handlers.NewUserHandler(repo)
 
    // Setup routes with middleware
    mux := http.NewServeMux()
    mux.Handle("/users", userHandler)
    mux.Handle("/users/", userHandler)
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"status":"healthy"}`))
    })
 
    // Apply middleware
    handler := middleware.Recovery(middleware.CORS(middleware.Logger(mux)))
 
    // Configure server
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
 
    server := &http.Server{
        Addr:         ":" + port,
        Handler:      handler,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }
 
    // Graceful shutdown
    go func() {
        sigCh := make(chan os.Signal, 1)
        signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
        <-sigCh
 
        log.Println("Shutting down server...")
        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()
 
        if err := server.Shutdown(ctx); err != nil {
            log.Printf("Server forced to shutdown: %v", err)
        }
    }()
 
    log.Printf("Server starting on port %s", port)
    if err := server.ListenAndServe(); err != http.ErrServerClosed {
        log.Fatalf("Server failed: %v", err)
    }
    log.Println("Server stopped")
}

Using Gorilla Mux for Advanced Routing

package main
 
import (
    "log"
    "net/http"
    "github.com/gorilla/mux"
    "my-api/handlers"
    "my-api/middleware"
    "my-api/repository"
)
 
func main() {
    repo := repository.NewMemoryUserRepo()
    userHandler := handlers.NewUserHandler(repo)
 
    r := mux.NewRouter()
 
    // Middleware
    r.Use(middleware.Logger)
    r.Use(middleware.CORS)
    r.Use(middleware.Recovery)
 
    // API v1 routes
    api := r.PathPrefix("/api/v1").Subrouter()
 
    api.HandleFunc("/users", userHandler.ListUsers).Methods("GET")
    api.HandleFunc("/users", userHandler.CreateUser).Methods("POST")
    api.HandleFunc("/users/{id:[a-f0-9-]+}", userHandler.GetUser).Methods("GET")
    api.HandleFunc("/users/{id:[a-f0-9-]+}", userHandler.UpdateUser).Methods("PUT")
    api.HandleFunc("/users/{id:[a-f0-9-]+}", userHandler.DeleteUser).Methods("DELETE")
 
    // Health check
    r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"status":"healthy"}`))
    }).Methods("GET")
 
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", r))
}

Backend development and API design

Real-World Use Cases

Use Case 1: Microservices

Go's fast startup time (typically under 100ms) and low memory footprint (5-20MB per service) make it ideal for microservices. Companies like Uber run thousands of Go microservices handling millions of requests per second.

Use Case 2: CLI Tools

Go compiles to single static binaries with no runtime dependencies, making it perfect for CLI tools. Docker, Kubernetes kubectl, Terraform, and Hugo are all Go CLI applications distributed as single binaries.

Use Case 3: Real-Time Systems

Go's goroutines and channels provide efficient concurrency for real-time systems. Chat applications, live dashboards, and real-time analytics platforms leverage Go's concurrency model for handling thousands of simultaneous connections.

Use Case 4: Infrastructure Tools

Go dominates the infrastructure tooling space. Docker containers, Kubernetes orchestration, Terraform infrastructure-as-code, and Prometheus monitoring are all written in Go, proving its reliability for critical infrastructure.

Best Practices for Production

  1. Use context for cancellation and timeouts: Pass context.Context through your call chain. Use context.WithTimeout for operations that might hang, and check ctx.Err() for cancellation.

  2. Handle errors explicitly: Never ignore errors. Check every error return value. Wrap errors with fmt.Errorf("...: %w", err) for context. Use errors.Is and errors.As for error matching.

  3. Use graceful shutdown: Handle SIGTERM and SIGINT signals to gracefully drain connections before shutting down. Use server.Shutdown(ctx) with a timeout.

  4. Set server timeouts: Configure ReadTimeout, WriteTimeout, and IdleTimeout on http.Server to prevent slow clients from holding connections indefinitely.

  5. Use structured logging: Use log/slog (Go 1.21+) or a structured logging library like zerolog for machine-parseable log output.

  6. Validate input: Validate all incoming request data. Use struct tags and validation libraries for complex validation rules.

  7. Use interfaces for testability: Define interfaces for external dependencies (database, HTTP clients, file system) so you can provide test implementations.

  8. Profile with pprof: Go's built-in net/http/pprof package provides CPU, memory, and goroutine profiling. Import it in development and staging environments.

Common Pitfalls and Solutions

PitfallImpactSolution
Ignoring error returnsSilent failures, data corruptionCheck every error; use linters like errcheck
Goroutine leaksMemory exhaustionUse context.Context for cancellation; monitor goroutine count
Race conditionsData corruption under concurrencyUse go test -race; protect shared state with mutexes or channels
Not closing response bodiesConnection leaksAlways call defer resp.Body.Close() after HTTP requests
Overusing global stateUntestable code, hidden dependenciesUse dependency injection through function parameters or constructors

Performance Optimization

// Performance optimization patterns in Go
 
// 1. Use sync.Pool for frequently allocated objects
var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}
 
func processData(data []byte) []byte {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer bufferPool.Put(buf)
    buf.Reset()
    buf.Write(data)
    // Process...
    return buf.Bytes()
}
 
// 2. Pre-allocate slices when size is known
users := make([]*User, 0, expectedCount) // Avoids repeated allocations
 
// 3. Use buffered channels
ch := make(chan Task, 100) // Buffer prevents goroutine blocking
 
// 4. Connection pooling with database/sql
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
 
// 5. HTTP client with connection pooling
client := &http.Client{
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
    },
    Timeout: 10 * time.Second,
}

Comparison with Alternatives

FeatureGoNode.jsPython (FastAPI)Rust (Actix)
PerformanceExcellentGoodModerateExcellent
ConcurrencyGoroutines (excellent)Event loop (good)Async (moderate)Tokio (excellent)
CompilationFastNo compileNo compileSlow
Binary sizeSmall (~10MB)Requires runtimeRequires runtimeSmall
Learning curveGentleGentleGentleSteep
EcosystemStrongMassiveStrongGrowing
DeploymentSingle binarynpm + runtimepip + runtimeSingle binary
Memory usageLowModerateHighVery low
Type safetyStrongWeak (JS), Moderate (TS)Moderate (type hints)Very strong

Go offers the best balance of performance, simplicity, and deployment ease. Node.js has the largest ecosystem. Python (FastAPI) provides the fastest development speed. Rust offers the best performance but with a steeper learning curve. Choose Go when you need production-ready backend services with excellent concurrency and simple deployment.

Advanced Patterns and Techniques

Dependency Injection with Wire

// Use Google Wire for compile-time dependency injection
package main
 
import "github.com/google/wire"
 
// ProviderSet defines the dependency graph
var ProviderSet = wire.NewSet(
    repository.NewPostgresUserRepo,
    handlers.NewUserHandler,
    NewServer,
)
 
func InitializeServer() (*http.Server, error) {
    wire.Build(ProviderSet)
    return nil, nil
}

Structured Concurrency with errgroup

import "golang.org/x/sync/errgroup"
 
func fetchAllData(ctx context.Context, ids []string) ([]*Data, error) {
    g, ctx := errgroup.WithContext(ctx)
    results := make([]*Data, len(ids))
 
    for i, id := range ids {
        i, id := i, id // Capture loop variables
        g.Go(func() error {
            data, err := fetchData(ctx, id)
            if err != nil {
                return fmt.Errorf("fetch %s: %w", id, err)
            }
            results[i] = data
            return nil
        })
    }
 
    if err := g.Wait(); err != nil {
        return nil, err
    }
 
    return results, nil
}

Testing Strategies

// handlers/user_test.go
package handlers_test
 
import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
    "my-api/handlers"
    "my-api/repository"
)
 
func TestCreateUser(t *testing.T) {
    repo := repository.NewMemoryUserRepo()
    handler := handlers.NewUserHandler(repo)
 
    body := `{"name":"John Doe","email":"john@example.com","age":30}`
    req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewBufferString(body))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()
 
    handler.ServeHTTP(w, req)
 
    if w.Code != http.StatusCreated {
        t.Errorf("expected status %d, got %d", http.StatusCreated, w.Code)
    }
 
    var response map[string]interface{}
    json.NewDecoder(w.Body).Decode(&response)
 
    data := response["data"].(map[string]interface{})
    if data["name"] != "John Doe" {
        t.Errorf("expected name 'John Doe', got %v", data["name"])
    }
}
 
func TestGetUser_NotFound(t *testing.T) {
    repo := repository.NewMemoryUserRepo()
    handler := handlers.NewUserHandler(repo)
 
    req := httptest.NewRequest(http.MethodGet, "/users/nonexistent", nil)
    w := httptest.NewRecorder()
 
    handler.ServeHTTP(w, req)
 
    if w.Code != http.StatusNotFound {
        t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
    }
}
 
// Integration test with testcontainers
func TestUserAPI_Integration(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }
 
    // Start test PostgreSQL container
    // ... setup code ...
 
    // Create server with real repository
    // Make HTTP requests and verify responses
}

Future Outlook

Go continues to evolve with generics (Go 1.18+), improved error handling proposals, and better tooling. The log/slog package standardizes structured logging. Go's module system is now mature and widely adopted. The ecosystem continues to grow with excellent libraries for web development, database access, and cloud services.

Go's position in cloud-native development is secureβ€”it powers Kubernetes, Docker, and most CNCF projects. The language's simplicity and performance make it an excellent choice for new backend services, especially in microservices architectures where fast startup, low memory, and easy deployment are critical.

Conclusion

Go is an excellent language for building REST APIs, offering a perfect balance of performance, simplicity, and developer productivity. Its standard library provides production-ready HTTP capabilities, goroutines enable efficient concurrency, and single-binary deployment simplifies operations. The key takeaways are: Go's standard library net/http package is sufficient for building production APIs without external dependencies.

Interfaces enable testable, decoupled code through the repository and service layer patterns. Goroutines and channels provide elegant concurrency for handling parallel requests and background tasks. Start by creating a simple API with the standard library, add middleware for logging and CORS, and gradually adopt more advanced patterns like dependency injection and structured concurrency. The Go documentation at go.dev/doc and the Go by Example site provide excellent learning resources.