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
| 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.
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.
Related Articles (Internal Links)
Other articles related to this topic.
- Docker Compose Complete Guide | Multi-Container Apps
- gRPC Complete Guide | Protocol Buffers· Service Definitions
Keywords Covered in This Article (Related Search Terms)
This article covers Go, Golang, Web, Web-API, Backend, REST.