Python

Go Web Development: net/http & Gin Framework

Panduan lengkap Go Web Development — membangun web server dengan net/http, routing, middleware, JSON API, dan Gin framework untuk REST API production-ready

1. 🌐 Pengenalan Web Development di Go

Go memiliki standard library yang sangat kuat untuk web development. Package net/http bisa digunakan untuk membuat web server production-ready tanpa framework pihak ketiga. Namun, framework seperti Gin, Echo, dan Fiber menyediakan fitur tambahan yang mempercepat development.

Go Web Framework Populer

Framework Kelebihan Stars GitHub
net/httpBuilt-in, zero dependency, sangat stabilStandard Library
GinCepat, API clean, middleware lengkap, komunitas besar78k+
EchoPerforma tinggi, fitur lengkap, auto TLS30k+
FiberInspired Express.js, sangat cepat (fasthttp)33k+
ChiLightweight, composable, net/http compatible18k+
Gorilla MuxRouter powerful, banyak fitur URL matching20k+

Kapan Pakai Framework vs Standard Library?

Aspek net/http (stdlib) Framework (Gin, dll)
DependencyZero external depsTambah dependency
RoutingBasic (path matching)Advanced (path params, groups)
JSON BindingManual parsingAuto binding + validation
MiddlewareManual implementasiBuilt-in + banyak pihak ketiga
Performa🟢 Sangat cepat🟢 Sangat cepat (Gin/Fiber)
Cocok untukMicroservice sederhana, libraryREST API kompleks, production
Diagram: Go Web Architecture
┌─────────────────────────────────────────────────────────┐
│                  GO WEB SERVER                          │
│                                                         │
│  Client ──→ HTTP Request                                │
│              │                                          │
│              ▼                                          │
│  ┌─────────────────┐                                    │
│  │   Router         │ ← URL matching, path params       │
│  └────────┬────────┘                                    │
│           ▼                                             │
│  ┌─────────────────┐                                    │
│  │   Middleware      │ ← Auth, CORS, Logging, Rate Limit │
│  └────────┬────────┘                                    │
│           ▼                                             │
│  ┌─────────────────┐                                    │
│  │   Handler         │ ← Business logic                 │
│  └────────┬────────┘                                    │
│           ▼                                             │
│  ┌─────────────────┐                                    │
│  │   Response        │ ← JSON, HTML, File               │
│  └────────┬────────┘                                    │
│           ▼                                             │
│  Client ←── HTTP Response                               │
└─────────────────────────────────────────────────────────┘

2. net/http: Web Server Dasar

Package net/http adalah standard library Go untuk HTTP. Sangat powerful — bahkan bisa untuk production tanpa framework.

Go — Hello Web Server
package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)

// Handler function — menerima ResponseWriter dan Request
func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Halo, dunia! 🌐\n")
    fmt.Fprintf(w, "Method: %s\n", r.Method)
    fmt.Fprintf(w, "URL: %s\n", r.URL.Path)
    fmt.Fprintf(w, "Time: %s\n", time.Now().Format(time.RFC3339))
}

func aboutHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Tentang BeebaneLabs\n")
    fmt.Fprintf(w, "Tutorial Go Web Development")
}

func main() {
    // Register handlers
    http.HandleFunc("/", helloHandler)
    http.HandleFunc("/about", aboutHandler)

    // Start server
    fmt.Println("🚀 Server berjalan di http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

// Jalankan: go run main.go
// Buka: http://localhost:8080
// Buka: http://localhost:8080/about

Serving Static Files & HTML

Go — Static Files & HTML
package main

import (
    "html/template"
    "log"
    "net/http"
)

// Struct untuk template data
type PageData struct {
    Title   string
    Message string
    Items   []string
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    // Parse template
    tmpl, err := template.ParseFiles("templates/index.html")
    if err != nil {
        http.Error(w, "Template tidak ditemukan", http.StatusInternalServerError)
        return
    }

    // Data untuk template
    data := PageData{
        Title:   "Beranda",
        Message: "Selamat datang di Go Web!",
        Items:   []string{"Go", "Gin", "net/http", "REST API"},
    }

    tmpl.Execute(w, data)
}

func main() {
    // Serve static files (CSS, JS, images)
    fs := http.FileServer(http.Dir("./static"))
    http.Handle("/static/", http.StripPrefix("/static/", fs))

    http.HandleFunc("/", homeHandler)

    log.Println("🚀 Server di http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Custom Server Configuration

Go — Custom Server
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "time"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("OK"))
    })

    // Custom server dengan konfigurasi
    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
    }

    // Graceful shutdown
    go func() {
        log.Println("🚀 Server berjalan di :8080")
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    // Tunggu signal untuk shutdown
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt)
    <-quit

    log.Println("⏳ Shutting down...")
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    server.Shutdown(ctx)
    log.Println("✅ Server stopped gracefully")
}

3. Routing dengan net/http

Sejak Go 1.22, net/http mendukung method-based routing dan path parameters secara built-in!

Go — HTTP Routing (Go 1.22+)
package main

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

type Response struct {
    Status  string      `json:"status"`
    Message string      `json:"message,omitempty"`
    Data    interface{} `json:"data,omitempty"`
}

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

    // Method-based routing (Go 1.22+)
    mux.HandleFunc("GET /api/users", getUsers)
    mux.HandleFunc("POST /api/users", createUser)
    mux.HandleFunc("GET /api/users/{id}", getUserByID)
    mux.HandleFunc("PUT /api/users/{id}", updateUser)
    mux.HandleFunc("DELETE /api/users/{id}", deleteUser)

    // Health check
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        json.NewEncoder(w).Encode(Response{Status: "ok"})
    })

    log.Println("🚀 API Server di http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

func getUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(Response{
        Status: "ok",
        Data:   []string{"Budi", "Ani", "Dimas"},
    })
}

func createUser(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(Response{
        Status:  "ok",
        Message: "User berhasil dibuat",
    })
}

func getUserByID(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    id := r.PathValue("id") // Path parameter (Go 1.22+)
    json.NewEncoder(w).Encode(Response{
        Status: "ok",
        Data:   map[string]string{"id": id, "name": "Budi"},
    })
}

func updateUser(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    id := r.PathValue("id")
    json.NewEncoder(w).Encode(Response{
        Status:  "ok",
        Message: fmt.Sprintf("User %s berhasil diupdate", id),
    })
}

func deleteUser(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    id := r.PathValue("id")
    json.NewEncoder(w).Encode(Response{
        Status:  "ok",
        Message: fmt.Sprintf("User %s berhasil dihapus", id),
    })
}

4. JSON API dengan net/http

Go memiliki package encoding/json untuk serialisasi dan deserialisasi JSON. Digunakan bersama net/http untuk membuat REST API.

Go — JSON API Lengkap
package main

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

// Model
type Product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
    Stock int     `json:"stock"`
}

// In-memory database
var (
    products = map[int]*Product{
        1: {ID: 1, Name: "Laptop", Price: 15000000, Stock: 10},
        2: {ID: 2, Name: "Mouse", Price: 150000, Stock: 50},
    }
    mu       sync.RWMutex
    nextID   = 3
)

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

// Helper: error response
func errorResponse(w http.ResponseWriter, status int, message string) {
    jsonResponse(w, status, map[string]string{
        "error": message,
    })
}

func productHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        mu.RLock()
        defer mu.RUnlock()
        list := make([]*Product, 0, len(products))
        for _, p := range products {
            list = append(list, p)
        }
        jsonResponse(w, http.StatusOK, list)

    case http.MethodPost:
        var p Product
        if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
            errorResponse(w, http.StatusBadRequest, "Invalid JSON")
            return
        }
        mu.Lock()
        p.ID = nextID
        nextID++
        products[p.ID] = &p
        mu.Unlock()
        jsonResponse(w, http.StatusCreated, p)

    default:
        errorResponse(w, http.StatusMethodNotAllowed, "Method not allowed")
    }
}

func productByIDHandler(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        errorResponse(w, http.StatusBadRequest, "Invalid ID")
        return
    }

    switch r.Method {
    case http.MethodGet:
        mu.RLock()
        p, ok := products[id]
        mu.RUnlock()
        if !ok {
            errorResponse(w, http.StatusNotFound, "Product not found")
            return
        }
        jsonResponse(w, http.StatusOK, p)

    case http.MethodDelete:
        mu.Lock()
        if _, ok := products[id]; !ok {
            mu.Unlock()
            errorResponse(w, http.StatusNotFound, "Product not found")
            return
        }
        delete(products, id)
        mu.Unlock()
        jsonResponse(w, http.StatusOK, map[string]string{
            "message": fmt.Sprintf("Product %d deleted", id),
        })

    default:
        errorResponse(w, http.StatusMethodNotAllowed, "Method not allowed")
    }
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /api/products", productHandler)
    mux.HandleFunc("POST /api/products", productHandler)
    mux.HandleFunc("GET /api/products/{id}", productByIDHandler)
    mux.HandleFunc("DELETE /api/products/{id}", productByIDHandler)

    log.Println("🚀 Product API di http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

5. Middleware Pattern

Middleware adalah fungsi yang membungkus handler untuk menambahkan behavior seperti logging, authentication, CORS, dan rate limiting.

Go — Middleware dengan net/http
package main

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

// Middleware type
type Middleware func(http.Handler) http.Handler

// Logging middleware
func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("→ %s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
        log.Printf("← %s %s (%v)", r.Method, r.URL.Path, time.Since(start))
    })
}

// CORS middleware
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")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Auth middleware (sederhana)
func Auth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        // Validasi token di sini...
        next.ServeHTTP(w, r)
    })
}

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

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

    mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Public endpoint 🌐"))
    })

    mux.HandleFunc("GET /admin", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Admin area 🔒"))
    })

    // Apply middleware
    logged := Chain(mux, Logging, CORS)

    log.Println("🚀 Server di http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", logged))
}

6. Gin Framework: Pengenalan

Gin adalah HTTP web framework paling populer untuk Go. Menyediakan routing, middleware, JSON binding, validation, dan banyak lagi dengan API yang sangat clean.

Instalasi Gin

Terminal
# Inisialisasi module
go mod init my-api

# Install Gin
go get -u github.com/gin-gonic/gin

# Verifikasi di go.mod:
# require github.com/gin-gonic/gin v1.10.0

Hello World dengan Gin

Go — Gin Hello World
package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    // Buat Gin router (default dengan Logger & Recovery)
    r := gin.Default()

    // Route sederhana
    r.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "Halo, Gin! 🐝",
            "status":  "ok",
        })
    })

    // HTML response
    r.GET("/hello", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.html", gin.H{
            "title": "Hello Gin",
        })
    })

    // String response
    r.GET("/ping", func(c *gin.Context) {
        c.String(http.StatusOK, "pong 🏓")
    })

    // Redirect
    r.GET("/old-path", func(c *gin.Context) {
        c.Redirect(http.StatusMovedPermanently, "/")
    })

    r.Run(":8080") // Start server
}

7. Routing dengan Gin

Go — Gin Routing
package main

import (
    "fmt"
    "net/http"
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    // Path parameters
    r.GET("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.JSON(http.StatusOK, gin.H{"user_id": id})
    })

    // Query parameters: /search?q=golang&page=1
    r.GET("/search", func(c *gin.Context) {
        query := c.DefaultQuery("q", "")
        page := c.DefaultQuery("page", "1")
        c.JSON(http.StatusOK, gin.H{
            "query": query,
            "page":  page,
        })
    })

    // Multiple path parameters
    r.GET("/users/:userId/posts/:postId", func(c *gin.Context) {
        userID := c.Param("userId")
        postID := c.Param("postId")
        c.JSON(http.StatusOK, gin.H{
            "user_id": userID,
            "post_id": postID,
        })
    })

    // Route groups — organisasi URL
    v1 := r.Group("/api/v1")
    {
        v1.GET("/users", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"version": "v1", "resource": "users"})
        })
        v1.GET("/products", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"version": "v1", "resource": "products"})
        })
    }

    v2 := r.Group("/api/v2")
    {
        v2.GET("/users", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"version": "v2", "resource": "users"})
        })
    }

    // Nested groups
    admin := r.Group("/admin")
    admin.Use(func(c *gin.Context) {
        // Auth middleware untuk admin
        token := c.GetHeader("X-Admin-Token")
        if token != "secret" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                "error": "Unauthorized",
            })
            return
        }
        c.Next()
    })
    {
        admin.GET("/dashboard", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"message": "Admin Dashboard"})
        })
        admin.GET("/settings", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"message": "Admin Settings"})
        })
    }

    // No route handler
    r.NoRoute(func(c *gin.Context) {
        c.JSON(http.StatusNotFound, gin.H{
            "error": fmt.Sprintf("Route %s not found", c.Request.URL.Path),
        })
    })

    r.Run(":8080")
}

8. JSON API dengan Gin

Gin menyediakan JSON binding dan validation yang sangat mudah digunakan. Tidak perlu parsing manual!

Go — Gin JSON Binding
package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator/v10"
)

// Request struct dengan validation tags
type CreateProductReq struct {
    Name  string  `json:"name" binding:"required,min=2,max=100"`
    Price float64 `json:"price" binding:"required,gt=0"`
    Stock int     `json:"stock" binding:"gte=0,lte=10000"`
    Category string `json:"category" binding:"required,oneof=electronics food clothing"`
}

// Response struct
type ProductResp struct {
    ID       int     `json:"id"`
    Name     string  `json:"name"`
    Price    float64 `json:"price"`
    Stock    int     `json:"stock"`
    Category string  `json:"category"`
}

func main() {
    r := gin.Default()

    // POST — JSON binding dengan validasi
    r.POST("/api/products", func(c *gin.Context) {
        var req CreateProductReq

        if err := c.ShouldBindJSON(&req); err != nil {
            // Format validation errors
            errors := make(map[string]string)
            if ve, ok := err.(validator.ValidationErrors); ok {
                for _, e := range ve {
                    errors[e.Field()] = validationMessage(e)
                }
            }
            c.JSON(http.StatusBadRequest, gin.H{"errors": errors})
            return
        }

        product := ProductResp{
            ID:       1,
            Name:     req.Name,
            Price:    req.Price,
            Stock:    req.Stock,
            Category: req.Category,
        }

        c.JSON(http.StatusCreated, gin.H{
            "status":  "success",
            "product": product,
        })
    })

    // GET — Query binding
    r.GET("/api/products", func(c *gin.Context) {
        var req struct {
            Category string `form:"category"`
            MinPrice float64 `form:"min_price"`
            MaxPrice float64 `form:"max_price"`
            Page     int     `form:"page,default=1"`
            Limit    int     `form:"limit,default=10"`
        }

        if err := c.ShouldBindQuery(&req); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        c.JSON(http.StatusOK, gin.H{
            "filters": req,
            "products": []ProductResp{
                {ID: 1, Name: "Laptop", Price: 15000000, Stock: 10, Category: "electronics"},
                {ID: 2, Name: "Mouse", Price: 150000, Stock: 50, Category: "electronics"},
            },
        })
    })

    r.Run(":8080")
}

func validationMessage(e validator.FieldError) string {
    switch e.Tag() {
    case "required":
        return "Field ini wajib diisi"
    case "min":
        return "Minimal " + e.Param() + " karakter"
    case "max":
        return "Maksimal " + e.Param() + " karakter"
    case "gt":
        return "Harus lebih besar dari " + e.Param()
    case "oneof":
        return "Harus salah satu dari: " + e.Param()
    default:
        return "Validasi gagal: " + e.Tag()
    }
}

9. Middleware di Gin

Go — Gin Middleware
package main

import (
    "log"
    "net/http"
    "time"
    "github.com/gin-gonic/gin"
)

// Custom logger middleware
func RequestLogger() gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()
        path := c.Request.URL.Path

        c.Next() // Proses request

        latency := time.Since(start)
        status := c.Writer.Status()

        log.Printf("[%d] %s %s (%v)",
            status, c.Request.Method, path, latency)
    }
}

// Auth middleware
func AuthRequired() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                "error": "Token diperlukan",
            })
            return
        }
        // Simulasi validasi token
        if token != "Bearer my-secret-token" {
            c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
                "error": "Token tidak valid",
            })
            return
        }
        // Set user info ke context
        c.Set("userID", 1)
        c.Set("userRole", "admin")
        c.Next()
    }
}

// Rate limiter middleware (sederhana)
func RateLimit(requests int, duration time.Duration) gin.HandlerFunc {
    // Implementasi rate limiting di sini
    return func(c *gin.Context) {
        c.Next()
    }
}

func main() {
    r := gin.New() // Tanpa default middleware

    // Global middleware
    r.Use(gin.Recovery())    // Panic recovery
    r.Use(RequestLogger())   // Request logging

    // Public routes
    r.GET("/api/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "healthy"})
    })

    r.POST("/api/login", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"token": "my-secret-token"})
    })

    // Protected routes
    api := r.Group("/api", AuthRequired())
    {
        api.GET("/profile", func(c *gin.Context) {
            userID, _ := c.Get("userID")
            userRole, _ := c.Get("userRole")
            c.JSON(http.StatusOK, gin.H{
                "user_id": userID,
                "role":    userRole,
            })
        })

        api.PUT("/profile", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"message": "Profile updated"})
        })
    }

    r.Run(":8080")
}

10. Membangun REST API Lengkap

Mari gabungkan semua yang sudah dipelajari untuk membangun REST API CRUD yang production-ready.

Go — Full REST API
package main

import (
    "net/http"
    "strconv"
    "sync"
    "time"
    "github.com/gin-gonic/gin"
)

// === MODELS ===
type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name" binding:"required"`
    Email     string    `json:"email" binding:"required,email"`
    CreatedAt time.Time `json:"created_at"`
}

// === STORE (in-memory) ===
type UserStore struct {
    mu     sync.RWMutex
    users  map[int]*User
    nextID int
}

func NewUserStore() *UserStore {
    return &UserStore{
        users:  make(map[int]*User),
        nextID: 1,
    }
}

func (s *UserStore) GetAll() []*User {
    s.mu.RLock()
    defer s.mu.RUnlock()
    list := make([]*User, 0, len(s.users))
    for _, u := range s.users {
        list = append(list, u)
    }
    return list
}

func (s *UserStore) GetByID(id int) (*User, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    u, ok := s.users[id]
    return u, ok
}

func (s *UserStore) Create(u *User) {
    s.mu.Lock()
    defer s.mu.Unlock()
    u.ID = s.nextID
    u.CreatedAt = time.Now()
    s.users[u.ID] = u
    s.nextID++
}

func (s *UserStore) Delete(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    if _, ok := s.users[id]; !ok {
        return false
    }
    delete(s.users, id)
    return true
}

// === HANDLERS ===
type UserHandler struct {
    store *UserStore
}

func (h *UserHandler) List(c *gin.Context) {
    users := h.store.GetAll()
    c.JSON(http.StatusOK, gin.H{
        "count": len(users),
        "users": users,
    })
}

func (h *UserHandler) Get(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
        return
    }
    user, ok := h.store.GetByID(id)
    if !ok {
        c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
        return
    }
    c.JSON(http.StatusOK, user)
}

func (h *UserHandler) Create(c *gin.Context) {
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    h.store.Create(&user)
    c.JSON(http.StatusCreated, user)
}

func (h *UserHandler) Delete(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
        return
    }
    if !h.store.Delete(id) {
        c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
        return
    }
    c.JSON(http.StatusOK, gin.H{"message": "User deleted"})
}

// === MAIN ===
func main() {
    store := NewUserStore()
    handler := &UserHandler{store: store}

    r := gin.Default()

    // CORS middleware
    r.Use(func(c *gin.Context) {
        c.Header("Access-Control-Allow-Origin", "*")
        c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
        c.Next()
    })

    // Routes
    api := r.Group("/api/v1")
    {
        api.GET("/users", handler.List)
        api.GET("/users/:id", handler.Get)
        api.POST("/users", handler.Create)
        api.DELETE("/users/:id", handler.Delete)
    }

    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "status": "ok",
            "time":   time.Now().Format(time.RFC3339),
        })
    })

    r.Run(":8080")
}

// Test dengan curl:
// GET    → curl http://localhost:8080/api/v1/users
// POST   → curl -X POST http://localhost:8080/api/v1/users -d '{"name":"Budi","email":"budi@mail.com"}'
// GET    → curl http://localhost:8080/api/v1/users/1
// DELETE → curl -X DELETE http://localhost:8080/api/v1/users/1
💡 Tips: Struktur Proyek Web Go

Untuk proyek yang lebih besar, pisahkan kode menjadi package: handlers/, models/, middleware/, store/, routes/. Gunakan dependency injection untuk memudahkan testing dan menghindari tight coupling antar komponen.

11. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Go Web Development:

Pertanyaan 1: Package apa yang digunakan untuk membuat web server di Go tanpa framework?

a) go/web
b) net/http
c) http/server
d) web/gin

Pertanyaan 2: Bagaimana cara mengirim JSON response di Gin?

a) c.Write(json)
b) c.JSON(statusCode, data)
c) c.Response(json)
d) c.Send(json)

Pertanyaan 3: Apa fungsi dari middleware dalam web development?

a) Mengganti database
b) Menambah behavior sebelum/sesudah handler (logging, auth, CORS)
c) Mengkompilasi kode lebih cepat
d) Menghapus route yang tidak dipakai

Pertanyaan 4: Bagaimana cara mengambil path parameter id dari URL /users/42 di Gin?

a) c.Query("id")
b) c.Param("id")
c) c.GetHeader("id")
d) c.FormValue("id")

Pertanyaan 5: Method Gin apa yang digunakan untuk mem-parse JSON dari request body?

a) c.ParseJSON()
b) c.ShouldBindJSON(&req)
c) c.DecodeJSON()
d) c.ReadJSON()
🔍 Zoom
100%
🎨 Tema