Building High-Performance APIs with Go

go api

Go was designed for building services at scale. Its simplicity, fast compilation, and goroutines make it ideal for high-performance APIs. Here’s a practical guide.

Why Go for APIs

package main

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

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

func handleHealth(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

No framework needed. Batteries included.

Project Structure

myapi/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   ├── handlers/
│   │   └── user.go
│   ├── models/
│   │   └── user.go
│   ├── repository/
│   │   └── user.go
│   └── middleware/
│       └── auth.go
├── pkg/
│   └── response/
│       └── response.go
├── go.mod
└── go.sum

Clean separation. internal/ is Go’s convention for private packages.

Basic API Structure

Main Entry

// cmd/server/main.go
package main

import (
    "log"
    "net/http"
    "os"
    
    "myapi/internal/handlers"
    "myapi/internal/middleware"
)

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    
    mux := http.NewServeMux()
    
    // Routes
    mux.HandleFunc("/api/users", handlers.HandleUsers)
    mux.HandleFunc("/api/users/", handlers.HandleUser)
    
    // Middleware chain
    handler := middleware.Logging(middleware.Auth(mux))
    
    log.Printf("Server starting on :%s", port)
    log.Fatal(http.ListenAndServe(":"+port, handler))
}

Handler

// internal/handlers/user.go
package handlers

import (
    "encoding/json"
    "net/http"
    "strings"
    
    "myapi/internal/models"
    "myapi/internal/repository"
    "myapi/pkg/response"
)

func HandleUsers(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        listUsers(w, r)
    case http.MethodPost:
        createUser(w, r)
    default:
        response.Error(w, http.StatusMethodNotAllowed, "Method not allowed")
    }
}

func listUsers(w http.ResponseWriter, r *http.Request) {
    users, err := repository.GetAllUsers()
    if err != nil {
        response.Error(w, http.StatusInternalServerError, "Failed to fetch users")
        return
    }
    response.JSON(w, http.StatusOK, users)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var user models.User
    if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
        response.Error(w, http.StatusBadRequest, "Invalid JSON")
        return
    }
    
    if err := user.Validate(); err != nil {
        response.Error(w, http.StatusBadRequest, err.Error())
        return
    }
    
    created, err := repository.CreateUser(&user)
    if err != nil {
        response.Error(w, http.StatusInternalServerError, "Failed to create user")
        return
    }
    
    response.JSON(w, http.StatusCreated, created)
}

Response Helper

// pkg/response/response.go
package response

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

func JSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

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

Middleware

// internal/middleware/auth.go
package middleware

import (
    "context"
    "log"
    "net/http"
    "strings"
    "time"
)

type contextKey string

const UserIDKey contextKey = "userID"

func Logging(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 Auth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Skip auth for public endpoints
        if strings.HasPrefix(r.URL.Path, "/api/public") {
            next.ServeHTTP(w, r)
            return
        }
        
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        
        userID, err := validateToken(token)
        if err != nil {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }
        
        ctx := context.WithValue(r.Context(), UserIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Goroutines for Performance

Concurrent Requests

func fetchUserWithDetails(userID string) (*UserDetails, error) {
    var user *User
    var orders []*Order
    var preferences *Preferences
    
    var wg sync.WaitGroup
    errs := make(chan error, 3)
    
    wg.Add(3)
    
    go func() {
        defer wg.Done()
        var err error
        user, err = repository.GetUser(userID)
        if err != nil {
            errs <- err
        }
    }()
    
    go func() {
        defer wg.Done()
        var err error
        orders, err = repository.GetUserOrders(userID)
        if err != nil {
            errs <- err
        }
    }()
    
    go func() {
        defer wg.Done()
        var err error
        preferences, err = repository.GetUserPreferences(userID)
        if err != nil {
            errs <- err
        }
    }()
    
    wg.Wait()
    close(errs)
    
    for err := range errs {
        if err != nil {
            return nil, err
        }
    }
    
    return &UserDetails{
        User:        user,
        Orders:      orders,
        Preferences: preferences,
    }, nil
}

Three requests in parallel instead of sequential.

Worker Pool

func processJobs(jobs <-chan Job, results chan<- Result) {
    numWorkers := runtime.NumCPU()
    var wg sync.WaitGroup
    
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                result := processJob(job)
                results <- result
            }
        }()
    }
    
    wg.Wait()
    close(results)
}

Database Access

// internal/repository/user.go
package repository

import (
    "database/sql"
    "myapi/internal/models"
)

var db *sql.DB

func InitDB(dataSourceName string) error {
    var err error
    db, err = sql.Open("postgres", dataSourceName)
    if err != nil {
        return err
    }
    
    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(5)
    
    return db.Ping()
}

func GetUser(id string) (*models.User, error) {
    user := &models.User{}
    err := db.QueryRow(
        "SELECT id, email, name, created_at FROM users WHERE id = $1",
        id,
    ).Scan(&user.ID, &user.Email, &user.Name, &user.CreatedAt)
    
    if err == sql.ErrNoRows {
        return nil, nil
    }
    if err != nil {
        return nil, err
    }
    return user, nil
}

func CreateUser(user *models.User) (*models.User, error) {
    err := db.QueryRow(
        `INSERT INTO users (email, name) VALUES ($1, $2)
         RETURNING id, created_at`,
        user.Email, user.Name,
    ).Scan(&user.ID, &user.CreatedAt)
    
    if err != nil {
        return nil, err
    }
    return user, nil
}

Graceful Shutdown

func main() {
    srv := &http.Server{
        Addr:    ":8080",
        Handler: setupRouter(),
    }
    
    go func() {
        if err := srv.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...")
    
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("Shutdown error: %v", err)
    }
    
    log.Println("Server stopped")
}

Performance Tips

1. Use sync.Pool

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
    buf := bufferPool.Get().(*bytes.Buffer)
    buf.Reset()
    defer bufferPool.Put(buf)
    
    // Use buf...
}

2. Streaming JSON

func streamLargeResponse(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Write([]byte("["))
    
    first := true
    for item := range items {
        if !first {
            w.Write([]byte(","))
        }
        first = false
        json.NewEncoder(w).Encode(item)
    }
    
    w.Write([]byte("]"))
}

3. Use a Router Library

// Chi router example
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)

r.Route("/api/users", func(r chi.Router) {
    r.Get("/", listUsers)
    r.Post("/", createUser)
    r.Route("/{userID}", func(r chi.Router) {
        r.Get("/", getUser)
        r.Put("/", updateUser)
        r.Delete("/", deleteUser)
    })
})

Final Thoughts

Go’s simplicity is its strength. No magic, no hidden behavior—just explicit, readable code that compiles fast and runs faster.

For high-throughput APIs, Go delivers.


Simple code, fast execution, happy operators.

All posts