Gin #

Gin adalah framework HTTP paling populer di ekosistem Go. Dengan API yang minimal, performa tinggi (berkat httprouter), dan ekosistem middleware yang matang, Gin menjadi pilihan default untuk membangun REST API dan microservice di Go. Docker Compose melengkapi Gin dengan cara menyediakan environment lokal yang konsisten — aplikasi, database, dan cache berjalan dalam container yang sama sehingga “works on my machine” tidak terjadi lagi.

Artikel ini membahas setup lengkap Docker Compose untuk local development Gin, dari Dockerfile multi-stage hingga integrasi dengan database, ORM, dan hot reload. Setelah membaca artikel ini, kamu akan punya template yang bisa langsung dipakai untuk project Gin baru maupun yang sudah berjalan.

Prasyarat #

Pastikan sudah terinstall di host:

  • Docker dan Docker Compose versi terbaru
  • Go 1.22+ (opsional, untuk development di host tanpa Docker)
  • Git untuk clone repository

Struktur project Gin standar dengan layout cmd/ + internal/:

my-gin-app/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── handler/
│   │   └── user.go
│   ├── service/
│   │   └── user.go
│   ├── repository/
│   │   └── user.go
│   └── model/
│       └── user.go
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
├── .air.toml
├── .env
└── .dockerignore

Layout cmd/ untuk entry point dan internal/ untuk kode privat (tidak bisa di-import package lain di luar project) adalah idiom Go yang menjaga agar API publik library-mu tetap bersih. Untuk service kecil, struktur cmd/api saja sudah cukup.

Dockerfile Multi-Stage #

Untuk Gin, Dockerfile multi-stage penting agar image runtime sekecil mungkin — tidak perlu membawa Go toolchain, build cache, atau binary sementara. Stage pertama untuk build, stage kedua hanya berisi binary akhir.

# syntax=docker/dockerfile:1.6
FROM golang:1.22-alpine AS builder

WORKDIR /app

# Install tools OS yang dibutuhkan saat build
RUN apk add --no-cache git ca-certificates

# Copy dependency descriptor dulu untuk cache layer
COPY go.mod go.sum ./
RUN go mod download

# Copy source code dan build binary statis
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/api ./cmd/api

# Stage kedua: runtime image yang ramping
FROM alpine:3.19

RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app

# Jalankan sebagai non-root
RUN addgroup -S app && adduser -S app -G app
USER app:app

COPY --from=builder /out/api /app/api

EXPOSE 8080

ENTRYPOINT ["/app/api"]

Pattern multi-stage memisahkan build environment (perlu Go toolchain, source code lengkap) dari runtime environment (cukup binary). Image akhir berbasis alpine:3.19 biasanya < 20 MB, padahal image golang:1.22-alpine untuk build bisa > 300 MB.

flowchart LR
    subgraph BUILD["Stage: builder (golang:1.22-alpine)"]
        SRC[Source code + go.mod] --> COMPILE[go build]
        COMPILE --> BIN1[Binary /out/api]
    end
    subgraph RUN["Stage: runtime (alpine:3.19)"]
        BIN1 --> COPY[COPY --from=builder]
        COPY --> IMG[Image final < 20 MB]
    end

Untuk development, kamu tidak perlu image sekecil ini — fokus pada kecepatan build dan hot reload. Tapi memisahkan Dockerfile dev dan prod adalah pola yang baik.

Dockerfile untuk Development dengan Hot Reload #

Gin sendiri tidak punya hot reload bawaan, tapi Go punya tool populer bernama Air yang memantau perubahan file dan otomatis rebuild + restart. Ini transformatif untuk DX Go.

# Dockerfile.dev
FROM golang:1.22-alpine

WORKDIR /app

RUN apk add --no-cache git curl bash

# Install Air untuk hot reload
RUN go install github.com/air-verse/air@latest

# Cache dependency: copy manifest dulu
COPY go.mod go.sum ./
RUN go mod download

# Source code di-mount via volume, tidak perlu COPY di sini
EXPOSE 8080

CMD ["air", "-c", ".air.toml"]

Konfigurasi Air (.air.toml):

root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
  args_bin = []
  bin = "./tmp/main"
  cmd = "go build -o ./tmp/main ./cmd/api"
  delay = 1000
  exclude_dir = ["assets", "tmp", "vendor", "testdata"]
  exclude_file = []
  exclude_regex = ["_test.go"]
  exclude_unchanged = true
  follow_symlink = false
  full_bin = ""
  include_dir = ["cmd", "internal"]
  include_ext = ["go", "tpl", "tmpl", "html", "yaml", "yml"]
  kill_delay = "0s"
  log = "build-errors.log"
  send_interrupt = false
  stop_on_error = false

[color]
  app = ""
  build = "yellow"
  main = "magenta"
  runner = "green"
  watcher = "cyan"

[log]
  time = true

[misc]
  clean_on_exit = true

Air memantau file dengan ekstensi go, tpl, tmpl, html, yaml di direktori cmd/ dan internal/. Saat file berubah, delay 1 detik, lalu rebuild. Pattern yang sama juga bisa dicapai dengan CompileDaemon atau fresh, tapi Air adalah yang paling populer.

docker-compose.yml untuk Development #

# docker-compose.yml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: gin-app:dev
    container_name: gin-api
    ports:
      - "8080:8080"
    volumes:
      - ./cmd:/app/cmd
      - ./internal:/app/internal
      - ./go.mod:/app/go.mod
      - ./go.sum:/app/go.sum
      - go-build:/app/tmp  # cache build binary
    environment:
      - APP_ENV=development
      - SERVER_PORT=8080
      - DATABASE_URL=postgres://app:dev@db:5432/ginapp?sslmode=disable
      - REDIS_URL=redis://cache:6379/0
      - JWT_SECRET=local-dev-secret-change-me
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy
    command: air -c .air.toml

  db:
    image: postgres:16-alpine
    container_name: gin-db
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=dev
      - POSTGRES_DB=ginapp
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d ginapp"]
      interval: 10s
      timeout: 5s
      retries: 5
    ports:
      - "5432:5432"

  cache:
    image: redis:7-alpine
    container_name: gin-cache
    command: redis-server --appendonly yes
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    volumes:
      - cache-data:/data
    ports:
      - "6379:6379"

  # Opsional: adminer untuk akses database via UI
  adminer:
    image: adminer:4
    container_name: gin-adminer
    ports:
      - "8081:8080"
    depends_on:
      - db
    profiles: ["tools"]

volumes:
  db-data:
  cache-data:
  go-build:

Penjelasan Service #

api — service utama Gin. Pakai Dockerfile.dev dengan Air. Mount source code sebagai bind mount — perubahan file di host langsung terlihat di container. Volume go-build meng-cache binary yang di-rebuild oleh Air agar lebih cepat. Environment variable DATABASE_URL dan REDIS_URL menunjuk ke service db dan cache di network Compose internal.

db — PostgreSQL untuk data persisten. Healthcheck pg_isready agar service api tidak start sebelum database benar-benar siap. Folder db/init bisa berisi SQL script yang di-run saat container pertama kali start.

cache — Redis untuk session, cache, dan rate limiting. Mode --appendonly yes mengaktifkan AOF persistence agar data tidak hilang saat restart.

adminer — opsional (diaktifkan dengan docker compose --profile tools up). UI web untuk mengelola database tanpa harus install psql client.

flowchart LR
    DEV[Developer] -->|edit code| HOST[Host filesystem]
    HOST -->|bind mount| CONTAINER[Gin container]
    CONTAINER -->|read/write| DB[(PostgreSQL)]
    CONTAINER -->|read/write| REDIS[(Redis)]
    HOST -->|localhost:8080| BROWSER[Browser/Postman]

Pola Integrasi Database dengan GORM #

GORM adalah ORM paling populer untuk Go. Untuk database access, pisahkan layer repository dari handler.

Model (internal/model/user.go):

package model

import "time"

type User struct {
    ID        uint64    `gorm:"primaryKey" json:"id"`
    Email     string    `gorm:"size:255;not null;uniqueIndex" json:"email"`
    Name      string    `gorm:"size:255;not null" json:"name"`
    Password  string    `gorm:"size:255;not null" json:"-"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

func (User) TableName() string {
    return "users"
}

Repository (internal/repository/user.go):

package repository

import (
    "context"
    "errors"
    "gorm.io/gorm"
    "myapp/internal/model"
)

type UserRepository struct {
    db *gorm.DB
}

func NewUserRepository(db *gorm.DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) Create(ctx context.Context, u *model.User) error {
    return r.db.WithContext(ctx).Create(u).Error
}

func (r *UserRepository) FindByID(ctx context.Context, id uint64) (*model.User, error) {
    var u model.User
    err := r.db.WithContext(ctx).First(&u, id).Error
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return nil, nil
    }
    return &u, err
}

func (r *UserRepository) FindByEmail(ctx context.Context, email string) (*model.User, error) {
    var u model.User
    err := r.db.WithContext(ctx).Where("email = ?", email).First(&u).Error
    if errors.Is(err, gorm.ErrRecordNotFound) {
        return nil, nil
    }
    return &u, err
}

func (r *UserRepository) List(ctx context.Context, limit, offset int) ([]model.User, error) {
    var users []model.User
    err := r.db.WithContext(ctx).Limit(limit).Offset(offset).Find(&users).Error
    return users, err
}

Service (internal/service/user.go):

package service

import (
    "context"
    "errors"
    "golang.org/x/crypto/bcrypt"
    "myapp/internal/model"
    "myapp/internal/repository"
)

type UserService struct {
    repo *repository.UserRepository
}

func NewUserService(repo *repository.UserRepository) *UserService {
    return &UserService{repo: repo}
}

func (s *UserService) Register(ctx context.Context, email, name, password string) (*model.User, error) {
    existing, _ := s.repo.FindByEmail(ctx, email)
    if existing != nil {
        return nil, errors.New("email already registered")
    }
    hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        return nil, err
    }
    u := &model.User{
        Email:    email,
        Name:     name,
        Password: string(hashed),
    }
    if err := s.repo.Create(ctx, u); err != nil {
        return nil, err
    }
    return u, nil
}

Handler (internal/handler/user.go):

package handler

import (
    "net/http"
    "strconv"
    "github.com/gin-gonic/gin"
    "myapp/internal/service"
)

type UserHandler struct {
    svc *service.UserService
}

func NewUserHandler(svc *service.UserService) *UserHandler {
    return &UserHandler{svc: svc}
}

type createUserRequest struct {
    Email    string `json:"email" binding:"required,email"`
    Name     string `json:"name" binding:"required,min=2"`
    Password string `json:"password" binding:"required,min=8"`
}

func (h *UserHandler) Create(c *gin.Context) {
    var req createUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    u, err := h.svc.Register(c.Request.Context(), req.Email, req.Name, req.Password)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    c.JSON(http.StatusCreated, u)
}

func (h *UserHandler) Get(c *gin.Context) {
    id, err := strconv.ParseUint(c.Param("id"), 10, 64)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
        return
    }
    u, err := h.svc.Get(c.Request.Context(), id)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
        return
    }
    if u == nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
        return
    }
    c.JSON(http.StatusOK, u)
}

Wiring di main.go:

package main

import (
    "context"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/redis/go-redis/v9"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"

    "myapp/internal/handler"
    "myapp/internal/model"
    "myapp/internal/repository"
    "myapp/internal/service"
)

func main() {
    // Setup database dengan retry
    var db *gorm.DB
    var err error
    dsn := os.Getenv("DATABASE_URL")
    for i := 0; i < 30; i++ {
        db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
        if err == nil {
            break
        }
        log.Printf("waiting for db: %v", err)
        time.Sleep(2 * time.Second)
    }
    if err != nil {
        log.Fatal("db connection failed: ", err)
    }

    // Auto-migrate schema (dev only!)
    if err := db.AutoMigrate(&model.User{}); err != nil {
        log.Fatal("migrate failed: ", err)
    }

    // Setup Redis
    rdb := redis.NewClient(&redis.Options{
        Addr: os.Getenv("REDIS_URL"),
    })

    // Setup layers
    userRepo := repository.NewUserRepository(db)
    userSvc := service.NewUserService(userRepo)
    userHandler := handler.NewUserHandler(userSvc)

    // Setup Gin
    if os.Getenv("APP_ENV") == "production" {
        gin.SetMode(gin.ReleaseMode)
    }
    r := gin.Default()

    r.GET("/health", func(c *gin.Context) {
        c.JSON(200, gin.H{"status": "ok"})
    })

    api := r.Group("/api/v1")
    {
        api.POST("/users", userHandler.Create)
        api.GET("/users/:id", userHandler.Get)
    }

    // Graceful shutdown
    srv := r
    go func() {
        if err := srv.Run(":8080"); err != nil {
            log.Printf("server stopped: %v", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("shutting down...")
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    _ = ctx
}

Middleware Bawaan dan Kustom #

Gin punya banyak middleware bawaan yang bisa kamu pakai langsung.

r := gin.Default()  // sudah include Logger + Recovery

// Middleware kustom: request ID
func RequestID() gin.HandlerFunc {
    return func(c *gin.Context) {
        rid := c.GetHeader("X-Request-ID")
        if rid == "" {
            rid = uuid.NewString()
        }
        c.Set("requestID", rid)
        c.Writer.Header().Set("X-Request-ID", rid)
        c.Next()
    }
}

// Middleware: CORS
func CORS() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(204)
            return
        }
        c.Next()
    }
}

// Middleware: simple rate limit pakai Redis
func RateLimit(rdb *redis.Client) gin.HandlerFunc {
    return func(c *gin.Context) {
        key := c.ClientIP()
        count, _ := rdb.Incr(c.Request.Context(), "rl:"+key).Result()
        if count > 100 {
            c.AbortWithStatusJSON(429, gin.H{"error": "rate limit exceeded"})
            return
        }
        c.Next()
    }
}

// Register
r.Use(RequestID())
r.Use(CORS())
r.Use(RateLimit(rdb))

Urutan middleware penting. RequestID harus paling awal agar ID bisa dilog oleh middleware lain. CORS sebelum RateLimit agar preflight OPTIONS tidak ke-rate-limit. Recovery (built-in) paling akhir agar bisa recover dari panic di middleware lain.

Validasi dengan go-playground/validator #

Gin menggunakan go-playground/validator lewat tag binding.

type createOrderRequest struct {
    ProductID uint64  `json:"product_id" binding:"required"`
    Quantity  int     `json:"quantity" binding:"required,min=1,max=100"`
    Notes     string  `json:"notes" binding:"max=500"`
    Email     string  `json:"email" binding:"required,email"`
    Tags      []string `json:"tags" binding:"required,min=1,dive,min=2,max=30"`
}

func createOrder(c *gin.Context) {
    var req createOrderRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    // lanjut proses
}

Validator bawaan sudah cukup untuk kebanyakan kasus: required, email, min, max, len, oneof, uuid, url. Untuk aturan kompleks, daftarkan custom validator.

Build dan Run #

# Build image development
docker compose build

# Jalankan semua service di background
docker compose up -d

# Lihat log aplikasi
docker compose logs -f api

# Jalankan migration (kalau tidak pakai AutoMigrate)
docker compose exec api ./api migrate

# Akses shell di dalam container
docker compose exec api sh

# Stop semua service
docker compose down

# Stop dan hapus volume (reset total)
docker compose down -v

Akses endpoint:

  • API: http://localhost:8080
  • Health check: http://localhost:8080/health
  • PostgreSQL: localhost:5432 (user app, password dev, db ginapp)
  • Redis: localhost:6379
  • Adminer (opsional): docker compose --profile tools up -d lalu http://localhost:8081

Test endpoint dengan curl:

# Register user
curl -X POST http://localhost:8080/api/v1/users \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","name":"Budi","password":"rahasia123"}'

# Get user by id
curl http://localhost:8080/api/v1/users/1

Testing dengan Testcontainers #

Untuk integration test, Testcontainers Go bisa spin up Postgres dan Redis otomatis.

// internal/repository/user_test.go
package repository_test

import (
    "context"
    "testing"
    "time"

    "github.com/stretchr/testify/assert"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
    "github.com/testcontainers/testcontainers-go/modules/redis"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"

    "myapp/internal/model"
    "myapp/internal/repository"
)

func setupDB(t *testing.T) (*gorm.DB, func()) {
    ctx := context.Background()

    pgC, err := postgres.RunContainer(ctx,
        testcontainers.WithImage("postgres:16-alpine"),
        postgres.WithDatabase("test"),
        postgres.WithUsername("test"),
        postgres.WithPassword("test"),
    )
    if err != nil {
        t.Fatal(err)
    }

    dsn, _ := pgC.ConnectionString(ctx, "sslmode=disable")
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        t.Fatal(err)
    }
    db.AutoMigrate(&model.User{})

    cleanup := func() {
        pgC.Terminate(ctx)
    }
    return db, cleanup
}

func TestUserRepository_Create(t *testing.T) {
    db, cleanup := setupDB(t)
    defer cleanup()

    repo := repository.NewUserRepository(db)
    u := &model.User{
        Email:    "[email protected]",
        Name:     "Test",
        Password: "hashed",
    }
    err := repo.Create(context.Background(), u)
    assert.NoError(t, err)
    assert.NotZero(t, u.ID)
}

Testcontainers otomatis pull image Postgres, jalankan container, jalankan test, lalu terminate container. Test tetap deterministik dan tidak bergantung pada state global.

Kapan Gin Kurang Tepat #

Gin adalah pilihan yang solid, tapi ada kondisi di mana alternatif lebih cocok.

Tetap gunakan Gin jika:
  ✓ REST API konvensional
  ✓ Butuh middleware ecosystem yang luas
  ✓ Prioritas performa dan kesederhanaan
  ✓ Tim familiar dengan Go standard library

Pertimbangkan alternatif jika:
  ✗ Butuh type-safe handler dengan schema → fiber
  ✗ Hanya butuh router ringan tanpa framework → chi
  ✗ Microservice dengan full HTTP control → net/http standard
  ✗ gRPC service → tetap pakai grpc-go

chi adalah alternatif minimalist yang sangat dekat dengan net/http — cocok untuk service yang ingin kontrol penuh. fiber menawarkan API mirip Express.js dengan performa sedikit di bawah Gin tapi ergonomics lebih familiar untuk developer JavaScript. Untuk service yang hanya butuh routing tanpa banyak middleware, net/http standar (dengan http.ServeMux baru) sudah lebih dari cukup.

Best Practice #

Pisahkan Dockerfile Dev dan Prod #

Gunakan Dockerfile.dev untuk development (dengan Air, full toolchain) dan Dockerfile untuk production (multi-stage ramping, binary statis). Jangan campur concern — production image tidak butuh Air, dan dev image tidak perlu optimasi size.

Cache Dependency Go #

Selalu copy go.mod dan go.sum lebih dulu, jalankan go mod download, baru copy source code. Ini membuat layer dependency tidak invalidate saat source code berubah. Build awal lebih lambat, tapi build berikutnya kilat.

Bind Mount Source Code, Bukan COPY #

Di development, pakai volumes di Compose untuk mount source code ke container. Container pakai Air untuk rebuild. Saat deploy production, COPY source ke image final.

Healthcheck untuk Service Dependent #

PostgreSQL dan Redis harus punya healthcheck. Service api pakai depends_on: condition: service_healthy agar tidak start sebelum dependency siap. Ini tidak menjamin koneksi siap, jadi tambahkan retry loop di kode aplikasi.

Pakai .env untuk Konfigurasi #

Semua konfigurasi yang bisa berbeda antar environment — port, database URL, secret — simpan di .env. File ini di-.gitignore. Sediakan .env.example di repo untuk onboarding.

Jalankan Sebagai Non-Root di Production #

Tambahkan USER app:app di Dockerfile production. Image development tidak terlalu penting karena biasanya hanya jalan di laptop, tapi production wajib non-root.

Logging Terstruktur #

Gunakan slog (Go 1.21+) atau library seperti zerolog/zap untuk log JSON. Di development, log plain text cukup — tapi di production, log JSON lebih mudah di-parse oleh aggregator (Loki, ELK, Datadog).

Troubleshooting #

Air Tidak Detect Perubahan #

Pastikan bind mount mencakup direktori source code, dan include_dir di .air.toml sesuai struktur project. Di Mac/Windows dengan Docker Desktop, file change events kadang butuh waktu sampai beberapa detik.

Database Connection Refused #

Pakai depends_on: condition: service_healthy di Compose, dan tambahkan retry loop di kode Go (lihat contoh main.go di atas). depends_on saja tidak menjamin database siap menerima koneksi.

Port 8080 Sudah Dipakai #

lsof -i :8080
# atau di Windows
netstat -ano | findstr :8080

Stop proses lain atau ubah port mapping di Compose ("8081:8080").

Go Mod Download Lambat #

Pakai Go module proxy: GOPROXY=https://proxy.golang.org,direct. Atau cache layer dengan volume go-build.

Hot Reload Loop Terlalu Cepat #

Naikkan delay di .air.toml (default 1000 ms). Kalau setiap save trigger banyak rebuild berurutan, naikkan ke 2000-3000 ms.

Ringkasan #

  • Gin ideal untuk local development REST API — Postgres, Redis, dan service lain berjalan sebagai container terpisah.
  • Dockerfile multi-stage untuk production: builder stage dengan golang:1.22-alpine, runtime stage ramping dengan alpine:3.19. Image akhir < 20 MB.
  • Dockerfile.dev untuk development: full toolchain + Air untuk hot reload. Build lebih berat tapi iterasi lebih cepat.
  • Hot reload dengan Air: pantau file .go, rebuild, restart server otomatis. Konfigurasi lewat .air.toml.
  • Layer cache Go: copy go.mod + go.sum dulu, jalankan go mod download, baru copy source. Build inkremental super cepat.
  • Healthcheck untuk service dependent: Postgres pg_isready, Redis redis-cli ping. Pakai depends_on: condition: service_healthy di Compose.
  • Pisahkan layer: handler (HTTP) → service (logic) → repository (data access). Dependency injection lewat konstruktor di main.go.
  • Retry loop untuk database: depends_on tidak menjamin koneksi siap, jadi tambahkan retry di kode aplikasi.
  • Testcontainers untuk integration test: otomatis spin up Postgres container, jalankan test, cleanup. Tidak bergantung pada state global.
  • Validasi dengan tag binding di struct: required, email, min, max, oneof. Library go-playground/validator sudah built-in di Gin.
  • Middleware: Logger + Recovery (built-in), CORS, Request ID, Rate Limit (Redis). Urutan penting — request ID paling awal.
  • Best practice: pisahkan Dockerfile dev & prod, cache dependency Go, bind mount source code, healthcheck untuk dependency, .env untuk konfigurasi, non-root user untuk production, log terstruktur.
  • Alternatif: chi untuk router ringan, fiber untuk API mirip Express, net/http standar untuk service minimal.

← Sebelumnya: Micronaut   Berikutnya: Fiber →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact