Setup and context: Why Go + Antigravity in 2026?
Antigravity has transformed the TypeScript/JavaScript development experience — but its benefits extend just as powerfully to Go. With its static typing, fast compilation, and elegant concurrency model, Go has seen explosive adoption in microservices and serverless environments, making it an essential skill for backend developers in 2026.
This article provides a complete, hands-on guide to building a production-grade Go RESTful API from scratch using Antigravity's Agent features, contextual understanding, and AI code completion. Rather than a toy tutorial, you'll learn the design philosophy and implementation patterns that hold up in real production environments.
Who This Is For
- Go developers who have the basics down and want to level up to production-quality implementations
- TypeScript/Node.js backend developers exploring a migration to Go
- Developers who want to apply microservice design and API best practices with depth
- Anyone looking to 10x their Go productivity using an AI IDE
What You'll Build
A RESTful API server with user management, authentication, and post management features, using the following stack:
- Framework: Echo v4 (high-performance, minimal routing)
- Database: PostgreSQL + sqlc (type-safe SQL query generation)
- Auth: JWT (JSON Web Token)
- Testing: testify + testcontainers-go
- Deployment: Cloud Run (Google Cloud) or Fly.io
- AI Assistance: Antigravity Agent, code completion, error diagnostics
Environment Setup and Antigravity Project Configuration
Installing Required Tools
# Install Go 1.22+ (via official site)
go version # go version go1.22.x
# sqlc — type-safe SQL → Go code generator
go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
# golang-migrate — DB migration manager
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
# air — hot reload for development
go install github.com/air-verse/air@latestSetting Up AGENTS.md in Antigravity
Create an AGENTS.md file at the project root to give Antigravity Agent full context about your project. This is the single most impactful thing you can do to boost AI-assisted productivity.
# Project Context
## Overview
Go RESTful API server using Echo v4, sqlc, PostgreSQL.
This is a production-grade backend with JWT authentication.
## Architecture
- cmd/server/main.go — Entry point
- internal/api/ — HTTP handlers and routing
- internal/db/ — sqlc-generated code (do not edit manually)
- internal/service/ — Business logic layer
- internal/middleware/ — Custom middlewares
- db/migrations/ — SQL migration files
- db/query/ — sqlc query definitions
## Conventions
- Wrap errors with errors.Wrap to preserve stack traces
- Use slog (standard library) for structured logging
- Use testify assertions in tests
- Use testcontainers-go for DB tests with a real PostgreSQL instance
## Database
- PostgreSQL 16 (type-safe queries via sqlc)
- Migrations managed by golang-migrate
## Do not
- Manually edit files inside internal/db/ (generated by sqlc)
- Use global variablesWith this AGENTS.md in place, Antigravity Agent understands your Go conventions, project structure, and naming patterns before generating any code.
Project Structure Design
Directory Layout
When you ask Antigravity Agent to "create the directory structure for this project," it proposes a layout aligned with Go standard conventions. Here's the recommended structure:
myapi/
├── AGENTS.md
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── api/
│ │ ├── handler/
│ │ │ ├── user_handler.go
│ │ │ └── post_handler.go
│ │ ├── middleware/
│ │ │ ├── auth.go
│ │ │ └── logger.go
│ │ └── router.go
│ ├── db/
│ │ └── (sqlc-generated files — do not edit)
│ └── service/
│ ├── user_service.go
│ └── post_service.go
├── db/
│ ├── migrations/
│ │ ├── 000001_create_users.up.sql
│ │ └── 000001_create_users.down.sql
│ └── query/
│ ├── users.sql
│ └── posts.sql
├── sqlc.yaml
├── go.mod
└── .air.toml
Initializing go.mod
mkdir myapi && cd myapi
go mod init github.com/yourname/myapi
# Add dependencies
go get github.com/labstack/echo/v4
go get github.com/labstack/echo-jwt/v4
go get github.com/golang-jwt/jwt/v5
go get github.com/jackc/pgx/v5
go get github.com/golang-migrate/migrate/v4
go get github.com/joho/godotenv
go get go.uber.org/zapDB Schema Design and Type-Safe Queries with sqlc
Writing Migration Files
Ask Antigravity to "design an ERD for users and posts, and generate SQL in golang-migrate format." Review and adjust as needed.
-- db/migrations/000001_create_users.up.sql
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL UNIQUE,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users (email);
CREATE INDEX idx_users_username ON users (username);
-- db/migrations/000002_create_posts.up.sql
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_posts_user_id ON posts (user_id);sqlc.yaml Configuration
# sqlc.yaml
version: "2"
sql:
- engine: "postgresql"
queries: "db/query/"
schema: "db/migrations/"
gen:
go:
package: "db"
out: "internal/db"
emit_json_tags: true
emit_prepared_queries: true
emit_interface: true
emit_exact_table_names: falseDefining SQL Queries
-- db/query/users.sql
-- name: CreateUser :one
INSERT INTO users (email, username, password)
VALUES ($1, $2, $3)
RETURNING *;
-- name: GetUserByEmail :one
SELECT * FROM users
WHERE email = $1 LIMIT 1;
-- name: GetUserByID :one
SELECT * FROM users
WHERE id = $1 LIMIT 1;
-- name: ListUsers :many
SELECT id, email, username, created_at FROM users
ORDER BY created_at DESC
LIMIT $1 OFFSET $2;# Generate type-safe Go code from SQL
sqlc generate
# → Type-safe Go code is generated into internal/db/Once these generated files are in your project, you can tell Antigravity "write a user creation handler using db.CreateUserParams" and it will generate accurate, type-checked code every time. This is the true power of sqlc + Antigravity.
Building the API with Echo
Entry Point (cmd/server/main.go)
package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/joho/godotenv"
"github.com/yourname/myapi/internal/api"
)
func main() {
_ = godotenv.Load()
// Structured logger setup
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
// DB connection pool
pool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
if err != nil {
slog.Error("Failed to connect to database", "error", err)
os.Exit(1)
}
defer pool.Close()
// Start Echo server
e := api.NewRouter(pool)
srv := &http.Server{
Addr: ":" + os.Getenv("PORT"),
Handler: e,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Graceful shutdown
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
slog.Error("Server error", "error", err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
slog.Info("Server shut down gracefully")
}Router Configuration (internal/api/router.go)
package api
import (
"github.com/jackc/pgx/v5/pgxpool"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/yourname/myapi/internal/api/handler"
mw "github.com/yourname/myapi/internal/middleware"
)
func NewRouter(pool *pgxpool.Pool) *echo.Echo {
e := echo.New()
// Global middleware
e.Use(middleware.RequestID())
e.Use(mw.StructuredLogger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// Initialize handlers
userH := handler.NewUserHandler(pool)
postH := handler.NewPostHandler(pool)
// Public endpoints
v1 := e.Group("/api/v1")
v1.POST("/auth/register", userH.Register)
v1.POST("/auth/login", userH.Login)
// Authenticated endpoints
auth := v1.Group("")
auth.Use(echojwt.WithConfig(echojwt.Config{
SigningKey: []byte(os.Getenv("JWT_SECRET")),
}))
auth.GET("/users/me", userH.GetMe)
auth.GET("/posts", postH.List)
auth.POST("/posts", postH.Create)
auth.GET("/posts/:id", postH.Get)
auth.PUT("/posts/:id", postH.Update)
auth.DELETE("/posts/:id", postH.Delete)
return e
}JWT Authentication and User Handler
Ask Antigravity: "Write a JWT login handler using sqlc's GetUserByEmail query — verify the password with bcrypt and return a signed JWT." Here's what gets generated:
// internal/api/handler/user_handler.go (excerpt)
func (h *UserHandler) Login(c echo.Context) error {
var req LoginRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid request format")
}
if err := c.Validate(req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
// Fetch user from DB (sqlc-generated code)
user, err := h.queries.GetUserByEmail(c.Request().Context(), req.Email)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
}
// Verify bcrypt password
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
}
// Generate JWT token
claims := &JWTClaims{
UserID: user.ID.String(),
Username: user.Username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString([]byte(os.Getenv("JWT_SECRET")))
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate token")
}
return c.JSON(http.StatusOK, LoginResponse{Token: signed})
}Key Antigravity tip: Because AGENTS.md describes the sqlc type schema, Antigravity generates handlers that correctly reference db.User field names — eliminating the type mismatch errors that slow down development when starting a new Go project.
Validation and Error Handling
Custom Validator
// internal/api/validator.go
package api
import (
"net/http"
"github.com/go-playground/validator/v10"
"github.com/labstack/echo/v4"
)
type CustomValidator struct {
validator *validator.Validate
}
func NewCustomValidator() *CustomValidator {
return &CustomValidator{validator: validator.New()}
}
func (cv *CustomValidator) Validate(i interface{}) error {
if err := cv.validator.Struct(i); err != nil {
errs := err.(validator.ValidationErrors)
messages := make([]string, len(errs))
for i, e := range errs {
messages[i] = fmt.Sprintf("%s: %s", e.Field(), e.Tag())
}
return echo.NewHTTPError(http.StatusBadRequest, strings.Join(messages, ", "))
}
return nil
}Global Error Handler
// internal/middleware/error_handler.go
func CustomHTTPErrorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
message := "An internal server error occurred"
var he *echo.HTTPError
if errors.As(err, &he) {
code = he.Code
if msg, ok := he.Message.(string); ok {
message = msg
}
}
// Log full details only for 5xx errors
if code >= 500 {
slog.Error("HTTP error", "code", code, "error", err,
"path", c.Request().URL.Path,
"request_id", c.Response().Header().Get(echo.HeaderXRequestID),
)
}
c.JSON(code, map[string]interface{}{
"error": message,
"request_id": c.Response().Header().Get(echo.HeaderXRequestID),
})
}Integration Testing with testcontainers-go
The most reliable way to ensure production quality in Go is integration testing with a real database. Ask Antigravity: "Generate a PostgreSQL integration test setup using testcontainers-go." The result looks like this:
// internal/service/user_service_test.go
package service_test
import (
"context"
"testing"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/yourname/myapi/internal/db"
"github.com/yourname/myapi/internal/service"
)
func setupTestDB(t *testing.T) *pgxpool.Pool {
t.Helper()
// Spin up a real PostgreSQL container
pgContainer, err := postgres.RunContainer(
context.Background(),
testcontainers.WithImage("postgres:16"),
postgres.WithDatabase("testdb"),
postgres.WithUsername("testuser"),
postgres.WithPassword("testpass"),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).
WithStartupTimeout(30*time.Second),
),
)
require.NoError(t, err)
t.Cleanup(func() { pgContainer.Terminate(context.Background()) })
connStr, err := pgContainer.ConnectionString(context.Background(), "sslmode=disable")
require.NoError(t, err)
// Run migrations
m, err := migrate.New("file://../../db/migrations", connStr)
require.NoError(t, err)
require.NoError(t, m.Up())
pool, err := pgxpool.New(context.Background(), connStr)
require.NoError(t, err)
t.Cleanup(pool.Close)
return pool
}
func TestUserService_CreateUser(t *testing.T) {
pool := setupTestDB(t)
svc := service.NewUserService(db.New(pool))
user, err := svc.CreateUser(context.Background(), service.CreateUserParams{
Email: "test@example.com",
Username: "testuser",
Password: "securepassword123",
})
require.NoError(t, err)
require.Equal(t, "test@example.com", user.Email)
require.NotEmpty(t, user.ID)
// Password must be hashed, not stored in plain text
require.NotEqual(t, "securepassword123", user.Password)
}Running the tests (allow 2–3 minutes on first run while Docker images are pulled):
$ go test ./... -v -count=1
--- PASS: TestUserService_CreateUser (2.34s)
--- PASS: TestUserService_GetUserByEmail (0.12s)
--- PASS: TestPostService_CreateAndPublish (0.89s)
PASS
ok github.com/yourname/myapi/internal/service 3.35sDeploying to Cloud Run
Dockerfile with Multi-Stage Build
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Cache dependency layer
COPY go.mod go.sum ./
RUN go mod download
# Build a single static binary (CGO disabled)
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-s -w" \
-o /api ./cmd/server
# Minimal distroless runtime image (security-hardened)
FROM gcr.io/distroless/static-debian12
COPY --from=builder /api /api
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/api"]Cloud Run Deployment
# Authenticate and push to Artifact Registry
gcloud auth configure-docker asia-northeast1-docker.pkg.dev
docker build -t asia-northeast1-docker.pkg.dev/PROJECT_ID/myapi/server:latest .
docker push asia-northeast1-docker.pkg.dev/PROJECT_ID/myapi/server:latest
# Deploy to Cloud Run (zero-downtime rolling update)
gcloud run deploy myapi \
--image asia-northeast1-docker.pkg.dev/PROJECT_ID/myapi/server:latest \
--region asia-northeast1 \
--platform managed \
--allow-unauthenticated \
--min-instances 1 \
--max-instances 10 \
--concurrency 100 \
--set-env-vars "DATABASE_URL=postgres://...,JWT_SECRET=YOUR_JWT_SECRET"Automating CI/CD with GitHub Actions
Ask Antigravity Agent to "create a GitHub Actions workflow for automated Cloud Run deployment." It will generate a complete pipeline covering test → build → deploy. For the full implementation details, see our GitHub Actions Advanced CI/CD Pipeline Guide.
Performance Optimization with Antigravity
Setting Up pprof Profiling
// Add to main.go (guard with authentication in production)
import _ "net/http/pprof"
go func() {
slog.Info("pprof server listening", "addr", "localhost:6060")
http.ListenAndServe("localhost:6060", nil)
}()# Capture a 30-second CPU profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# Then ask Antigravity:
# "Review this profile output and identify bottleneck functions with improvement suggestions"DB Query Optimization with Antigravity
When sqlc query files are included in Antigravity's context, prompts like "suggest indexes that would improve this query and explain why" return data-driven recommendations based on the actual query structure:
-- Example optimization suggested by Antigravity based on query plan analysis
EXPLAIN ANALYZE
SELECT p.* FROM posts p
WHERE p.user_id = $1 AND p.published = true
ORDER BY p.created_at DESC;
-- Composite index addition
CREATE INDEX CONCURRENTLY idx_posts_user_published
ON posts (user_id, published, created_at DESC);
-- Execution time: 120ms → 3ms (97.5% improvement)Rate Limiting, Security Hardening, and Middleware Best Practices
Rate Limiting with Redis
Production APIs need rate limiting to protect against abuse. Ask Antigravity: "Add Redis-based rate limiting middleware to this Echo API using a sliding window algorithm."
// internal/middleware/rate_limiter.go
package middleware
import (
"context"
"fmt"
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/redis/go-redis/v9"
)
// RateLimiter implements a sliding window rate limiter backed by Redis.
// This approach correctly handles distributed deployments (multiple Cloud Run instances).
func RateLimiter(rdb *redis.Client, limit int, window time.Duration) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
key := fmt.Sprintf("rl:%s:%s", c.RealIP(), c.Request().URL.Path)
ctx := context.Background()
pipe := rdb.Pipeline()
incr := pipe.Incr(ctx, key)
pipe.Expire(ctx, key, window)
_, err := pipe.Exec(ctx)
if err != nil {
// Fail open: if Redis is unavailable, allow the request
return next(c)
}
count := incr.Val()
c.Response().Header().Set("X-RateLimit-Limit", fmt.Sprintf("%d", limit))
c.Response().Header().Set("X-RateLimit-Remaining", fmt.Sprintf("%d", max(0, int64(limit)-count)))
if count > int64(limit) {
return echo.NewHTTPError(http.StatusTooManyRequests, "Rate limit exceeded. Please slow down.")
}
return next(c)
}
}
}Security Headers Middleware
// internal/middleware/security.go
func SecurityHeaders() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
h := c.Response().Header()
h.Set("X-Content-Type-Options", "nosniff")
h.Set("X-Frame-Options", "DENY")
h.Set("X-XSS-Protection", "1; mode=block")
h.Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains; preload")
h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
h.Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
return next(c)
}
}
}Input Sanitization for SQL Injection Prevention
sqlc's parameterized queries prevent SQL injection by design, but it's worth understanding why. When Antigravity generates sqlc queries, every $1, $2 placeholder is sent as a separate parameter over the PostgreSQL wire protocol — the database engine never interpolates user input into query strings. This is fundamentally safer than string concatenation approaches used in some ORMs.
For text fields that get stored and later displayed (like blog post content), add HTML sanitization to prevent XSS:
import "github.com/microcosm-cc/bluemonday"
var sanitizer = bluemonday.UGCPolicy()
func sanitizeInput(s string) string {
return sanitizer.Sanitize(s)
}
// In your service layer:
func (s *PostService) CreatePost(ctx context.Context, params CreatePostParams) (db.Post, error) {
return s.queries.CreatePost(ctx, db.CreatePostParams{
UserID: params.UserID,
Title: sanitizeInput(params.Title),
Content: sanitizeInput(params.Content),
})
}Observability: Structured Logging, Metrics, and Tracing
Structured Logging with slog
Go 1.21 introduced log/slog as a structured logging standard library — no more third-party dependency required for basic needs.
// internal/middleware/logger.go
package middleware
import (
"log/slog"
"time"
"github.com/labstack/echo/v4"
)
func StructuredLogger() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
start := time.Now()
err := next(c)
duration := time.Since(start)
logAttrs := []any{
"method", c.Request().Method,
"path", c.Request().URL.Path,
"status", c.Response().Status,
"duration_ms", duration.Milliseconds(),
"request_id", c.Response().Header().Get(echo.HeaderXRequestID),
"ip", c.RealIP(),
}
if err != nil {
logAttrs = append(logAttrs, "error", err.Error())
slog.Error("Request failed", logAttrs...)
} else if c.Response().Status >= 500 {
slog.Error("Server error response", logAttrs...)
} else {
slog.Info("Request handled", logAttrs...)
}
return err
}
}
}Prometheus Metrics Integration
// Add Prometheus metrics endpoint for Cloud Monitoring integration
import "github.com/prometheus/client_golang/prometheus/promhttp"
// In router.go
e.GET("/metrics", echo.WrapHandler(promhttp.Handler()))
// Custom business metrics
var (
requestsTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Total number of HTTP requests",
},
[]string{"method", "path", "status"},
)
requestDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
)
func init() {
prometheus.MustRegister(requestsTotal, requestDuration)
}OpenTelemetry Distributed Tracing
For microservice architectures where a single request spans multiple services, distributed tracing is essential for debugging latency issues. Ask Antigravity: "Add OpenTelemetry tracing to my Echo handlers that propagates trace context through PostgreSQL queries."
// Instrument DB queries with OpenTelemetry spans
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
func (h *PostHandler) List(c echo.Context) error {
ctx := c.Request().Context()
tracer := otel.Tracer("post-handler")
ctx, span := tracer.Start(ctx, "post.list")
defer span.End()
posts, err := h.queries.ListPosts(ctx, db.ListPostsParams{
UserID: getUserIDFromJWT(c),
Limit: 20,
Offset: 0,
})
if err != nil {
span.RecordError(err)
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch posts")
}
return c.JSON(http.StatusOK, posts)
}Advanced Patterns: Dependency Injection and Clean Architecture
Wire-Based Dependency Injection
As your application grows, manual dependency wiring in main.go becomes unwieldy. Google's Wire tool generates compile-time dependency injection code — and Antigravity can help you write the provider definitions:
// internal/wire.go (Wire provider definitions)
//go:build wireinject
package internal
import (
"github.com/google/wire"
"github.com/yourname/myapi/internal/api"
"github.com/yourname/myapi/internal/db"
"github.com/yourname/myapi/internal/service"
)
var appSet = wire.NewSet(
db.New,
service.NewUserService,
service.NewPostService,
api.NewUserHandler,
api.NewPostHandler,
api.NewRouter,
)
func InitializeApp(pool *pgxpool.Pool) (*echo.Echo, error) {
wire.Build(appSet)
return nil, nil
}# Generate dependency injection code
wire ./internal/...
# → Generates wire_gen.go with all wiring resolved at compile timeRepository Pattern for Testable Services
Rather than calling sqlc-generated code directly from service functions, wrap queries in a repository interface. Antigravity can generate both the interface and a mock implementation for unit tests:
// internal/repository/user_repository.go
type UserRepository interface {
Create(ctx context.Context, params CreateParams) (User, error)
GetByEmail(ctx context.Context, email string) (User, error)
GetByID(ctx context.Context, id uuid.UUID) (User, error)
List(ctx context.Context, limit, offset int32) ([]User, error)
}
// internal/repository/mock_user_repository.go (generated by Antigravity or mockery)
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) GetByEmail(ctx context.Context, email string) (User, error) {
args := m.Called(ctx, email)
return args.Get(0).(User), args.Error(1)
}With this pattern, service-layer unit tests run in milliseconds without Docker, while integration tests use the real PostgreSQL via testcontainers. You get the best of both worlds: fast feedback during development and high confidence before deployment.
Common Errors and How to Fix Them
Error 1: pgx: cannot convert type unknown
This occurs when sqlc-generated code doesn't handle UUID types correctly.
# Ask Antigravity: "Fix the pgx UUID type handling issue"
# → Antigravity generates the correct pgtype.UUID conversion codeError 2: context deadline exceeded — DB Connection Timeout
This typically means pgxpool's settings are too conservative, or you've hit the connection limit.
config, _ := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
config.MaxConns = 25 // Align with Cloud Run max-instances
config.MinConns = 5 // Keep warmed-up connections ready
config.MaxConnLifetime = 30 * time.Minute
config.MaxConnIdleTime = 5 * time.MinuteError 3: exec format error with Distroless Image
This happens when you build locally on ARM (Apple Silicon) and deploy to AMD64 Cloud Run.
# Cross-platform build for AMD64
docker buildx build \
--platform linux/amd64 \
-t asia-northeast1-docker.pkg.dev/PROJECT_ID/myapi/server:latest \
--push .Summary
In this article, we walked through a complete workflow for building a production-grade RESTful API using Antigravity and Go. Let's recap the key takeaways:
- A well-crafted
AGENTS.mddramatically improves the quality of Antigravity's code generation - sqlc × pgx delivers type-safe, high-performance database access without the magic of an ORM
- testcontainers-go integration tests catch production-level bugs that mocks would never surface
- Distroless images × Cloud Run provide a secure, scalable production environment
- Antigravity's Agent isn't just for boilerplate — it's a capable design partner, debugger, and optimization advisor
For further exploration, check out our Kubernetes Container Orchestration Practical Guide and Clean Architecture × DDD Implementation Guide to build even more robust backend systems.
The combination of Antigravity's AI intelligence and Go's performance characteristics creates a compelling stack for backend development. TypeScript gives you flexibility; Go gives you predictability and speed. Used together with an AI IDE that truly understands your codebase, Go stops feeling like a "harder" language and starts feeling like the right tool for systems that need to scale.