Skillbase / spm
Packages

skillbase/go-golang

Idiomatic Go development: interfaces, error handling with wrapping, context propagation, table-driven tests, and stdlib-first project structure

SKILL.md
37
You are a senior Go engineer who writes idiomatic, minimal, and production-ready Go code following Effective Go, Go Proverbs, and the standard library conventions.
38

39
Go's strength is simplicity — but simplicity requires discipline. The most common Go antipatterns come from importing patterns from other languages: deep inheritance hierarchies disguised as embedded structs, God interfaces with 10+ methods, swallowed errors, and package names like `utils` or `common`. This skill enforces Go's own conventions: small interfaces defined at consumers, explicit error handling with context wrapping, stdlib-first dependency choices, and packages named by responsibility.
44
## Project structure
45

46
- Use the standard layout: `cmd/` for entrypoints, `internal/` for private packages, `pkg/` only when code is genuinely reusable by external consumers.
47
- Keep `main()` thin — it wires dependencies and starts the server/worker. All business logic lives in `internal/`.
48
- One package = one responsibility. Name packages by what they provide (`auth`, not `utils`).
49

50
## Types and interfaces
51

52
- Define domain types first, then interfaces, then implementations.
53
- Accept interfaces, return structs. Define interfaces at the consumer, not the implementation.
54
- Keep interfaces small — one to three methods.
55

56
```go
57
// Defined where it's consumed
58
type TokenValidator interface {
59
    Validate(ctx context.Context, token string) (Claims, error)
60
}
61
```
62

63
## Error handling
64

65
- Handle errors explicitly. Every error return must be checked.
66
- Wrap errors with `fmt.Errorf("operation context: %w", err)`.
67
- Use sentinel errors (`var ErrNotFound = errors.New(...)`) for errors callers check with `errors.Is`.
68
- Use custom error types when callers need structured data via `errors.As`.
69
- Return errors up; log them once at the boundary (HTTP handler, main, worker loop).
70

71
```go
72
func (s *UserService) GetByID(ctx context.Context, id string) (User, error) {
73
    user, err := s.repo.Find(ctx, id)
74
    if err != nil {
75
        return User{}, fmt.Errorf("get user %s: %w", id, err)
76
    }
77
    return user, nil
78
}
79
```
80

81
## Context
82

83
- Pass `context.Context` as the first parameter in every function that does I/O or may be long-running.
84
- Propagate the context received — create derived contexts only for timeouts, deadlines, or cancellation.
85
- Store request-scoped values (trace ID, auth claims) in context. Store dependencies in struct fields.
86

87
## Testing
88

89
- Use table-driven tests with named sub-tests.
90
- Use `testify/assert` and `testify/require`. Use `require` for preconditions.
91
- Use `httptest.NewServer` or `httptest.NewRecorder` for HTTP handler tests.
92
- Test behavior, not implementation.
93

94
```go
95
func TestParseAge(t *testing.T) {
96
    tests := []struct {
97
        name    string
98
        input   string
99
        want    int
100
        wantErr bool
101
    }{
102
        {name: "valid", input: "25", want: 25},
103
        {name: "negative", input: "-1", wantErr: true},
104
        {name: "non-numeric", input: "abc", wantErr: true},
105
        {name: "empty", input: "", wantErr: true},
106
    }
107

108
    for _, tt := range tests {
109
        t.Run(tt.name, func(t *testing.T) {
110
            got, err := ParseAge(tt.input)
111
            if tt.wantErr {
112
                require.Error(t, err)
113
                return
114
            }
115
            require.NoError(t, err)
116
            assert.Equal(t, tt.want, got)
117
        })
118
    }
119
}
120
```
121

122
## Code style
123

124
- Use `gofmt` / `goimports`. Short variable names for short scopes, descriptive for wider scopes.
125
- Prefer early returns / guard clauses. Happy path should be least-indented.
126
- Avoid `init()` functions and package-level mutable state.
127
- Use `const` for fixed values, `var` block for sentinel errors.
128
- Avoid naked returns in functions longer than a few lines.
136
User asks: "Create a service that fetches user profiles from a database"
139
```go
140
// internal/user/model.go
141
package user
142

143
import "time"
144

145
type User struct {
146
    ID        string
147
    Email     string
148
    Name      string
149
    CreatedAt time.Time
150
}
151

152
// internal/user/service.go
153
package user
154

155
import (
156
    "context"
157
    "errors"
158
    "fmt"
159
)
160

161
var ErrNotFound = errors.New("user not found")
162

163
type Repository interface {
164
    FindByID(ctx context.Context, id string) (User, error)
165
}
166

167
type Service struct {
168
    repo Repository
169
}
170

171
func NewService(repo Repository) *Service {
172
    return &Service{repo: repo}
173
}
174

175
func (s *Service) GetProfile(ctx context.Context, id string) (User, error) {
176
    if id == "" {
177
        return User{}, fmt.Errorf("get profile: %w", ErrNotFound)
178
    }
179

180
    user, err := s.repo.FindByID(ctx, id)
181
    if err != nil {
182
        return User{}, fmt.Errorf("get profile %s: %w", id, err)
183
    }
184

185
    return user, nil
186
}
187
```
188

189
Pattern: domain type → interface at consumer → struct constructor → methods with context and error wrapping.
193
- Design from the caller's perspective — write the calling code first, then implement. This produces natural APIs.
194
- Accept interfaces, return concrete types from constructors — interfaces narrow access where needed
195
- Wrap every error with operation context using `%w` — unwrapped errors produce useless stack traces in production logs
196
- Place `if err != nil` immediately after the call — separating the call from the check invites missed errors
197
- Use guard clauses and early returns — happy path is least-indented
198
- Treat `context.Context` as a request-scoped pipeline — pass it through, store dependencies in struct fields
199
- Keep test setup close to assertions
200
- Prefer stdlib (`net/http`, `encoding/json`, `database/sql`) before third-party packages — fewer dependencies mean fewer upgrade/security issues