| name | go-backend-init |
| description | Initialize a new Go backend repository with production-grade engineering setup. Use when creating new Go microservice projects, setting up Go repos from scratch, or when the user asks to scaffold a Go backend with best practices (Uber FX, Ent ORM, gRPC, CI/CD, linting, Docker). |
| metadata | {"author":"luoling8192","version":"2026.04.02"} |
Go Backend Repository Initialization Skill
Initialize a production-grade Go backend repository following battle-tested patterns.
When to Use
- User asks to create a new Go backend project
- User wants to scaffold a Go microservice repo
- User needs a Go project template with CI/CD, linting, Docker, etc.
Initialization Checklist
When invoked, walk through these steps in order. Ask the user for project-specific details first:
- Project name (Go module path, e.g.,
github.com/org/my-service)
- Service names (e.g.,
platform, api, worker — at least one)
- Go version (default: latest stable, currently 1.26)
- Database (PostgreSQL with Ent ORM is default; ask if needed)
- Proto/gRPC (yes/no — if yes, set up buf + grpc-gateway for HTTP/JSON)
- System dependencies (any C libraries like libsoxr, etc.)
Directory Structure
Create this structure, adapting service names to the user's input:
.
├── cmd/
│ └── <service>/
│ └── main.go # Cobra + Uber FX app entry point
├── internal/
│ ├── configs/ # Config loading (.env + env vars)
│ │ ├── config.go
│ │ └── module.go
│ ├── datastore/ # Database client initialization (Ent)
│ │ ├── datastore.go
│ │ └── module.go
│ ├── grpc/
│ │ ├── servers/ # gRPC + HTTP server lifecycle
│ │ ├── services/ # gRPC service implementations (business logic)
│ │ └── registers/ # Service registration with gRPC server
│ └── models/ # Data access layer (Ent ORM CRUD)
├── pkg/ # Reusable packages
│ ├── apierrors/ # Standardized error handling
│ └── logging/ # slog + tint colored console logger
├── schema/ # Ent ORM schema definitions
│ └── generate.go # go:generate directive for Ent
├── ent/ # (generated) Ent ORM code — DO NOT EDIT
├── api/
│ ├── proto/<service>/v1/ # .proto source files (if using gRPC)
│ └── generated/ # (generated) Proto/gRPC code — DO NOT EDIT
├── .github/
│ ├── actions/
│ │ └── setup-go-env/
│ │ └── action.yml # Composite action: Go + system deps
│ └── workflows/
│ └── ci.yml # Lint + Test + Build + Autofix
├── .vscode/
│ ├── settings.json
│ └── extensions.json
├── .editorconfig
├── .gitignore
├── .golangci.yml
├── .tool-versions
├── .env.example
├── buf.yaml # (if using gRPC) Buf CLI config
├── buf.gen.yaml # (if using gRPC) Proto code generation
├── cspell.config.yaml
├── renovate.json
├── Makefile # build, test, lint, generate, dev
├── Dockerfile
├── docker-compose.yml # Local dev infra (postgres, etc.)
├── go.mod
└── CLAUDE.md # AI assistant context
File Templates
1. go.mod
module {{MODULE_PATH}}
go {{GO_VERSION}}
require (
go.uber.org/fx v1.24.0
github.com/spf13/cobra v1.9.1
github.com/lmittmann/tint v1.1.3
github.com/samber/lo v1.50.0
github.com/nekomeowww/fo v1.3.0
github.com/nekomeowww/xo v1.9.0
entgo.io/ent v0.14.6
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.11
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0
github.com/stretchr/testify v1.11.1
)
After creating, run go mod tidy to resolve dependencies.
2. .editorconfig
root = true
[*.toml]
indent_size = 4
indent_style = space
max_line_length = 100
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{js,ts,vue,tsx,jsx,html,css,json,yaml,yml}]
indent_size = 2
indent_style = space
trim_trailing_whitespace = true
[*.go]
indent_size = 2
indent_style = tab
trim_trailing_whitespace = true
[*.proto]
indent_size = 2
indent_style = space
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
3. .golangci.yml
See references/golangci.yml for the full config.
Key principles:
- Use
version: "2" format
- Start with
default: all, then disable noisy/opinionated linters
- Enable
gofmt + goimports formatters
- Exclude generated code paths (
api/, ent/) with generated: lax
- Set sane thresholds:
dupl: 600, nestif: min-complexity: 9
4. .gitignore
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin/*
dist/*
out/*
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
# Go workspace file
go.work
# Editor and IDE
.idea
*.swp
*.swo
*~
.DS_Store
# Config files (keep example)
config/*
!config/config.yaml
__debug*
.env
.env.local
# Binary outputs (add your service names)
./main
5. CI Workflow (.github/workflows/ci.yml)
See references/ci.yml for the full workflow.
Key jobs:
- Lint (push only): golangci-lint with 7m timeout
- Unit Test:
go test ./...
- Nilaway (null safety): static analysis excluding generated code
- Build Test (PR only):
go build ./...
- Autofix (PR only): auto-fix lint and commit with
[autofix] tag, with conflict guard
6. Composite Action (.github/actions/setup-go-env/action.yml)
name: setup-go-env
description: Setup Go environment with required dependencies
runs:
using: composite
steps:
- uses: actions/setup-go@v6
with:
go-version: "^{{GO_VERSION}}"
cache: true
7. .vscode/settings.json
{
"go.useLanguageServer": true,
"go.lintOnSave": "package",
"go.vetOnSave": "workspace",
"go.coverOnSave": false,
"go.lintTool": "golangci-lint-v2",
"go.lintFlags": [
"--fast-only",
"--config=${workspaceFolder}/.golangci.yml"
],
"go.formatTool": "gofmt",
"go.inferGopath": true,
"go.coverOnSingleTest": true,
"go.testTimeout": "900s",
"go.testFlags": ["-v", "-count=1"],
"go.toolsManagement.autoUpdate": true,
"gopls": {
"build.buildFlags": [],
"ui.completion.usePlaceholders": true,
"ui.semanticTokens": true
},
"[go]": {
"editor.insertSpaces": false,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": "always"
}
},
"go.testEnvVars": {
"LOG_LEVEL": "debug"
}
}
8. .vscode/extensions.json
{
"recommendations": [
"streetsidesoftware.code-spell-checker",
"mikestead.dotenv",
"EditorConfig.EditorConfig",
"yzhang.markdown-all-in-one",
"redhat.vscode-yaml",
"golang.go",
"bufbuild.vscode-buf"
]
}
9. renovate.json
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":dependencyDashboard",
":semanticPrefixFixDepsChoreOthers",
":prHourlyLimitNone",
":prConcurrentLimitNone",
":ignoreModulesAndTests",
"group:monorepos",
"group:recommended",
"group:allNonMajor",
"replacements:all",
"workarounds:all"
],
"rangeStrategy": "bump",
"labels": ["dependencies"],
"minimumReleaseAge": "3 days"
}
10. .tool-versions
golang {{GO_VERSION}}
11. cspell.config.yaml
version: "0.2"
ignorePaths: []
dictionaryDefinitions: []
dictionaries: []
words:
- entgo
- Ents
- entsql
- enttest
- fxevent
- nolint
- stretchr
- structpb
- timestamppb
12. Dockerfile (multi-stage)
See references/Dockerfile for the full template.
Key patterns:
- Build stage:
golang:<version>-bookworm with system deps
- Go mod download first (layer caching)
- Build all services under
./cmd/* dynamically
- Runtime stage:
debian:bookworm-slim (minimal image)
- Use
--mount=type=cache for Go build cache
13. Makefile
.PHONY: build test lint generate dev
build:
go build -o bin/{{SERVICE_NAME}} ./cmd/{{SERVICE_NAME}}/
test:
go test ./... -v -count=1
lint:
golangci-lint run ./...
generate:
go generate ./...
dev:
go run ./cmd/{{SERVICE_NAME}}/
14. Functional Utility Libraries
Always include samber/lo, nekomeowww/fo, and nekomeowww/xo. Prefer functional style over imperative loops.
samber/lo — Lodash-style generics utilities (slices, maps, transforms):
active := lo.Filter(users, func(u *User, _ int) bool {
return u.IsActive
})
names := lo.Map(users, func(u *User, _ int) string {
return u.Name
})
admin, ok := lo.Find(users, func(u *User) bool {
return u.Role == "admin"
})
byRole := lo.GroupBy(users, func(u *User) string {
return u.Role
})
port := lo.CoalesceOrEmpty(os.Getenv("PORT"), "8080")
nekomeowww/fo — Error suppression and function invocation:
val := fo.May(strconv.Atoi(os.Getenv("PORT")))
dsn := fo.May(lo.Coalesce(os.Getenv("DATABASE_URL"), defaultDSN))
result := fo.InvokeWithTimeout(ctx, 5*time.Second, func() (string, error) {
return fetchRemoteConfig()
})
nekomeowww/xo — Extended utilities (types, infra, streaming):
Functional style guidelines:
- Prefer
lo.Map/lo.Filter/lo.Reduce over manual for loops for data transformations
- Use
fo.May for safe unwrapping in non-critical paths (config defaults, optional values)
- Compose functions: chain
lo and fo together for readable pipelines
- Never use
any type — use generics with constraints from lo/fo/xo
15. Logging Package (pkg/logging/)
Use tint for colorized text console output. Never use JSON handler for local dev.
package logging
import (
"io"
"log/slog"
"github.com/lmittmann/tint"
)
func New(level string, w io.Writer) *slog.Logger {
return slog.New(tint.NewHandler(w, &tint.Options{
Level: parseLevel(level),
}))
}
func parseLevel(level string) slog.Level {
switch level {
case "debug":
return slog.LevelDebug
case "warn":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}
16. Cobra + Uber FX Entry Point (cmd/<service>/main.go)
package main
import (
"log/slog"
"os"
"github.com/spf13/cobra"
"go.uber.org/fx"
"{{MODULE_PATH}}/internal/configs"
"{{MODULE_PATH}}/internal/datastore"
"{{MODULE_PATH}}/internal/models"
"{{MODULE_PATH}}/pkg/logging"
)
func main() {
rootCmd := &cobra.Command{
Use: "{{SERVICE_NAME}}",
Short: "{{SERVICE_DESCRIPTION}}",
RunE: func(cmd *cobra.Command, args []string) error {
return run()
},
}
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
func run() error {
cfg, err := configs.Load()
if err != nil {
return err
}
logger := logging.New(cfg.LogLevel, os.Stdout)
app := fx.New(
fx.Supply(cfg),
fx.Provide(func() *slog.Logger { return logger }),
datastore.Module(),
models.Modules(),
)
app.Run()
return nil
}
Key points:
- FX uses default stdout logger — do NOT route FX logs through slog; let FX print directly
logging.New (tint) — colorized text output, not JSON
- Cobra wraps the root command for CLI extensibility (subcommands, flags)
- Logger is created outside FX, then supplied into the graph
17. Config Pattern (internal/configs/)
package configs
import (
"os"
"github.com/joho/godotenv"
)
type Config struct {
LogLevel string
DatabaseDSN string
HTTPPort string
GRPCPort string
}
func Load() (*Config, error) {
_ = godotenv.Load()
cfg := &Config{
LogLevel: getEnv("LOG_LEVEL", "info"),
DatabaseDSN: getEnv("DATABASE_DSN", "postgres://user:pass@localhost:5432/dbname?sslmode=disable"),
HTTPPort: getEnv("HTTP_PORT", "8080"),
GRPCPort: getEnv("GRPC_PORT", "9090"),
}
return cfg, nil
}
func getEnv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
package configs
import "go.uber.org/fx"
func Module() fx.Option {
return fx.Options(
fx.Provide(Load),
)
}
18. FX Module Pattern
All services MUST follow this pattern — fx.In params struct + lifecycle hooks:
package myservice
import (
"context"
"log/slog"
"go.uber.org/fx"
"{{MODULE_PATH}}/internal/configs"
"{{MODULE_PATH}}/ent"
)
type Params struct {
fx.In
Config *configs.Config
Logger *slog.Logger
DB *ent.Client
}
type Service struct {
config *configs.Config
logger *slog.Logger
db *ent.Client
}
func NewService(p Params) *Service {
return &Service{
config: p.Config,
logger: p.Logger.With("module", "myservice"),
db: p.DB,
}
}
func Module() fx.Option {
return fx.Options(
fx.Provide(NewService),
fx.Invoke(func(lc fx.Lifecycle, svc *Service) {
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
svc.logger.Info("starting myservice")
return svc.Start(ctx)
},
OnStop: func(ctx context.Context) error {
svc.logger.Info("stopping myservice")
return svc.Stop(ctx)
},
})
}),
)
}
19. Ent ORM Schema (schema/generate.go)
package schema
Schema conventions:
- UUID primary keys:
field.UUID("id", uuid.UUID{}).Default(uuid.New).Unique().Immutable()
- Soft delete:
field.Time("deleted_at").Optional().Nillable()
- Enum status fields:
field.Enum("status").Values("ACTIVE", "INACTIVE")
- Timestamps:
field.Time("created_at").Default(time.Now).Immutable()
20. Datastore Module (internal/datastore/)
package datastore
import (
"context"
"fmt"
"log/slog"
"go.uber.org/fx"
"{{MODULE_PATH}}/ent"
"{{MODULE_PATH}}/internal/configs"
_ "github.com/lib/pq"
)
type Params struct {
fx.In
Config *configs.Config
Logger *slog.Logger
}
func NewClient(p Params) (*ent.Client, error) {
client, err := ent.Open("postgres", p.Config.DatabaseDSN)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
return client, nil
}
func Module() fx.Option {
return fx.Options(
fx.Provide(NewClient),
fx.Invoke(func(lc fx.Lifecycle, client *ent.Client, logger *slog.Logger) {
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
logger.Info("running database auto-migration")
return client.Schema.Create(ctx)
},
OnStop: func(ctx context.Context) error {
return client.Close()
},
})
}),
)
}
21. API Error Handling (pkg/apierrors/)
package apierrors
import (
"fmt"
"net/http"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type APIError struct {
Code int
Message string
Err error
}
func (e *APIError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
return e.Message
}
func (e *APIError) Unwrap() error { return e.Err }
func (e *APIError) GRPCStatus() *status.Status {
return status.New(httpToGRPC(e.Code), e.Message)
}
var (
ErrNotFound = &APIError{Code: http.StatusNotFound, Message: "not found"}
ErrUnauthorized = &APIError{Code: http.StatusUnauthorized, Message: "unauthorized"}
ErrForbidden = &APIError{Code: http.StatusForbidden, Message: "forbidden"}
ErrInvalidInput = &APIError{Code: http.StatusBadRequest, Message: "invalid input"}
ErrInternal = &APIError{Code: http.StatusInternalServerError, Message: "internal error"}
)
func httpToGRPC(code int) codes.Code {
switch code {
case 400:
return codes.InvalidArgument
case 401:
return codes.Unauthenticated
case 403:
return codes.PermissionDenied
case 404:
return codes.NotFound
case 409:
return codes.AlreadyExists
default:
return codes.Internal
}
}
22. Buf CLI Config (if using gRPC)
buf.yaml:
version: v2
lint:
use:
- STANDARD
breaking:
use:
- FILE
buf.gen.yaml:
version: v2
managed:
enabled: true
override:
- file_option: go_package_prefix
value: {{MODULE_PATH}}/api/generated
plugins:
- remote: buf.build/protocolbuffers/go
out: api/generated
opt: paths=source_relative
- remote: buf.build/grpc/go
out: api/generated
opt: paths=source_relative
- remote: buf.build/grpc-ecosystem/gateway
out: api/generated
opt: paths=source_relative
- remote: buf.build/grpc-ecosystem/openapiv2
out: api/generated/openapiv2
opt: merge_file_name={{SERVICE}},output_format=yaml,allow_merge=true
23. Docker Compose (local dev)
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_USER: {{SERVICE_NAME}}
POSTGRES_PASSWORD: {{SERVICE_NAME}}
POSTGRES_DB: {{SERVICE_NAME}}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
24. .env.example
LOG_LEVEL=info
DATABASE_DSN=postgres://{{SERVICE_NAME}}:{{SERVICE_NAME}}@localhost:5432/{{SERVICE_NAME}}?sslmode=disable
HTTP_PORT=8080
GRPC_PORT=9090
25. CLAUDE.md Template
Generate a CLAUDE.md that documents:
- Project overview (what it does, key capabilities)
- Technology stack table (layer → technology)
- Essential commands (build, test, lint, generate, dev, docker)
- Architecture (modules, layers)
- Directory structure with annotations
- DI patterns (Uber FX constructors, lifecycle hooks)
- Code style (functional-first, slog logging, error wrapping, table-driven tests)
Use the reference project's CLAUDE.md as a structural template, but tailor content to the new project.
Post-Initialization Steps
After scaffolding, run these commands:
go mod init {{MODULE_PATH}}
go mod tidy
git init
go generate ./...
golangci-lint run
go build ./...
git add .
git commit -m "feat: initial project scaffold"
Key Principles
- Functional-first — prefer
lo.Map/lo.Filter/lo.Reduce over imperative for loops; use fo.May for safe unwrapping; compose small pure functions instead of OOP inheritance
- Use
samber/lo, nekomeowww/fo, nekomeowww/xo — these are standard utilities in every project; never rewrite what they already provide
- Config via .env — no YAML config files; use environment variables with
.env for local dev
- Uber FX everywhere — all DI through FX constructors with
fx.In params structs; never manual instantiation
- FX default logger — let FX use its default stdout output; do NOT wire FX logs through slog
- Tint for logging — colorized text console output via
pkg/logging; never JSON handler for dev
- Cobra for CLI — wrap the root command with
cobra.Command for flags and subcommand extensibility
- Generated code is sacred — never edit
ent/ or api/generated/, always regenerate
- Soft delete by default —
deleted_at field on user-facing entities
- Structured logging —
*slog.Logger injected via FX with .With("module", "name") for context; never fmt.Println
- Error wrapping — always
fmt.Errorf("context: %w", err); use pkg/apierrors at API boundaries
- Table-driven tests — with
testify/assert; test file suffix _test.go in the same package
- Lifecycle hooks — use
fx.Lifecycle with OnStart/OnStop for resources that need cleanup
- No
any type — use concrete types or generics with constraints; any requires explicit justification