一键导入
go-foundations
Go coding conventions for dependency injection, module structure, error handling, testing, and context propagation. Load when working on .go files.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Go coding conventions for dependency injection, module structure, error handling, testing, and context propagation. Load when working on .go files.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | go-foundations |
| description | Go coding conventions for dependency injection, module structure, error handling, testing, and context propagation. Load when working on .go files. |
| argument-hint | Working on .go files — services, repositories, handlers, domain models, or tests |
| stack | go |
Baseline: Follow Effective Go as the foundation. Everything below extends or overrides where noted.
Read stack.md for the go_principles field to determine how strictly to apply these conventions:
adopt — New project or project born with these principles. All new code MUST follow the patterns. Structure new modules with the full internal/module/domain/service/repository/factory.go layout. Flag deviations as errors.recommend — Existing codebase not originally built with these patterns. New code follows the patterns. Existing code is left alone. When reviewing, suggest improvements as recommendations, not requirements. Phrase as: "Consider extracting..." or "This could benefit from...".off — Skill is inactive. Follow Effective Go only.If go_principles is not set in stack.md, default to recommend.
Interfaces are defined where they are consumed, not where they are implemented.
// internal/user/service/user_service.go — the CONSUMER defines what it needs
package service
import "context"
type UserRepository interface {
Create(ctx context.Context, u *User) (*User, error)
Update(ctx context.Context, u *User) (*User, error)
Delete(ctx context.Context, id string) error
GetByID(ctx context.Context, id string) (*User, error)
}
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
Interface size matches consumer needs. If the service needs 10 methods, the interface has 10 methods. Do not impose artificial caps.
The implementation struct is unexported (lowercase). The constructor is exported and returns the consumer's interface type.
// internal/user/repository/user_repository.go — the IMPLEMENTATION
package repository
import (
"context"
"database/sql"
"myapp/internal/user/service"
)
type userRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) service.UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) Create(ctx context.Context, u *service.User) (*service.User, error) {
// implementation
}
When one implementation satisfies interfaces from different consumers, the constructor returns a composite:
// service.UserRepository requires: FindByID, Save
// reporting.UserQuery requires: FindByID, ListActive
func NewUserRepository(db *sql.DB) interface {
service.UserRepository
reporting.UserQuery
} {
return &userRepository{db: db}
}
Each consumer receives only the interface it declared. The composite is only visible at the wiring site.
Each module has a factory.go at the module root that wires its internals and returns ready-to-use components.
// internal/user/factory.go
package user
import (
"database/sql"
"myapp/internal/user/repository"
"myapp/internal/user/service"
)
// Services exposes what this module provides to the outside world.
type Services struct {
UserService service.UserServiceAPI // exposed as interface for cross-module use
}
func NewModule(db *sql.DB) *Services {
repo := repository.NewUserRepository(db)
svc := service.NewUserService(repo)
return &Services{UserService: svc}
}
If the module has a single service, returning it directly is fine:
func NewModule(db *sql.DB) *service.UserService {
repo := repository.NewUserRepository(db)
return service.NewUserService(repo)
}
Pass interfaces through the factory — never concrete types or another module's internal packages:
// internal/task/factory.go
package task
func NewModule(db *sql.DB, userQuerier service.UserQuerier) *Services {
repo := repository.NewTaskRepository(db)
svc := service.NewTaskService(repo, userQuerier)
return &Services{TaskService: svc}
}
// cmd/server/main.go — composition root
userMod := user.NewModule(db)
taskMod := task.NewModule(db, userMod.UserService) // cross-module via interface
factory.go per module at the module root packageServices struct exposes interfaces, not concrete types for cross-module usemain.go only imports module root packages and shared packages — never internal/user/repository directlymain.go imports a sub-package of a module, the factory is incompleteinternal/
user/ # module: everything related to users
factory.go # wires internals, returns Services struct
domain/ # domain types for this module
user.go # User struct, value objects
service/ # business logic + interfaces it needs
user_service.go # UserService + UserRepository interface
user_service_test.go # unit tests with mockery mocks
mock_user_repository.go # generated by mockery (next to interface)
repository/ # data layer implementations
user_repository.go # unexported userRepository
task/ # module: everything related to tasks
factory.go
domain/
service/
repository/
shared/ # cross-module infrastructure
clock/ # testable time
slogx/ # context-propagated logger
aop/ # execution tracing
errorx/ # structured error types
cmd/
server/
main.go # composition root — imports modules, not layers
Within a module: layers import inward toward the domain.
handler -> service <- repository
^
domain (shared within the module)
user/domain for shared types. Keep domain packages free of business logic.When importing multiple packages with the same name, use meaningful aliases:
import (
userService "myapp/internal/user/service"
orderService "myapp/internal/order/service"
)
Never use service1, service2, or similar meaningless aliases.
Domain models live in domain/ and are internal to the module. They may carry ORM tags.
// internal/user/domain/user.go
package domain
import "time"
type User struct {
ID string `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex"`
FirstName string
LastName string
CreatedAt time.Time
UpdatedAt time.Time
}
For API representation, use a models/ package (or DTOs) with JSON tags.
When the shapes are identical, use type aliases for zero-cost refactor safety:
// internal/user/models/user.go
package models
import "myapp/internal/user/domain"
// Alias — external consumers import models.User, not domain.User.
// When the shapes diverge, break the alias into a concrete type with mapping.
type User = domain.User
When shapes diverge, introduce a concrete type with explicit mapping:
type UserResponse struct {
ID string `json:"id"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
IsActive bool `json:"isActive"`
CreatedAt string `json:"createdAt"`
}
func FromDomain(u *domain.User) *UserResponse {
return &UserResponse{
ID: u.ID,
Email: u.Email,
FirstName: u.FirstName,
LastName: u.LastName,
CreatedAt: u.CreatedAt.Format(time.RFC3339),
}
}
Rule: Start with aliases. Break into concrete types only when the domain and API shapes genuinely diverge.
Define sentinel errors for domain error categories. Wrap with %w at layer boundaries.
// internal/shared/errorx/errors.go
package errorx
import "errors"
var (
ErrNotFound = errors.New("not found")
ErrAlreadyExists = errors.New("already exists")
ErrNotAuthorized = errors.New("not authorized")
ErrValidation = errors.New("validation error")
)
For rich API responses, define an AppError that carries a code, message, and details while still supporting errors.Is():
// internal/shared/errorx/app_error.go
package errorx
import "fmt"
type AppError struct {
Cause error // the sentinel (ErrNotFound, etc.) — for errors.Is() routing
Code string // machine-readable code: "USER_NOT_FOUND"
Message string // human-readable message: "User with id 123 not found"
Details map[string]any // structured context: {"id": "123"}
}
func (e *AppError) Error() string {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Cause
}
// New creates a structured error wrapping a sentinel.
func New(cause error, code string, message string) *AppError {
return &AppError{Cause: cause, Code: code, Message: message}
}
// WithDetails adds structured context to the error.
func (e *AppError) WithDetails(details map[string]any) *AppError {
e.Details = details
return e
}
func (s *UserService) GetByID(ctx context.Context, id string) (*User, error) {
user, err := s.repo.GetByID(ctx, id)
if err != nil {
return nil, errorx.New(errorx.ErrNotFound, "USER_NOT_FOUND",
fmt.Sprintf("User with id %s not found", id),
).WithDetails(map[string]any{"id": id})
}
return user, nil
}
The middleware (or handler) uses errors.Is() on the sentinel to determine HTTP status, and the AppError fields for the response body:
// In the error-handling middleware or handler helper:
var appErr *errorx.AppError
if errors.As(err, &appErr) {
status := mapSentinelToHTTPStatus(appErr.Cause) // ErrNotFound -> 404
c.JSON(status, map[string]any{
"code": appErr.Code,
"message": appErr.Message,
"details": appErr.Details,
})
return
}
Key principle: Services never import net/http. The sentinel-to-status mapping lives at the transport boundary.
When a repository returns a raw error (e.g., from the database driver), wrap it with context:
func (r *userRepository) GetByID(ctx context.Context, id string) (*service.User, error) {
var user service.User
err := r.db.QueryRowContext(ctx, "SELECT ...", id).Scan(&user.ID, &user.Email)
if err != nil {
return nil, fmt.Errorf("querying user %s: %w", id, err)
}
return &user, nil
}
The service layer then decides whether to wrap this into an AppError or propagate it as-is.
Use mockery to generate mocks. Never hand-write mock structs.
Add go:generate directives next to the interface:
//go:generate mockery --name=UserRepository
type UserRepository interface {
Create(ctx context.Context, u *User) (*User, error)
GetByID(ctx context.Context, id string) (*User, error)
}
Mocks are generated next to the interface file (same directory), not in a mocks/ subfolder. Configure .mockery.yaml:
all: false
with-expecter: true
dir: "{{.InterfaceDir}}"
outpkg: "{{.PackageName}}"
filename: "mock_{{.InterfaceName | snakecase}}.go"
Use table-driven tests with t.Run subtests. Assertions use testify assert and require only — no suite, no testify mock package (mockery handles mocking).
func TestUserService_GetByID(t *testing.T) {
tests := []struct {
name string
id string
setupMock func(*MockUserRepository)
want *User
wantErr error
}{
{
name: "returns user when found",
id: "usr-123",
setupMock: func(m *MockUserRepository) {
m.EXPECT().GetByID(mock.Anything, "usr-123").
Return(&User{ID: "usr-123", Email: "test@example.com"}, nil)
},
want: &User{ID: "usr-123", Email: "test@example.com"},
},
{
name: "returns error when not found",
id: "usr-999",
setupMock: func(m *MockUserRepository) {
m.EXPECT().GetByID(mock.Anything, "usr-999").
Return(nil, fmt.Errorf("not found"))
},
wantErr: errorx.ErrNotFound,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRepo := NewMockUserRepository(t)
tt.setupMock(mockRepo)
svc := NewUserService(mockRepo)
got, err := svc.GetByID(context.Background(), tt.id)
if tt.wantErr != nil {
require.Error(t, err)
assert.True(t, errors.Is(err, tt.wantErr))
return
}
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
assert for soft checks, require for fatal checks. Use require when failure makes subsequent assertions meaningless (e.g., nil checks before accessing fields).mock.Anything for context.Context parameters. Always.mock.MatchedBy(func) for partial field matching when the exact value doesn't matter but a specific field does.t.Cleanup handle mock verification — NewMock*(t) auto-registers cleanup. Don't call AssertExpectations manually._test.go, not _test package). Prefer testing exported behavior; access unexported fields only when testing internal state that cannot be observed through the public API.Never call time.Now() directly in services or repositories.
// internal/shared/clock/clock.go
package clock
import (
"context"
"time"
)
type ctxKey struct{}
func WithTime(ctx context.Context, t time.Time) context.Context {
return context.WithValue(ctx, ctxKey{}, t)
}
func Now(ctx context.Context) time.Time {
if t, ok := ctx.Value(ctxKey{}).(time.Time); ok {
return t
}
return time.Now().UTC()
}
In services: use clock.Now(ctx) for all timestamps.
In tests: inject a fixed time via context:
fixed := time.Date(2026, 3, 19, 12, 0, 0, 0, time.UTC)
ctx := clock.WithTime(context.Background(), fixed)
// all clock.Now(ctx) calls return fixed
// internal/shared/slogx/slogx.go
package slogx
import (
"context"
"log/slog"
)
type ctxKey struct{}
func WithLogger(ctx context.Context, l *slog.Logger) context.Context {
return context.WithValue(ctx, ctxKey{}, l)
}
func Logger(ctx context.Context) *slog.Logger {
if l, ok := ctx.Value(ctxKey{}).(*slog.Logger); ok {
return l
}
return slog.Default()
}
Middleware enriches the logger with request attributes (request ID, method, path). Every layer reads via slogx.Logger(ctx).
Trace entry and exit of meaningful operations with identifying attributes from the method payload.
// internal/shared/aop/trace.go
package aop
import (
"context"
"fmt"
"time"
"myapp/internal/shared/slogx"
)
func Trace(ctx context.Context, operation string, args ...any) func() {
logger := slogx.Logger(ctx)
logger.Debug(fmt.Sprintf("-> %s", operation), args...)
start := time.Now()
return func() {
logger.Debug(fmt.Sprintf("<- %s (%s)", operation, time.Since(start)), args...)
}
}
Usage: defer aop.Trace(ctx, "UserService.Create", "email", email)()
Attributes to include: entity IDs, lookup keys, business-meaningful values (amount, status). Exclude: full request bodies, secrets, large arrays (log len(items) instead).
With*(ctx, value) context.Context + FromContext(ctx) / Logger(ctx) / Now(ctx)WithLogger or WithTime themselves (except in tests)All JSON field names use lowerCamelCase. Every exported struct field that appears in JSON must have an explicit json:"..." tag.
type UserResponse struct {
ID string `json:"id"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
IsActive bool `json:"isActive"`
CreatedAt string `json:"createdAt"`
Password string `json:"-"` // never serialize
}
Rules:
json:"-" for fields that must never be serialized (passwords, internal flags)json:"field,omitempty" for optional fields omitted when zero-valuedjson:"userId", json:"apiKey" — not json:"userID", json:"APIKey"When reviewing code in recommend mode, identify deviations from these patterns and provide direction without rewriting:
Flag the gap:
"This service creates its own database connection instead of receiving a repository through the constructor."
Suggest the target:
"Consider extracting a
UserRepositoryinterface in the service package, moving the DB logic to arepository/package with an unexported struct, and wiring them in afactory.goat the module root."
Do not:
recommend modeIn adopt mode, deviations in new code are errors — flag and fix. Existing code is still left alone unless the developer explicitly requests a refactoring plan.
Guidelines for building Flutter/Dart UI, theming, and reusable components. Use this skill whenever working on Flutter UI — creating or styling widgets, components, screens, buttons, pills, badges, inputs, navigation bars, drawers, dialogs; setting up or editing colors, ColorScheme, ThemeData, ThemeExtension, TextTheme, fonts, gradients, shadows; or any task that produces or modifies Flutter visual code. Apply it even when the user just says "build a screen", "make a button", "style this", or "add a component", not only when they mention theming explicitly. It enforces a strict palette/theme architecture, prefers native Material widgets over hand-built ones, and prevents unnecessary widget nesting.
Use this skill whenever Go code interacts with a database via GORM — including model definitions, querying, inserts/updates/deletes, transactions, preloading relations, joins, migrations, or repository implementations. Triggers include any mention of `gorm.DB`, `gorm:"..."` tags, `db.Find`, `db.Where`, `db.Create`, `db.Preload`, `db.Joins`, `db.Exec`, `db.Raw`, "use GORM", "load related records", "N+1", "master/detail", or any Go file that imports `gorm.io/gorm`. Apply this skill even when the user doesn't explicitly ask for GORM guidance — if they're touching GORM code, these rules apply.
Use this skill whenever a Flutter app needs to call a backend API — including initial API client setup, adding new endpoints, handling auth tokens, error mapping, retries, request/response logging, multipart uploads, or regenerating the client from an updated OpenAPI spec. Triggers include "add an endpoint", "call the backend", "Dio interceptor", "OpenAPI", "swagger", "regenerate the API client", "auth token", "401 refresh", or any HTTP work in a Flutter project. Apply even when the user doesn't mention OpenAPI by name — generated clients from the backend spec are the default pattern here.
Use this skill whenever a new Flutter/Dart project is being scaffolded, an existing Flutter project is being reorganized, or a new feature is being added to an existing Flutter app. Triggers include "new Flutter app", "start a Flutter project", "where should this file go in Flutter", "add a feature to my Flutter app", "refactor Flutter folder structure", or any mention of `lib/`, `pubspec.yaml`, or feature module organization in Dart code. Use this even when the user doesn't explicitly ask for structure advice — if they're starting a Flutter project or adding a feature, apply this layout by default.
Use this skill whenever state management decisions arise in Flutter code — including creating providers, choosing between Notifier/AsyncNotifier/FutureProvider/StreamProvider, handling async data, side effects in widgets, dependency injection, or testing stateful logic. Triggers include "add a provider", "manage state in Flutter", "Riverpod", "ref.watch", "ref.read", "AsyncValue", "how do I share state across screens", or any Flutter code change that involves reactive state. Apply this skill even when the user doesn't say "Riverpod" — it's the default for all Flutter state work in this codebase.
Use this skill whenever tests are being written, modified, or debugged in a Flutter project — including unit tests for providers/repositories, widget tests for screens, golden tests for visual regression, and integration tests for end-to-end flows. Triggers include "write a test for this", "test this Flutter code", "widget test", "golden test", "mock the API", "test a Riverpod provider", "pumpAndSettle", or any failing test discussion. Apply even when the user doesn't ask explicitly — when modifying production Flutter code, propose the matching tests.