Claude Code with Go: Gin, Echo, and Stdlib Workflows
Why Go developers need a different Claude Code setup
Claude Code handles Go well. It understands interfaces, goroutines, error wrapping, and the standard library layout. The gap between "Claude knows Go" and "Claude writes Go that fits your codebase" is the same gap it is for every language: framework-level knowledge without project-level context.
Go has a stricter surface area than most languages. A codebase using Gin handles routes differently from one using Echo or stdlib net/http. Struct tag conventions for JSON serialization, database ORM mapping, and validation diverge significantly across projects. Table-driven tests with t.Run follow patterns your team has established over time. Without a CLAUDE.md that captures these conventions, Claude generates Go code that compiles, but does not fit.
Go's strict type system means Claude Code's output compiles on the first attempt more often than with Python or JavaScript. The failure mode shifts from compilation errors to convention drift, where generated code is technically correct but subtly wrong for your codebase.
This guide covers the CLAUDE.md configuration for Go projects using Gin, Echo, and stdlib, plus table-driven test patterns, struct tag discipline, and permission hooks. If you have not installed Claude Code yet, the Claude Code setup guide covers installation and authentication before any project configuration applies.
The Go CLAUDE.md
The CLAUDE.md at your project root is read before every Claude Code session. For a Go project, it needs to answer: what is the module path, which HTTP framework is in use, how are packages organized, what struct tag conventions apply, how are errors handled, and how are tests run?
# Go project rules
## Module and versions
- Module: github.com/yourorg/yourservice
- Go: 1.22
- Run: `go build ./...` to verify, `go test ./...` to run all tests
- Linter: golangci-lint (config at .golangci.yml), run before marking any task complete
## HTTP framework
- Framework: Gin v1.10
- Router setup: routers/router.go, returned as *gin.Engine from SetupRouter()
- All route groups registered in SetupRouter(), not in main.go
- Middleware applied at group level, not per-route unless route-specific
## Project layout (Standard Go project layout)
- cmd/server/main.go, entry point, no business logic
- internal/handlers/, HTTP handler functions (one file per resource)
- internal/services/, business logic, interfaces defined here
- internal/repository/, data access layer, interfaces defined here
- internal/models/, domain models (no framework imports)
- internal/middleware/, Gin middleware functions
- pkg/, exported packages safe for external use
- config/, configuration structs and loader
- scripts/, migration scripts, code gen helpers
## Error handling
- Errors wrapped with fmt.Errorf("context: %w", err) at every boundary
- Sentinel errors defined in internal/errors/errors.go
- HTTP error responses use a shared helper: handlers.RespondError(c, err)
- No panic() in handlers or services. Recover in middleware only.
- log.Fatal() in main.go only, never in packages
## Hard rules
- No global variables outside of main.go and config initialisation
- Interfaces defined in the package that uses them, not the package that implements them
- Context passed as first argument to every function that does I/O
- No naked returns in functions longer than 3 lines
- No init() functions in business logic packages
Three sections of this CLAUDE.md prevent the most common Claude Code failures in Go.
The project layout section tells Claude where to place new code. Without it, Claude defaults to flat package structures or places handler logic in main.go. Go projects follow the cmd/internal/pkg convention, and Claude needs to know which parts of your project are in internal/ versus pkg/ to generate correct import paths.
The error handling section prevents inconsistent error wrapping. Claude will wrap errors correctly in isolation, but will vary between fmt.Errorf("context: %w", err), errors.Wrap() from github.com/pkg/errors, and raw err returns across generated code. One explicit rule locks the pattern.
The interface declaration rule matters for Go specifically. The Go convention is for interfaces to live in the package that consumes them, not the package that provides the implementation. Claude's training data includes significant Go code that defines interfaces on the implementation side. Without the explicit rule, Claude will generate interfaces in internal/repository/ that internal/services/ imports, inverting the dependency direction.
Gin handler patterns
Gin is used by approximately 48% of Go developers writing HTTP services (JetBrains Go developer survey 2026). Claude Code generates clean Gin handlers when given the right patterns in CLAUDE.md.
Add a handlers section:
## Gin handler conventions
### Handler function signature
// Handlers live in internal/handlers/{resource}.go
// Handler struct holds dependencies
type ItemHandler struct {
service services.ItemService
logger *slog.Logger
}
func NewItemHandler(service services.ItemService, logger *slog.Logger) *ItemHandler {
return &ItemHandler{service: service, logger: logger}
}
// Handler methods are receivers on the struct
func (h *ItemHandler) List(c *gin.Context) {
items, err := h.service.List(c.Request.Context())
if err != nil {
h.logger.Error("list items failed", "err", err)
RespondError(c, err)
return
}
c.JSON(http.StatusOK, items)
}
### Response helpers (internal/handlers/response.go)
func RespondError(c *gin.Context, err error) {
// Map sentinel errors to status codes
var notFound *errors.NotFoundError
if errors.As(err, ¬Found) {
c.JSON(http.StatusNotFound, gin.H{"error": notFound.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal error"})
}
### Binding and validation
// Always use ShouldBindJSON, not BindJSON (ShouldBind does not abort on error)
func (h *ItemHandler) Create(c *gin.Context) {
var req CreateItemRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// proceed with req
}
The handler struct pattern is the most important convention to establish. Without it, Claude will generate standalone handler functions that accept dependencies as parameters or, worse, access global variables. The struct approach keeps handlers testable because dependencies are injected at construction time.
The ShouldBindJSON versus BindJSON distinction is worth making explicit. BindJSON calls c.AbortWithError internally on validation failure, which interacts poorly with custom error middleware. ShouldBindJSON returns the error for you to handle explicitly.
Echo and stdlib net/http patterns
For projects using Echo instead of Gin, the CLAUDE.md handler section changes. Claude knows both frameworks but will default to Gin patterns if not directed:
## Echo handler conventions
### Handler function signature (Echo)
type ItemHandler struct {
service services.ItemService
}
// Echo handlers return error, not void
func (h *ItemHandler) List(c echo.Context) error {
items, err := h.service.List(c.Request().Context())
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, items)
}
### Echo middleware registration
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.RequestID())
// Group-level middleware
api := e.Group("/api/v1")
api.Use(AuthMiddleware)
For stdlib net/http projects without a framework, the pattern is different again:
## stdlib net/http conventions
### Handler pattern
type ItemHandler struct {
service services.ItemService
}
// Implement http.Handler interface
func (h *ItemHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.list(w, r)
case http.MethodPost:
h.create(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
### JSON response helper
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
Specifying which stdlib router you use also matters. Claude knows http.ServeMux, gorilla/mux, and chi, and will pick between them based on recent context. Add "Router: chi v5" or "Router: stdlib ServeMux" to the HTTP framework section.
Struct tag conventions
Go struct tags are where Claude Code produces the most inconsistent output without guidance. JSON field names, database column mappings, and validation rules all live in struct tags, and there is significant variation in community convention.
Add a struct tags section to your CLAUDE.md:
## Struct tag conventions
### Domain models (internal/models/)
// JSON: camelCase. DB: snake_case (matches column names). Validate: go-playground/validator v10.
type Item struct {
ID int64 `json:"id" db:"id"`
Name string `json:"name" db:"name" validate:"required,min=1,max=255"`
Description string `json:"description" db:"description"`
Price float64 `json:"price" db:"price" validate:"required,gt=0"`
CreatedAt time.Time `json:"createdAt" db:"created_at"`
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
}
### Request DTOs (internal/handlers/)
// No db tag on request structs. Validate required on all user-supplied fields.
type CreateItemRequest struct {
Name string `json:"name" validate:"required,min=1,max=255"`
Description string `json:"description"`
Price float64 `json:"price" validate:"required,gt=0"`
}
### Rules
- Never use json:"-" to hide a field unless it contains a secret
- Never omitempty on ID fields in response structs
- Always use pointer types for optional update fields (null = no change)
- Use time.Time for all timestamps, not int64 Unix timestamps
The separation between domain models with db tags and request DTOs without them prevents a common mistake: Claude will sometimes add db tags to request structs or reuse domain models as request bodies. The explicit split keeps the layers distinct and stops Claude from generating code that leaks internal model structure into the API contract.
The pointer type rule for update structs is worth examining. For PATCH-style updates, the pattern Name *string allows distinguishing between "field not included in request" (nil) and "field explicitly set to empty string" (""). Without this rule, Claude will generate update structs with non-pointer fields, making partial updates impossible without a custom merge function.
Table-driven tests with t.Run
Go's table-driven test pattern with t.Run is where Claude Code produces its best output when given a clear template. Without one, Claude writes individual test functions per case or uses a non-idiomatic loop structure.
Add to CLAUDE.md:
## Testing conventions
### Table-driven test pattern
func TestItemService_Create(t *testing.T) {
tests := []struct {
name string
input CreateItemRequest
want *Item
wantErr bool
errType error // sentinel error to check with errors.Is
}{
{
name: "valid item",
input: CreateItemRequest{Name: "Widget", Price: 9.99},
want: &Item{Name: "Widget", Price: 9.99},
},
{
name: "missing name",
input: CreateItemRequest{Price: 9.99},
wantErr: true,
errType: ErrValidation,
},
{
name: "negative price",
input: CreateItemRequest{Name: "Widget", Price: -1},
wantErr: true,
errType: ErrValidation,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := NewItemService(newMockRepo(t))
got, err := svc.Create(context.Background(), tt.input)
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
if tt.errType != nil && !errors.Is(err, tt.errType) {
t.Errorf("error type: got %T, want %T", err, tt.errType)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.Name != tt.want.Name {
t.Errorf("Name: got %q, want %q", got.Name, tt.want.Name)
}
})
}
}
### Mock interfaces
// testify/mock for service mocks in handler tests
// testify is the only allowed test assertion library
// Standard library t.Fatal/t.Error for everything else
// Run: go test ./... -race to check for data races
With this template in CLAUDE.md, Claude Code generates complete test tables for any new service method rather than skeleton tests with a single happy path. The errType field in the test struct is worth keeping because it forces Claude to test specific error types with errors.Is, not just the presence of any error.
The -race flag in the run instruction is important. Go's race detector catches data races in concurrent code, and Claude Code will generate goroutine patterns that occasionally have races in error paths or shared state. Adding go test ./... -race to the test run instruction means Claude checks for races as part of its own verification loop.
The Claude Code testing guide covers the broader test generation workflow, including how to drive TDD loops where Claude writes the test before the implementation.
GORM and sqlx patterns
For database work, Claude needs to know which library you use. GORM and sqlx are the most common choices, and they require different code patterns.
For GORM:
## Database: GORM v2
- ORM: GORM v2 (gorm.io/gorm)
- Driver: gorm.io/driver/postgres
- Session: *gorm.DB injected via dependency injection, never global
- Auto-migrate: never call AutoMigrate in production code
- Migrations: golang-migrate/migrate, files in migrations/
### GORM model conventions
type Item struct {
gorm.Model // includes ID, CreatedAt, UpdatedAt, DeletedAt
Name string `gorm:"not null;size:255"`
Description string
Price float64 `gorm:"not null"`
}
### Query patterns
// Use WithContext always
db.WithContext(ctx).Where("price > ?", minPrice).Find(&items)
// Preload for associations
db.WithContext(ctx).Preload("Category").Find(&items)
// Never use raw SQL without a comment explaining why
For sqlx:
## Database: sqlx
- Library: github.com/jmoiron/sqlx
- Driver: lib/pq (PostgreSQL)
- DB: *sqlx.DB injected via DI, never global
- Queries: named queries preferred over positional ($1, $2)
- Migrations: golang-migrate/migrate, files in migrations/
### sqlx query pattern
func (r *itemRepository) GetByID(ctx context.Context, id int64) (*Item, error) {
var item Item
err := r.db.GetContext(ctx, &item, "SELECT * FROM items WHERE id = $1", id)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return &item, err
}
Specifying which ORM you use also prevents Claude from generating GORM migrations using AutoMigrate in production code, a common Claude failure mode. GORM's AutoMigrate is a development convenience that silently drops columns and causes data loss when used against production schemas. The explicit rule "never call AutoMigrate in production code" blocks this pattern immediately.
The Claude Code database guide covers broader database workflow patterns including migration strategies and query optimization workflows that apply across Go and other languages.
Permission hooks for Go projects
Go projects have several commands that need gating before Claude Code can run them autonomously. Database migrations, go generate, and build scripts are the main candidates.
In .claude/settings.local.json:
{
"permissions": {
"allow": [
"Bash(go build ./...)",
"Bash(go test ./...)",
"Bash(go vet ./...)",
"Bash(golangci-lint run*)",
"Bash(go mod tidy)",
"Bash(migrate version*)",
"Bash(migrate goto*)"
],
"deny": [
"Bash(migrate up*)",
"Bash(migrate down*)",
"Bash(migrate drop*)",
"Bash(go generate ./...)",
"Bash(make deploy*)",
"Bash(docker-compose up*)"
]
}
}
This configuration lets Claude build, test, lint, and check migration state freely. It blocks Claude from applying or reverting database migrations, running go generate (which can modify generated code like protobuf stubs or mocks), and deploying.
The go generate block is specific to Go. Many Go projects use go generate to regenerate mock interfaces, protobuf bindings, or database query code. Claude should read these generated files for context but never regenerate them as a side effect of a task, because regeneration might overwrite manual changes or use a different generator version.
For the full picture of how Claude Code permission hooks work and what other categories of commands are worth gating, the Claude Code hooks guide covers the permission system across project types.
What Go developers get wrong first
Three patterns come up consistently when Go developers start using Claude Code on production codebases.
Not specifying the module path in CLAUDE.md. Go imports are absolute paths. Without the module path in CLAUDE.md, Claude generates import statements using placeholder paths like github.com/example/service that you have to find and replace. One line fixes this permanently.
Missing context propagation rules. Go's context.Context should be passed as the first argument to every function that does I/O. Claude knows this convention, but will sometimes generate functions that create their own context.Background() internally instead of accepting the caller's context. An explicit rule in CLAUDE.md, "context passed as first argument to every function that does I/O", eliminates this pattern.
No error type definitions. Claude generates error messages as strings by default. Go projects benefit from sentinel errors (var ErrNotFound = errors.New("not found")) and custom error types for structured error handling. Without a defined error strategy in CLAUDE.md, Claude will generate fmt.Errorf("item not found: %d", id) strings that cannot be matched with errors.Is. Define your sentinel errors in internal/errors/errors.go and reference that file in CLAUDE.md.
Getting more from your Go workflow
The CLAUDE.md configuration in this guide produces a Go setup where handler patterns are consistent across Gin, Echo, or stdlib, struct tags follow project conventions, table-driven tests are generated with complete cases, and permission hooks prevent destructive commands.
The payoff compounds over time. The first week, Claude Code saves you from fixing convention drift on generated handlers. The second week, it generates complete test tables without prompting. By the end of the first month, the generated code fits your codebase well enough that reviews focus on logic rather than convention.
For Go developers moving from reviewing Claude's output to trusting it as a primary author, the work is in the CLAUDE.md. The Claude Code best practices guide covers the configuration principles that apply across languages, with Go being one of the highest-leverage languages to configure well because of its strict compiler and convention-heavy community.
Claudify includes a Go-specific CLAUDE.md template as part of the Claude Code workflow kit, pre-configured for Gin, sqlx, table-driven tests, and golang-migrate permission hooks.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify