Golang Backend Development
Golang Backend Development Standards
This skill defines the architectural requirements, coding standards, and best practices for the Golang backend. AI agents must adhere to these guidelines to ensure consistency, maintainability, and extensibility.
1. Project Organization
-
See
references/PROJECT_ORGANIZATION.mdfor examples. -
backend/cmd/: Application entry points. Keepmain.goslim; use it for configuration and component initialization. -
backend/internal/: Project-internal code.handler/: HTTP layer. UsesHandlergeneric wrapper. Defers logic toservice.service/: Domain logic layer. Orchestrates business rules and repository calls.repository/: Data layer. Handles raw SQL queries viapgxpool.models/: Data structures (DB models, DTOs, request/response types).
-
backend/pkg/: Shared utility packages (e.g., customstatemachine). -
backend/migrations/: SQL migration scripts.
2. Core Architectural Patterns
Layered Responsibility
- See
references/LAYERED_RESPONSIBILITY.mdfor examples. - Handlers must NOT contain business logic. They decode requests, call services, and encode responses.
- Services are the source of truth for business rules. They must be agnostic to the delivery method (HTTP).
- Repositories bridge the domain models and the database.
Interface Design
- Keep interfaces small with minimal methods to provide maximum flexibility for implementers.
- Return Structs, Accept Interfaces: Functions should return concrete types even if they implement an interface. Parameters should be interfaces to reduce coupling.
Request Validation
- See
references/VALIDATOR_INTERFACE.mdfor examples. - When request parameters require validation, implement a
Validatorinterface pattern. - Use a composable approach (e.g.,
AllPassValidator) to combine multiple validation rules. - Inject the validator into the service to keep validation logic decoupled from business logic.
The Handler Wrapper
- See
references/THE_HANDLER_WRAPPER.mdfor examples. Every HTTP endpoint should be wrapped using theHandler[REQ, RESP]. This ensures consistent: - Request decoding (
DecodeFunc,DefaultDecoder) - Pre-handling: Logic executed before the main handler (e.g., validation)
- Post-handling: Logic executed after the main handler (e.g., logging)
- Response encoding (
EncodeFunc,DefaultEncoder) - Error handling (
DefaultErrorHandler)
Dependency Injection
Dependencies must be injected via constructors:
- See
references/DEPENDENCY_INJECTION.mdfor examples.
3. Coding Standards & Tooling
Standard Library First
- See
references/STANDARD_LIBRARY_FIRST.mdfor examples. - Always prioritize using the Go standard library to implement functionality unless strictly necessary. Keep external dependencies to a minimum.
Context First
- See
references/CONTEXT_USAGE.mdfor examples. - Pass
context.Contextto allserviceandrepositorymethods to support cancellation and timeouts.
Database: Raw SQL with pgx or sqlx
- See
references/DATABASE_ACCESS.mdfor examples. - Prefer raw SQL over ORMs for performance and transparency.
- Use
github.com/jackc/pgx/v5andpgxpool, orgithub.com/jmoiron/sqlx. - Scan results into structs defined in
internal/models.
HTTP Middleware
Apply cross-cutting concerns via middleware:
- See
references/MIDDLEWARE.mdfor examples. LoggingMiddleware: Records full request/response details for debugging.AuthMiddleware: Validates JWT and extractsuserID.CORSMiddleware: Standard CORS headers.
Function Parameter Design
- See
references/FUNCTION_PARAMETER_DESIGN.mdfor examples. - Backward Compatibility: Design function parameters with backward compatibility in mind. Use patterns like Functional Options or wrap multiple parameters into a Request/Options Struct to avoid breaking changes when extending functionality.
- Avoid Parameter Mutation:
- See
references/AVOID_MUTATING_PARAMETERS.mdfor examples. - Avoid modifying input parameters within a function, even if they are pointer types. This reduces side effects and makes call-site behavior more predictable.
- Exception: Mutation is only permissible when justified by severe performance requirements (e.g., high-frequency hot paths, extremely large structs).
- See
Concurrency & Goroutines
- See
references/GOROUTINE_POOLS.mdfor examples. - If dynamic generation of a large number of goroutines is required (e.g., in a heavily called handler), you must use a goroutine pool for management.
- Unbounded goroutine creation can lead to memory leaks. See issue #9869.
- When starting a new task using a goroutine, if the task involves an infinite loop, ensure that the goroutine accepts a
context.Contextparameter. This allows the use ofcontext.Contextto terminate the loop, avoiding memory leaks. - Channel Ownership: The goroutine that writes to a channel should be the one responsible for closing it. This prevents panic scenarios where a channel is closed while being written to.
- Mutex Locking:
- See
references/MUTEX_LOCKING.mdfor examples. - Minimize the duration mutex locks are held.
- When multiple independent resources require locking, use fine-grained locking (one mutex per resource) to avoid unnecessary blocking. Use the generic
Mutex[T]pattern to enforce this.
- See
Design Principles
- See
references/DESIGN_PRINCIPLES.mdfor examples. - KISS (Keep It Simple and Stupid): Prioritize simplicity over complexity. Avoid over-engineering.
- SOLID: Follow SOLID principles to ensure maintainable and scalable code, but always prioritize KISS:
- S - Single Responsibility Principle (SRP): A class or function should have one, and only one, reason to change.
- O - Open/Closed Principle (OCP): Entities should be open for extension, but closed for modification.
- L - Liskov Substitution Principle (LSP): Derived types must be completely substitutable for their base types.
- I - Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they do not use.
- D - Dependency Inversion Principle (DIP): Depend on abstractions, not on concretions.
Testing & Coverage
- See
references/TESTING.mdfor examples. - All service, repository, and package implementations must have test case coverage.
- Mocking: Use the mockery library to generate mock files for interfaces. This facilitates consistent and easy unit testing.
- Prioritize testability in all implementations.
4. Code Quality & Linting
- Configuration: If
.golangci.ymldoes not exist in the project root, create one using the reference configuration from golangci-lint reference. - Enforcement: Run
golangci-lint run ./...on the entire project after making changes or fixing issues to ensure code quality and consistency.
4. Error Handling & Return Values
- See
references/ERROR_HANDLING.mdfor examples. - Handle All Errors: Ensure all errors are properly handled. Ignoring errors or only checking them without action can lead to silent failures.
- Return clear, typed errors from services (e.g.,
ErrUnauthorized). - Ensure all JSON-serialized fields have appropriate
jsontags.
Must-Prefix Functions & Panic Safety
- See
references/MUST_FUNCTIONS.mdfor examples. - Must-prefix functions (e.g.,
template.Must,regexp.MustCompile) and helper functions likefunc Must[T any](t T, err error) Tthat panic on error must only be used in safe contexts:- Initialization in
main.go: Where immediate failure is acceptable and expected (e.g., loading critical configuration, compiling static regexes) - Guaranteed safe inputs: Where you can manually verify that the input will never cause an error (e.g., hardcoded valid regex patterns, compile-time constants)
- Initialization in
- Never use Must-functions with:
- User input or external data
- Runtime-generated values that could be invalid
- Any operation in request handlers, services, or repositories where recovery is possible
- Rationale: Panics in production services cause crashes and service disruption. Proper error handling allows graceful degradation and better observability.
Constructor Error Handling
- See
references/CONSTRUCTOR_ERROR_HANDLING.mdfor examples. - If a constructor executes logic that returns an error (e.g., parsing config, opening DB), the constructor must return
(*Type, error)instead of panicking.
5. Extensibility Goal
- See
references/EXTENSIBILITY.mdfor examples. - When implementing new features, consider how side effects (notifications, logs, state transitions) can be hooked into the existing flow without tightly coupling the core business logic.
6. Performance Optimization
- See
references/PERFORMANCE_OPTIMIZATION.mdfor examples. - Object Pooling: For objects that are frequently allocated and deallocated, use
sync.Poolto reduce GC pressure. - Stack Allocation: Prefer returning concrete objects (values) rather than pointers when the object is small or where stack allocation is possible, reducing the burden on the heap and GC.
- Hot Paths: Only apply aggressive optimizations in verified hot paths where performance issues are significant.
Summary of Key Patterns
✅ Always Do:
- Use constructor-based dependency injection
- Pass
context.Contextas the first parameter - Use raw SQL with pgx/sqlx instead of ORMs
- Return concrete types, accept interfaces
- Use
uuid.UUIDfor identifiers - Define typed errors as package variables
- Add proper
jsontags to all models - Write tests with mockery-generated mocks
- Use functional options or request structs for extensibility
- Avoid parameter mutation (unless strictly required for performance)
- Apply middleware for cross-cutting concerns
- Keep handlers thin - defer to services
- Use event-driven or hook patterns for extensibility
- Use goroutine pools for high-concurrency dynamic tasks
- Limit Must-functions to main.go initialization or guaranteed-safe inputs
- Ensure infinite loop goroutines are cancellable via context
- Handle all errors explicitly; do not ignore them
- Close channels from the writer side
- Use composable Validator interface for request validation
❌ Never Do:
- Put business logic in handlers or main.go
- Use global variables for dependencies
- Forget context in service/repository methods
- Use ORMs instead of raw SQL
- Use string IDs instead of UUIDs
- Return generic string errors
- Expose sensitive fields (like passwords) in JSON
- Write untestable code with tight coupling
- Add too many positional parameters
- Mutate input parameters (pointers) within functions without performance justification
- Hardcode side effects in core business logic
- Over-engineer simple solutions
- Add unnecessary external dependencies
- Create unbounded goroutines in hot paths
- Use Must-prefix functions or panic helpers with user input or in request paths
- Run infinite loop goroutines without a cancellation mechanism
- Ignore errors or use
_to suppress them - Close channels from the receiver side
Remember: Follow KISS principle first, then apply SOLID where it adds clear value. Prioritize testability, maintainability, and extensibility in all implementations.