go-best-practices
npx machina-cli add skill aiskillstore/marketplace/go-best-practices --openclawGo Best Practices
Type-First Development
Types define the contract before implementation. Follow this workflow:
- Define data structures - structs and interfaces first
- Define function signatures - parameters, return types, and error conditions
- Implement to satisfy types - let the compiler guide completeness
- Validate at boundaries - check inputs where data enters the system
Make Illegal States Unrepresentable
Use Go's type system to prevent invalid states at compile time.
Structs for domain models:
// Define the data model first
type User struct {
ID UserID
Email string
Name string
CreatedAt time.Time
}
type CreateUserRequest struct {
Email string
Name string
}
// Functions follow from the types
func CreateUser(req CreateUserRequest) (*User, error) {
// implementation
}
Custom types for domain primitives:
// Distinct types prevent mixing up IDs
type UserID string
type OrderID string
func GetUser(id UserID) (*User, error) {
// Compiler prevents passing OrderID here
}
func NewUserID(raw string) UserID {
return UserID(raw)
}
// Methods attach behavior to the type
func (id UserID) String() string {
return string(id)
}
Interfaces for behavior contracts:
// Define what you need, not what you have
type Reader interface {
Read(p []byte) (n int, err error)
}
type UserRepository interface {
GetByID(ctx context.Context, id UserID) (*User, error)
Save(ctx context.Context, user *User) error
}
// Accept interfaces, return structs
func ProcessInput(r Reader) ([]byte, error) {
return io.ReadAll(r)
}
Enums with iota:
type Status int
const (
StatusActive Status = iota + 1
StatusInactive
StatusPending
)
func (s Status) String() string {
switch s {
case StatusActive:
return "active"
case StatusInactive:
return "inactive"
case StatusPending:
return "pending"
default:
return fmt.Sprintf("Status(%d)", s)
}
}
// Exhaustive handling in switch
func ProcessStatus(s Status) (string, error) {
switch s {
case StatusActive:
return "processing", nil
case StatusInactive:
return "skipped", nil
case StatusPending:
return "waiting", nil
default:
return "", fmt.Errorf("unhandled status: %v", s)
}
}
Functional options for flexible construction:
type ServerOption func(*Server)
func WithPort(port int) ServerOption {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(d time.Duration) ServerOption {
return func(s *Server) {
s.timeout = d
}
}
func NewServer(opts ...ServerOption) *Server {
s := &Server{
port: 8080, // sensible defaults
timeout: 30 * time.Second,
}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage: NewServer(WithPort(3000), WithTimeout(time.Minute))
Embed for composition:
type Timestamps struct {
CreatedAt time.Time
UpdatedAt time.Time
}
type User struct {
Timestamps // embedded - User has CreatedAt, UpdatedAt
ID UserID
Email string
}
Module Structure
Prefer smaller files within packages: one type or concern per file. Split when a file handles multiple unrelated types or exceeds ~300 lines. Keep tests in _test.go files alongside implementation. Package boundaries define the API; internal organization is flexible.
Functional Patterns
- Use value receivers when methods don't mutate state; reserve pointer receivers for mutation.
- Avoid package-level mutable variables; pass dependencies explicitly via function parameters.
- Return new structs/slices rather than mutating inputs; makes data flow explicit.
- Use closures and higher-order functions where they simplify code (e.g.,
sort.Slice, iterators).
Instructions
- Return errors with context using
fmt.Errorfand%wfor wrapping. This preserves the error chain for debugging. - Every function returns a value or an error; unimplemented paths return descriptive errors. Explicit failures are debuggable.
- Handle all branches in
switchstatements; include adefaultcase that returns an error. Exhaustive handling prevents silent bugs. - Pass
context.Contextto external calls with explicit timeouts. Runaway requests cause cascading failures. - Reserve
panicfor truly unrecoverable situations; prefer returning errors. Panics crash the program. - Add or update table-driven tests for new logic; cover edge cases (empty input, nil, boundaries).
Examples
Explicit failure for unimplemented logic:
func buildWidget(widgetType string) (*Widget, error) {
return nil, fmt.Errorf("buildWidget not implemented for type: %s", widgetType)
}
Wrap errors with context to preserve the chain:
out, err := client.Do(ctx, req)
if err != nil {
return nil, fmt.Errorf("fetch widget failed: %w", err)
}
return out, nil
Exhaustive switch with default error:
func processStatus(status string) (string, error) {
switch status {
case "active":
return "processing", nil
case "inactive":
return "skipped", nil
default:
return "", fmt.Errorf("unhandled status: %s", status)
}
}
Structured logging with slog:
import "log/slog"
var log = slog.With("component", "widgets")
func createWidget(name string) (*Widget, error) {
log.Debug("creating widget", "name", name)
widget := &Widget{Name: name}
log.Debug("created widget", "id", widget.ID)
return widget, nil
}
Configuration
- Load config from environment variables at startup; validate required values before use. Missing config should cause immediate exit.
- Define a Config struct as single source of truth; avoid
os.Getenvscattered throughout code. - Use sensible defaults for development; require explicit values for production secrets.
Examples
Typed config struct:
type Config struct {
Port int
DatabaseURL string
APIKey string
Env string
}
func LoadConfig() (*Config, error) {
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
return nil, fmt.Errorf("DATABASE_URL is required")
}
apiKey := os.Getenv("API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("API_KEY is required")
}
port := 3000
if p := os.Getenv("PORT"); p != "" {
var err error
port, err = strconv.Atoi(p)
if err != nil {
return nil, fmt.Errorf("invalid PORT: %w", err)
}
}
return &Config{
Port: port,
DatabaseURL: dbURL,
APIKey: apiKey,
Env: getEnvOrDefault("ENV", "development"),
}, nil
}
Source
git clone https://github.com/aiskillstore/marketplace/blob/main/skills/0xbigboss/go-best-practices/SKILL.mdView on GitHub Overview
Type-first development puts structs and interfaces at the core, then builds behavior to satisfy them. It emphasizes compile-time safety with custom domain types, enums, interfaces, and clear constructors. This approach is essential when reading or writing Go files to ensure robust, predictable patterns.
How This Skill Works
Start by defining data structures and function signatures, then implement to satisfy those types so the compiler enforces correctness. Use custom types for domain primitives (UserID, OrderID) and interfaces to express behavior contracts; leverage functional options to compose flexible constructors and avoid mutable globals. Embed for composition to share common fields and enable compile-time guarantees.
When to Use It
- When modeling domain data before behavior (structs, IDs, and interfaces).
- When you need explicit type safety to prevent mixing identifiers (custom IDs).
- When constructing configurable objects (functional options).
- When enforcing exhaustive handling of states (enums with iota).
- When organizing code across packages with small, focused files and tests.
Quick Start
- Step 1: Define domain structs and IDs (e.g., User, UserID).
- Step 2: Add interfaces and a constructor using functional options.
- Step 3: Compile and iterate, verifying compile-time guarantees.
Best Practices
- Define data structures and function signatures before implementing logic.
- Use custom domain types (e.g., UserID string) to prevent mix-ups.
- Prefer interfaces for behavior contracts and accept interfaces, not concrete types.
- Use functional options for flexible, explicit construction.
- Prefer embedding for composition and let the compiler enforce boundaries.
Example Use Cases
- Type User with ID, Email, Name, and CreatedAt to model domain data.
- Seasoned domain types: UserID and OrderID with String methods.
- Interfaces like Reader and UserRepository to decouple I/O and storage.
- Enums with iota to represent Status and an exhaustive ProcessStatus.
- Server construction via NewServer with WithPort and WithTimeout options.