본문으로 건너뛰기 Go Web Development Guide | REST APIs· Middleware

Go Web Development Guide | REST APIs· Middleware

Go Web Development Guide | REST APIs· Middleware

이 글의 핵심

Go's standard library is powerful enough to build production web APIs without a framework. This guide covers routing, middleware, database integration, and deployment ??from first endpoint to production.

What This Guide Covers

Go’s standard library ships a production-capable HTTP server. This guide builds a complete REST API from scratch ??routing, middleware, JSON, database, auth ??then covers deployment.

Real-world insight: Rewriting a Node.js service in Go cut memory from 512MB to 28MB and latency p99 from 180ms to 12ms ??with fewer lines of code.


Setup

go mod init github.com/yourname/myapi

No external dependencies required for a basic server.


1. Basic HTTP Server

package main

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

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
    })

    mux.HandleFunc("GET /hello/{name}", func(w http.ResponseWriter, r *http.Request) {
        name := r.PathValue("name")  // Go 1.22+
        fmt.Fprintf(w, "Hello, %s!\n", name)
    })

    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

Go 1.22 added method and path parameter support to ServeMux ??no router library needed for most APIs.


2. JSON Request and Response

type CreateUserRequest struct {
    Name  string `json:"name"  validate:"required"`
    Email string `json:"email" validate:"required,email"`
}

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

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

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

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }
    if req.Name == "" || req.Email == "" {
        writeError(w, http.StatusBadRequest, "name and email are required")
        return
    }
    user := UserResponse{ID: 1, Name: req.Name, Email: req.Email}
    writeJSON(w, http.StatusCreated, user)
}

3. Middleware

Middleware wraps http.Handler ??chain them for logging, auth, CORS, etc.

// Logging middleware
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 %v", r.Method, r.URL.Path, time.Since(start))
    })
}

// Auth middleware
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            writeError(w, http.StatusUnauthorized, "missing token")
            return
        }
        // validate token...
        next.ServeHTTP(w, r)
    })
}

// CORS middleware
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, Authorization")
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusNoContent)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Chain middleware
func chain(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middleware) - 1; i >= 0; i-- {
        h = middleware[i](h)
    }
    return h
}

// Usage
mux.Handle("/api/", chain(apiHandler, loggingMiddleware, corsMiddleware, authMiddleware))

4. Context

Use context.Context to pass request-scoped values and handle cancellation:

type contextKey string
const userKey contextKey = "user"

// Set in middleware
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user := validateToken(r.Header.Get("Authorization"))
        ctx := context.WithValue(r.Context(), userKey, user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Read in handler
func profileHandler(w http.ResponseWriter, r *http.Request) {
    user, ok := r.Context().Value(userKey).(*User)
    if !ok {
        writeError(w, http.StatusUnauthorized, "not authenticated")
        return
    }
    writeJSON(w, http.StatusOK, user)
}

5. Database with GORM

go get gorm.io/gorm gorm.io/driver/postgres
import (
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type User struct {
    gorm.Model
    Name  string `gorm:"not null"`
    Email string `gorm:"unique;not null"`
}

func initDB() (*gorm.DB, error) {
    dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable",
        os.Getenv("DB_HOST"),
        os.Getenv("DB_USER"),
        os.Getenv("DB_PASS"),
        os.Getenv("DB_NAME"),
    )
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        return nil, err
    }
    db.AutoMigrate(&User{})
    return db, nil
}

// CRUD
db.Create(&user)
db.First(&user, id)
db.Where("email = ?", email).First(&user)
db.Save(&user)
db.Delete(&user, id)

Dependency injection via struct

type UserHandler struct {
    db *gorm.DB
}

func (h *UserHandler) GetAll(w http.ResponseWriter, r *http.Request) {
    var users []User
    h.db.Find(&users)
    writeJSON(w, http.StatusOK, users)
}

func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
    var user User
    json.NewDecoder(r.Body).Decode(&user)
    h.db.Create(&user)
    writeJSON(w, http.StatusCreated, user)
}

// Wire up
h := &UserHandler{db: db}
mux.HandleFunc("GET /users", h.GetAll)
mux.HandleFunc("POST /users", h.Create)

6. JWT Authentication

go get github.com/golang-jwt/jwt/v5
import "github.com/golang-jwt/jwt/v5"

var jwtSecret = []byte(os.Getenv("JWT_SECRET"))

func generateToken(userID uint) (string, error) {
    claims := jwt.MapClaims{
        "sub": userID,
        "exp": time.Now().Add(24 * time.Hour).Unix(),
    }
    return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(jwtSecret)
}

func validateToken(tokenStr string) (*jwt.MapClaims, error) {
    token, err := jwt.ParseWithClaims(tokenStr, &jwt.MapClaims{},
        func(t *jwt.Token) (any, error) {
            if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
                return nil, fmt.Errorf("unexpected signing method")
            }
            return jwtSecret, nil
        },
    )
    if err != nil || !token.Valid {
        return nil, err
    }
    claims, _ := token.Claims.(*jwt.MapClaims)
    return claims, nil
}

7. Error Handling Pattern

type AppError struct {
    Code    int
    Message string
    Err     error
}

func (e *AppError) Error() string { return e.Message }

type HandlerFunc func(w http.ResponseWriter, r *http.Request) error

func handle(fn HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        if err := fn(w, r); err != nil {
            var appErr *AppError
            if errors.As(err, &appErr) {
                writeError(w, appErr.Code, appErr.Message)
            } else {
                log.Printf("internal error: %v", err)
                writeError(w, http.StatusInternalServerError, "internal server error")
            }
        }
    }
}

// Usage ??return errors instead of writing responses inline
mux.HandleFunc("GET /users/{id}", handle(func(w http.ResponseWriter, r *http.Request) error {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        return &AppError{Code: 400, Message: "invalid id"}
    }
    var user User
    if result := db.First(&user, id); result.Error != nil {
        return &AppError{Code: 404, Message: "user not found"}
    }
    return writeJSON(w, 200, user)
}))

8. Graceful Shutdown

func main() {
    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    go func() {
        log.Println("Starting on :8080")
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(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()
    server.Shutdown(ctx)
}

9. Docker Deployment

FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server ./cmd/server

FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

Final image is ~10MB with no runtime dependencies.


Key Takeaways

TopicKey point
RoutingGo 1.22 ServeMux handles method + path params natively
MiddlewareWrap http.Handler ??compose with a chain helper
JSONencoding/json ??decode into structs, encode responses
ContextPass auth user and request values via r.Context()
DatabaseGORM for productivity; database/sql for full control
Error handlingReturn errors from handlers; central error middleware
DeploymentMulti-stage Docker build ??~10MB scratch image

Go’s web development story is unusually clean: the standard library handles most needs, goroutines give you concurrency for free, and the compiled binary deploys without a runtime. Start with net/http, add a router when you need it, and reach for frameworks only when the project complexity justifies it.


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Go’s standard library is powerful enough to build production web APIs without a framework. This guide covers routing, middleware, database integration, and deployment ??from first endpoint to production.

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Go, Golang, Web, Web-API, Backend, REST.