Go Web Development Guide | REST APIs, Middleware, and Production
이 글의 핵심
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
| Topic | Key point |
|---|---|
| Routing | Go 1.22 ServeMux handles method + path params natively |
| Middleware | Wrap http.Handler — compose with a chain helper |
| JSON | encoding/json — decode into structs, encode responses |
| Context | Pass auth user and request values via r.Context() |
| Database | GORM for productivity; database/sql for full control |
| Error handling | Return errors from handlers; central error middleware |
| Deployment | Multi-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.