go-patterns
Go backend patterns including chi router, gRPC, pgx, context handling, and concurrency.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Go Backend Patterns
Production Go patterns for backend services with focus on concurrency, error handling, and idiomatic Go.
Core Stack
| Component | Library | Purpose |
|---|---|---|
| Language | Go 1.22+ | Backend development |
| Build | go build, go test | Compilation and testing |
| Linting | golangci-lint | Code quality |
| HTTP | chi router | RESTful APIs |
| RPC | grpc-go | Service communication |
| Database | pgx, sqlc | PostgreSQL |
| Cache | redis-go | Caching |
| Testing | testify, gomock | Test framework and mocking |
| Observability | OpenTelemetry | Tracing and metrics |
Context7 Library IDs
Query these libraries for current best practices:
-
Chi Router:
/go-chi/chi -
pgx:
/jackc/pgx -
sqlc:
/sqlc-dev/sqlc -
testify:
/stretchr/testify -
OpenTelemetry Go:
/open-telemetry/opentelemetry-go
Execution Rules
- golangci-lint always. Run linting before commits
- No naked returns. Always name return values in complex functions
- Error handling. Wrap errors with context, don't discard them
- Documentation. GoDoc comments on all exported items
-
Tests. Table-driven tests in
_test.gofiles
Error Handling
Wrapping Errors with Context
import (
"fmt"
"errors"
)
func GetUser(id string) (*User, error) {
user, err := db.FindUser(id)
if err != nil {
return nil, fmt.Errorf("get user %s: %w", id, err)
}
return user, nil
}
Checking Specific Errors
if errors.Is(err, ErrNotFound) {
return nil, status.Error(codes.NotFound, "user not found")
}
Custom Error Types
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}
Context Usage
Timeouts and Cancellation
func FetchData(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("execute request: %w", err)
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
Context Values (Use Sparingly)
type contextKey string
const userIDKey contextKey = "userID"
func WithUserID(ctx context.Context, userID string) context.Context {
return context.WithValue(ctx, userIDKey, userID)
}
func UserIDFromContext(ctx context.Context) (string, bool) {
userID, ok := ctx.Value(userIDKey).(string)
return userID, ok
}
Concurrency Patterns
Graceful Goroutine Management
func worker(ctx context.Context, jobs <-chan Job) {
for {
select {
case <-ctx.Done():
return
case job, ok := <-jobs:
if !ok {
return
}
process(job)
}
}
}
Worker Pool
func StartWorkerPool(ctx context.Context, jobs <-chan Job, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
worker(ctx, jobs)
}()
}
wg.Wait()
}
Mutex for Shared State
type SafeCounter struct {
mu sync.RWMutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.count
}
Structured Logging
import (
"log/slog"
"context"
)
func ProcessRequest(ctx context.Context, id string) {
logger := slog.With("request_id", id)
logger.InfoContext(ctx, "processing request")
// On error
logger.ErrorContext(ctx, "failed to process",
"error", err,
"user_id", userID,
)
}
Chi Router Patterns
import "github.com/go-chi/chi/v5"
func NewRouter() chi.Router {
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))
r.Route("/api/v1", func(r chi.Router) {
r.Get("/users/{id}", GetUser)
r.Post("/users", CreateUser)
})
return r
}
Table-Driven Tests
func TestGetUser(t *testing.T) {
tests := []struct {
name string
userID string
want *User
wantErr bool
}{
{
name: "valid user",
userID: "123",
want: &User{ID: "123", Name: "Test"},
},
{
name: "invalid user",
userID: "999",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetUser(tt.userID)
if (err != nil) != tt.wantErr {
t.Errorf("GetUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetUser() = %v, want %v", got, tt.want)
}
})
}
}
Validation Commands
go fmt ./...
go vet ./...
golangci-lint run ./...
go test ./... -race -v
go build ./...
Guidelines
- Keep functions small and focused
- Use interfaces for abstraction
- Handle errors explicitly, don't panic
- Prefer composition over inheritance
- Document exported functions and types
- Use context for cancellation and timeouts
- Leverage goroutines appropriately (not excessively)
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-03-03
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!