一键导入
go-practices
Go-specific patterns for dependency injection, interface design, testing with mockery, and idiomatic project structure.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Go-specific patterns for dependency injection, interface design, testing with mockery, and idiomatic project structure.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Document a bug report with reproduction steps, severity, and expected behavior. Use when the user explicitly requests the Virtual Team bug workflow or when this workflow is the next stage of an active Virtual Team pipeline.
Quiz the developer on technical decisions in the current work. Use when the user explicitly requests the Virtual Team check workflow or when this workflow is the next stage of an active Virtual Team pipeline.
Create clean, atomic git commits following project conventions. Use when the user explicitly requests the Virtual Team commit workflow or when this workflow is the next stage of an active Virtual Team pipeline.
Extract, define, and validate API contracts as concrete schema files. Use when the user explicitly requests the Virtual Team contracts workflow or when this workflow is the next stage of an active Virtual Team pipeline.
Investigate a bug, reproduce it, trace the code, and document the root cause. Use when the user explicitly requests the Virtual Team debug workflow or when this workflow is the next stage of an active Virtual Team pipeline.
Query project conventions and architectural decisions with source references. Use when the user explicitly requests the Virtual Team decisions workflow or when this workflow is the next stage of an active Virtual Team pipeline.
| name | go-practices |
| description | Go-specific patterns for dependency injection, interface design, testing with mockery, and idiomatic project structure. |
| stack | go |
| loaded_when | Working on .go files — services, repositories, handlers, or tests |
These are Go-specific conventions that implement the architectural principles from the design-principles skill with concrete Go patterns. When this skill conflicts with a plugin skill, this skill wins — it reflects the project's actual conventions.
Go's implicit interface satisfaction makes dependency inversion natural — but only if you follow the pattern consistently.
Interfaces are defined where they are consumed, not where they are implemented.
// internal/user/service/user_service.go — the CONSUMER defines what it needs
type UserRepository interface {
FindByID(ctx context.Context, id string) (*model.User, error)
Save(ctx context.Context, user *model.User) error
}
type UserService struct {
repo UserRepository // interface, not *repository.userRepository
email EmailSender // interface, not *smtp.Client
logger Logger // interface, not *zap.Logger
}
func NewUserService(repo UserRepository, email EmailSender, logger Logger) *UserService {
return &UserService{repo: repo, email: email, logger: logger}
}
The implementation struct is unexported (lowercase). The constructor is exported and returns the consumer's interface type, not the concrete type. This gives two guarantees:
// internal/user/repository/postgres_user.go — the IMPLEMENTATION
import (
"myapp/internal/user/model"
"myapp/internal/user/service"
)
// unexported struct — no one outside this package can name it
type userRepository struct {
db *sql.DB
}
// exported constructor returns the consumer's interface
func NewUserRepository(db *sql.DB) service.UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) FindByID(ctx context.Context, id string) (*model.User, error) {
// implementation
}
func (r *userRepository) Save(ctx context.Context, user *model.User) error {
// implementation
}
Why this matters: If userRepository is missing a method that service.UserRepository requires, the compiler error appears in NewUserRepository — right where you're building the implementation. You don't discover it later in main.go or when a test fails.
When the same implementation satisfies interfaces from different consumers, the constructor returns a composite interface:
// Two different consumers need different views of the same repository
// package service: type UserRepository interface { FindByID(...); Save(...) }
// package reporting: type UserQuery interface { FindByID(...); ListActive(...) }
// The constructor returns both
func NewUserRepository(db *sql.DB) interface {
service.UserRepository
reporting.UserQuery
} {
return &userRepository{db: db}
}
Each consumer still receives only the interface it declared — the composite is only visible at the wiring site. This keeps each consumer's dependency minimal.
Each module exports a factory function at the module root package (internal/user/) that wires its own internals and returns a struct of ready-to-use components. This keeps the composition root clean — main.go only knows about modules, not their internal layers.
// internal/user/module.go — the module's public factory
package user
import (
"database/sql"
"myapp/internal/user/handler"
"myapp/internal/user/repository"
"myapp/internal/user/service"
)
// Services exposes what this module provides to the outside world
type Services struct {
Handler *handler.UserHandler
UserService service.UserService // exposed as interface for cross-module use
}
// New wires the module's internal dependency graph and returns ready-to-use components
func New(db *sql.DB, emailSender service.EmailSender, logger service.Logger) *Services {
repo := repository.NewUserRepository(db) // returns service.UserRepository
svc := service.NewUserService(repo, emailSender, logger)
h := handler.NewUserHandler(svc)
return &Services{
Handler: h,
UserService: svc,
}
}
Why this matters: Without the factory, main.go ends up with aliased imports for every layer of every module (userRepo, userSvc, userHandler, taskRepo, taskSvc...). With the factory, main.go only imports module root packages — no aliasing needed.
// cmd/server/main.go — clean composition root
import (
"myapp/internal/user"
"myapp/internal/task"
"myapp/internal/shared/email"
)
func main() {
db := setupDB()
emailSender := email.NewEmailSender(cfg.SMTP)
// One call per module — each module wires itself
userMod := user.New(db, emailSender, logger)
taskMod := task.New(db, logger)
// Wire routes
router := gin.Default()
userMod.Handler.RegisterRoutes(router)
taskMod.Handler.RegisterRoutes(router)
router.Run(":8080")
}
When one module needs another module's service, pass the interface through the factory:
// internal/task/module.go
package task
type Services struct {
Handler *handler.TaskHandler
TaskService service.TaskService
}
// task.New receives the user service interface — not the user module struct
func New(db *sql.DB, userService service.UserQuerier, logger service.Logger) *Services {
repo := repository.NewTaskRepository(db)
svc := service.NewTaskService(repo, userService, logger)
h := handler.NewTaskHandler(svc)
return &Services{Handler: h, TaskService: svc}
}
// cmd/server/main.go — cross-module wiring is explicit
userMod := user.New(db, emailSender, logger)
taskMod := task.New(db, userMod.UserService, logger) // task depends on user's service interface
The dependency between modules is visible in main.go and flows through interfaces — never through concrete types or repository access.
module.go at the module root packageServices struct exposes interfaces, not concrete types — other modules depend on the interface, not the implementationtask.New needs user data, it receives service.UserQuerier (the interface the task module defines), not *user.Servicesmain.go only imports module root packages and shared packages — never internal/user/repository or internal/user/service directlymain.go importing a sub-package of a module, the module factory is incompleteUse mockery to generate mocks from interfaces. Never write mock implementations by hand.
Why:
AssertExpectations, On, Return) across the entire codebasego generate updates all mocks# .mockery.yaml at project root
all: false
with-expecter: true
dir: "{{.InterfaceDir}}/mocks"
outpkg: mocks
packages:
# CUSTOMIZE: List your packages that define interfaces to mock
github.com/yourorg/yourapp/internal/user/service:
interfaces:
UserRepository:
EmailSender:
github.com/yourorg/yourapp/internal/task/service:
interfaces:
TaskRepository:
# Generate all mocks defined in .mockery.yaml
go generate ./...
# Or run mockery directly
mockery
Add a go:generate directive in the file that defines the interface:
//go:generate mockery --name=UserRepository
type UserRepository interface {
FindByID(ctx context.Context, id string) (*User, error)
Save(ctx context.Context, user *User) error
}
// internal/user/service/user_service_test.go
func TestUserService_CreateUser(t *testing.T) {
// Arrange — create mock instances (generated by mockery into service/mocks/)
mockRepo := mocks.NewMockUserRepository(t) // auto-cleanup via t.Cleanup
mockEmail := mocks.NewMockEmailSender(t)
// Set expectations
mockRepo.EXPECT().Save(mock.Anything, mock.AnythingOfType("*model.User")).Return(nil)
mockEmail.EXPECT().SendWelcome(mock.Anything, mock.AnythingOfType("*model.User")).Return(nil)
// Act — inject mocks through constructor
svc := NewUserService(mockRepo, mockEmail, logger)
err := svc.CreateUser(ctx, "test@example.com", "Test User")
// Assert
assert.NoError(t, err)
// mockRepo and mockEmail auto-verify expectations via t.Cleanup
}
Mock rules:
mock.Anything for context.Context parametersmock.AnythingOfType("*service.User") when the exact value doesn't mattermock.MatchedBy(func(u *service.User) bool { return u.Email == "..." }) when you need to assert on specific fieldst.Cleanup handle expectation verification — don't call AssertExpectations manually when using NewMock*(t)All JSON field names use lowerCamelCase. Define explicit json struct tags on every exported field — never rely on Go's default (which would expose PascalCase).
type User struct {
ID string `json:"id"`
Email string `json:"email"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
IsActive bool `json:"isActive"`
}
Rules:
json:"..." tagjson:"-" for fields that must never be serialized (passwords, internal flags)json:"field,omitempty" for optional fields that should be omitted when zero-valuedjson:"billingAddress", not json:"billing_address"json:"userId", json:"apiKey", json:"htmlContent" — not json:"userID", json:"APIKey"type CreateUserRequest struct {
Email string `json:"email" binding:"required,email"`
FirstName string `json:"firstName" binding:"required"`
LastName string `json:"lastName" binding:"required"`
Password string `json:"password" binding:"required,min=8"`
}
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
}
If a different project needs snake_case: change this section to document snake_case conventions (json:"first_name", json:"created_at") and update the examples. The rest of the skill is unaffected.
Define sentinel errors or typed errors for business rule violations:
// package service
var (
ErrUserNotFound = errors.New("user not found")
ErrEmailTaken = errors.New("email already registered")
ErrNotAuthorized = errors.New("not authorized")
)
// For errors that carry context
type ValidationError struct {
Field string
Message string
}
Wrap errors at layer boundaries to add context without losing the original:
func (s *UserService) FindByID(ctx context.Context, id string) (*User, error) {
user, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("finding user %s: %w", id, err)
}
return user, nil
}
The API layer uses errors.Is() to translate domain errors to HTTP responses — services never import net/http.
These packages live in internal/shared/ (or internal/pkg/, internal/platform/ — pick one, be consistent). They follow the same principle: wrap a standard library concern behind a context-propagated interface so every layer gets consistent behavior and tests can override it.
Never call time.Now() directly in services or repositories. Use a clock package that provides the current time through an overridable default and context propagation.
// internal/shared/clock/clock.go
package clock
import (
"context"
"time"
)
type ctxKey struct{}
// Default is the package-level clock used when no override is set.
// Tests replace this to control time.
var Default = func() time.Time { return time.Now() }
// Now returns the current time from the context (if set) or the default clock.
func Now(ctx context.Context) time.Time {
if t, ok := ctx.Value(ctxKey{}).(time.Time); ok {
return t
}
return Default()
}
// WithTime injects a fixed time into the context.
// Used by middleware to pin all timestamps within a request to the same moment.
func WithTime(ctx context.Context, t time.Time) context.Context {
return context.WithValue(ctx, ctxKey{}, t)
}
Usage in middleware — pin the request time so created_at and updated_at within the same request are identical:
// internal/shared/middleware/clock.go
func ClockMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
ctx := clock.WithTime(c.Request.Context(), time.Now())
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
Usage in services/repositories:
func (s *UserService) CreateUser(ctx context.Context, email string) (*model.User, error) {
now := clock.Now(ctx) // consistent within the request
user := &model.User{
Email: email,
CreatedAt: now,
UpdatedAt: now,
}
return user, s.repo.Save(ctx, user)
}
Usage in tests — control time without interfaces or injection:
func TestCreateUser_SetsTimestamps(t *testing.T) {
fixed := time.Date(2026, 3, 19, 12, 0, 0, 0, time.UTC)
ctx := clock.WithTime(context.Background(), fixed)
// ... create user with ctx
assert.Equal(t, fixed, user.CreatedAt)
}
Use log/slog with context propagation. Middleware enriches the logger with request attributes (request ID, user ID, method, path), and every layer reads the logger from context — ensuring all log lines within a request carry the same trace attributes.
// internal/shared/logger/logger.go
package logger
import (
"context"
"log/slog"
)
type ctxKey struct{}
// FromContext returns the logger from context, or the default slog logger.
func FromContext(ctx context.Context) *slog.Logger {
if l, ok := ctx.Value(ctxKey{}).(*slog.Logger); ok {
return l
}
return slog.Default()
}
// WithLogger injects a logger into the context.
func WithLogger(ctx context.Context, l *slog.Logger) context.Context {
return context.WithValue(ctx, ctxKey{}, l)
}
Usage in middleware — enrich with request attributes:
// internal/shared/middleware/logger.go
func LoggerMiddleware(base *slog.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
reqLogger := base.With(
slog.String("request_id", c.GetHeader("X-Request-ID")),
slog.String("method", c.Request.Method),
slog.String("path", c.Request.URL.Path),
slog.String("remote_addr", c.ClientIP()),
)
ctx := logger.WithLogger(c.Request.Context(), reqLogger)
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
Usage in any layer — handler, service, repository all use the same pattern:
func (s *UserService) CreateUser(ctx context.Context, email string) (*model.User, error) {
log := logger.FromContext(ctx)
log.Info("creating user", slog.String("email", email))
// ... business logic
if err != nil {
log.Error("failed to create user", slog.String("email", email), slog.Any("error", err))
return nil, err
}
return user, nil
}
Every log line from the same request automatically carries request_id, method, path — no manual threading required.
A trace package that logs entry and exit of meaningful layers with identifying attributes from the method payload. The goal is not to log everything — it's to include enough context in each trace line that you can identify which entity is being processed without cross-referencing other logs.
// internal/shared/aop/trace.go
package aop
import (
"context"
"log/slog"
"time"
"myapp/internal/shared/logger"
)
// Trace logs entry and exit of a named operation with timing and contextual attributes.
// Attrs should be identifying information from the method payload — IDs, names, status.
// Usage: defer aop.Trace(ctx, "UserService.CreateUser", slog.String("email", email))()
func Trace(ctx context.Context, operation string, attrs ...slog.Attr) func() {
log := logger.FromContext(ctx)
// Build attribute list: operation name + caller-provided attrs
traceAttrs := make([]any, 0, len(attrs)+1)
traceAttrs = append(traceAttrs, slog.String("op", operation))
for _, attr := range attrs {
traceAttrs = append(traceAttrs, attr)
}
log.Debug("entering", traceAttrs...)
start := time.Now()
return func() {
traceAttrs = append(traceAttrs, slog.Duration("duration", time.Since(start)))
log.Debug("exiting", traceAttrs...)
}
}
Usage — pass identifying attributes from the method parameters. The defer + trailing () pattern ensures exit is logged even on panic or early return:
func (h *UserHandler) CreateUser(c *gin.Context) {
// Handler: trace the inbound request with route-level identifiers
defer aop.Trace(c.Request.Context(), "UserHandler.CreateUser",
slog.String("content_type", c.ContentType()),
)()
var req CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil { ... }
// After parsing, the service call carries the meaningful identifiers
user, err := h.service.CreateUser(c.Request.Context(), req.Email, req.Name)
// ...
}
func (s *UserService) CreateUser(ctx context.Context, email, name string) (*model.User, error) {
// Service: trace with the business-meaningful identifiers
defer aop.Trace(ctx, "UserService.CreateUser",
slog.String("email", email),
slog.String("name", name),
)()
// ...
}
func (s *UserService) AssignTask(ctx context.Context, taskID, assigneeID string) error {
// Service: trace with entity IDs that identify the operation
defer aop.Trace(ctx, "UserService.AssignTask",
slog.String("task_id", taskID),
slog.String("assignee_id", assigneeID),
)()
// ...
}
func (r *userRepository) Save(ctx context.Context, user *model.User) error {
// Repository: trace with the entity's primary identifier
defer aop.Trace(ctx, "UserRepository.Save",
slog.String("user_id", user.ID),
slog.String("email", user.Email),
)()
// ...
}
func (r *userRepository) FindByID(ctx context.Context, id string) (*model.User, error) {
// Repository: trace with the lookup key
defer aop.Trace(ctx, "UserRepository.FindByID",
slog.String("user_id", id),
)()
// ...
}
func (c *paymentClient) Charge(ctx context.Context, invoiceID string, amount int64) error {
// External client: trace with identifiers + the value being sent
defer aop.Trace(ctx, "PaymentClient.Charge",
slog.String("invoice_id", invoiceID),
slog.Int64("amount_cents", amount),
)()
// ...
}
Resulting log output for a single request:
level=DEBUG msg=entering op=UserHandler.CreateUser content_type=application/json request_id=abc-123
level=DEBUG msg=entering op=UserService.CreateUser email=test@example.com name="Test User" request_id=abc-123
level=DEBUG msg=entering op=UserRepository.Save user_id=usr_7f3a email=test@example.com request_id=abc-123
level=DEBUG msg=exiting op=UserRepository.Save user_id=usr_7f3a email=test@example.com duration=2.1ms request_id=abc-123
level=DEBUG msg=exiting op=UserService.CreateUser email=test@example.com name="Test User" duration=3.4ms request_id=abc-123
level=DEBUG msg=exiting op=UserHandler.CreateUser content_type=application/json duration=4.8ms request_id=abc-123
Each trace line carries enough context to identify the specific entity — you can grep for user_id=usr_7f3a or email=test@example.com and see its full journey through the layers.
During /implement, the AI must select relevant attributes for each aop.Trace call. This is not optional — bare Trace(ctx, "OpName") calls without attributes are incomplete.
Include (identifying attributes):
user_id, task_id, invoice_id, order_idExclude (noise):
slog.Int("items", len(tasks)))Rule of thumb: if you're debugging a production issue and filtering logs by this operation name, what would you need to see to identify which specific invocation failed? Those are your attributes.
Wire all cross-cutting middleware in order — clock first (so all layers see the pinned time), then logger (so all layers have the enriched logger), then routes:
// cmd/server/main.go
router := gin.Default()
router.Use(middleware.ClockMiddleware())
router.Use(middleware.LoggerMiddleware(baseLogger))
userMod.Handler.RegisterRoutes(router)
taskMod.Handler.RegisterRoutes(router)
WithX(ctx, value) context.Context + FromContext(ctx) XWithLogger or WithTime themselves (except in tests)shared/ — they are the only packages that every module is allowed to importThe project follows a module/layer layout: each domain module is a top-level directory, and each layer within that module is a separate Go package. This gives strong module boundaries (each module is a self-contained subtree) and proper encapsulation between layers (Go package visibility).
internal/
user/ ← module: everything related to users
module.go ← factory: New() wires internals, returns Services struct
model/ ← 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
mocks/ ← generated by mockery
handler/ ← HTTP handlers for this module
user_handler.go ← UserHandler — depends on service interfaces
repository/ ← data layer implementations
postgres_user.go ← unexported userRepository, exported NewUserRepository
task/ ← module: everything related to tasks
module.go ← factory: New() wires internals, returns Services struct
model/
task.go
service/
task_service.go ← TaskService + TaskRepository interface
task_service_test.go
mocks/
handler/
task_handler.go
repository/
postgres_task.go
shared/ ← cross-module infrastructure
clock/ ← testable time: Now(ctx), WithTime(ctx, t)
clock.go
logger/ ← context-propagated slog: FromContext(ctx), WithLogger(ctx, l)
logger.go
aop/ ← request tracing: Trace(ctx, "op")
trace.go
middleware/ ← Gin/gRPC middleware: clock, logger, auth
clock.go
logger.go
auth.go
email/ ← external service implementations
sender.go ← unexported emailSender, exported NewEmailSender
cmd/
server/
main.go ← composition root — imports modules, not layers
The interface-based pattern works the same way — the only difference is the import paths:
// internal/user/service/user_service.go — consumer defines what it needs
package service
import "myapp/internal/user/model"
type UserRepository interface {
FindByID(ctx context.Context, id string) (*model.User, error)
Save(ctx context.Context, user *model.User) error
}
type UserService struct {
repo UserRepository
email EmailSender
}
// internal/user/repository/postgres_user.go — implementation returns consumer's interface
package repository
import (
"myapp/internal/user/model"
"myapp/internal/user/service"
)
type userRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) service.UserRepository {
return &userRepository{db: db}
}
Within a module: layers import inward toward the domain.
handler → service ← repository
↑
model (shared within the module)
Between modules: only through service interfaces, never direct repository or handler imports. If task needs user data, it depends on user/service.UserRepository (the interface), not on user/repository.
The composition root (main.go) only imports module root packages — each module's factory handles its own internal wiring:
// cmd/server/main.go
import (
"myapp/internal/user"
"myapp/internal/task"
"myapp/internal/shared/email"
)
user/model for shared domain types. Keep model packages free of business logicuser/service starts managing billing), extract a new module