Part 6 · 2 chapters · ~12 min

Gin End to End

Setting up a Gin service, route groups and parameters, binding and validation with struct tags, middleware for recovery, logging, auth and request ids, error handling with one problem-details format, database access with pgx and sqlc, configuration, graceful shutdown, and project layout.

11

A Gin service

code
type CreateTransfer struct {
	AmountKobo int64  `json:"amount_kobo" binding:"required,gt=0"`
	To         string `json:"to" binding:"required"`
}

func main() {
	r := gin.New()
	r.Use(gin.Recovery(), requestID(), slogLogger())
	v1 := r.Group("/v1", auth())
	v1.POST("/transfers", func(c *gin.Context) {
		var in CreateTransfer
		if err := c.ShouldBindJSON(&in); err != nil { problem(c, 422, "validation", err.Error()); return }
		key := c.GetHeader("Idempotency-Key")
		if key == "" { problem(c, 400, "missing_idempotency_key", "Idempotency-Key header required"); return }
		t, err := svc.Create(c.Request.Context(), userID(c), in, key)
		switch {
		case errors.Is(err, ErrInsufficientFunds): problem(c, 422, "insufficient_funds", err.Error())
		case err != nil: problem(c, 500, "internal", "something went wrong")
		default: c.JSON(201, t)
		}
	})
	srv := &http.Server{Addr: ":8080", Handler: r, ReadHeaderTimeout: 5 * time.Second}
	go srv.ListenAndServe()
	stop := make(chan os.Signal, 1); signal.Notify(stop, syscall.SIGTERM, os.Interrupt); <-stop
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second); defer cancel()
	srv.Shutdown(ctx)                                   // stop accepting, finish in-flight requests
}
12

Data access: pgx and sqlc

code
-- queries.sql (sqlc generates type-safe Go from SQL)
-- name: DebitAccount :one
UPDATE accounts SET balance_kobo = balance_kobo - $2 WHERE id = $1 AND balance_kobo >= $2 RETURNING balance_kobo;

// generated: func (q *Queries) DebitAccount(ctx context.Context, arg DebitAccountParams) (int64, error)
pool, _ := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))     // pgx: the fastest Postgres driver, with a pool
q := db.New(pool)
bal, err := q.DebitAccount(ctx, db.DebitAccountParams{ID: id, BalanceKobo: amt})
if errors.Is(err, pgx.ErrNoRows) { return ErrInsufficientFunds }

The Go community leans toward SQL first: pgx or database/sql, sqlc for generated types, and goose or golang-migrate for migrations. GORM exists (an ORM, ORM course part 6 for comparisons), but many teams prefer explicit SQL.