go-expert
Write idiomatic, production-quality Go code. Use when building Go APIs, CLIs, microservices, or systems code. Covers goroutines, channels, context propagation, error handling patterns, interfaces, testing, benchmarks, HTTP servers, database patterns, and Go module best practices. Expert-level Go idioms that senior engineers expect.
Go Production Expert
Go Philosophy
Go is explicit, not clever. Idiomatic Go is:
- Simple over clever — readable in 6 months
- Errors as values — handle them explicitly, every time
- Composition over inheritance — small interfaces, embedding
- Concurrency via CSP — communicate by sharing memory? No. Share memory by communicating.
Project Structure
myapp/
├── cmd/
│ ├── server/
│ │ └── main.go # Entry point — thin, delegates to internal
│ └── worker/
│ └── main.go
├── internal/ # Private code — not importable by external packages
│ ├── api/
│ │ ├── handler.go
│ │ ├── handler_test.go
│ │ └── middleware.go
│ ├── domain/
│ │ ├── user.go
│ │ └── user_test.go
│ ├── repository/
│ │ ├── postgres.go
│ │ └── postgres_test.go
│ └── service/
│ └── user_service.go
├── pkg/ # Public packages (reusable by external packages)
├── migrations/
├── go.mod
├── go.sum
└── Makefile
Error Handling
Rule 1: Always return errors, never ignore them.
Rule 2: Wrap errors with context at each layer.
Rule 3: Check errors.Is and errors.As, not string comparison.
// Define domain errors
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s with id %s not found", e.Resource, e.ID)
}
// Use sentinel errors for expected cases
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrConflict = errors.New("conflict")
)
// Wrap with context — %w makes it unwrappable
func (r *UserRepo) GetByID(ctx context.Context, id string) (*User, error) {
var u User
err := r.db.QueryRowContext(ctx, "SELECT * FROM users WHERE id=$1", id).Scan(&u)
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
}
if err != nil {
return nil, fmt.Errorf("query user %s: %w", id, err)
}
return &u, nil
}
// Check wrapped errors
user, err := repo.GetByID(ctx, userID)
if errors.Is(err, ErrNotFound) {
http.Error(w, "user not found", http.StatusNotFound)
return
}
if err != nil {
log.Error("failed to get user", "error", err, "user_id", userID)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
Interfaces (Small is Powerful)
// Define interfaces at the point of USE, not definition
// Small interfaces > large interfaces
// io.Reader is the gold standard — 1 method
type Reader interface {
Read(p []byte) (n int, err error)
}
// Your service interfaces
type UserStore interface {
GetByID(ctx context.Context, id string) (*User, error)
Create(ctx context.Context, u *User) error
Update(ctx context.Context, u *User) error
Delete(ctx context.Context, id string) error
}
// Compose for flexibility
type UserReader interface {
GetByID(ctx context.Context, id string) (*User, error)
}
type UserWriter interface {
Create(ctx context.Context, u *User) error
Update(ctx context.Context, u *User) error
}
// Read-only service only needs UserReader
type ReadService struct {
store UserReader // NOT UserStore
}
// Test with a mock — no mocking library needed
type mockUserStore struct {
users map[string]*User
}
func (m *mockUserStore) GetByID(_ context.Context, id string) (*User, error) {
u, ok := m.users[id]
if !ok {
return nil, ErrNotFound
}
return u, nil
}
// Compile-time interface check
var _ UserStore = (*PostgresUserStore)(nil)
Context and Cancellation
// Always accept ctx as first parameter
func (s *Service) ProcessOrder(ctx context.Context, orderID string) error {
// Propagate cancellation to all operations
user, err := s.userStore.GetByID(ctx, orderID)
if err != nil {
return err
}
// Check for cancellation explicitly in long operations
select {
case <-ctx.Done():
return ctx.Err()
default:
}
return s.doExpensiveWork(ctx, user)
}
// Set deadlines — always
func handler(w http.ResponseWriter, r *http.Request) {
// Add timeout to request context
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel() // ALWAYS defer cancel to prevent context leak
result, err := service.Process(ctx, r.FormValue("id"))
// ...
}
// Context values — only for request-scoped data
type contextKey string
const (
RequestIDKey contextKey = "request_id"
UserIDKey contextKey = "user_id"
)
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, RequestIDKey, id)
}
func RequestIDFrom(ctx context.Context) string {
id, _ := ctx.Value(RequestIDKey).(string)
return id
}
Goroutines and Channels
// Goroutine lifecycle management with WaitGroup + errgroup
import "golang.org/x/sync/errgroup"
func ProcessBatch(ctx context.Context, items []Item) error {
g, ctx := errgroup.WithContext(ctx)
// Limit concurrency
sem := make(chan struct{}, 10) // 10 concurrent max
for _, item := range items {
item := item // Capture loop variable (pre-Go 1.22)
g.Go(func() error {
sem <- struct{}{}
defer func() { <-sem }()
return processItem(ctx, item)
})
}
return g.Wait() // Returns first error, cancels context
}
// Fan-out/Fan-in pattern
func fanOut(ctx context.Context, input <-chan Work) []<-chan Result {
numWorkers := 5
channels := make([]<-chan Result, numWorkers)
for i := range channels {
channels[i] = worker(ctx, input)
}
return channels
}
func fanIn(ctx context.Context, channels ...<-chan Result) <-chan Result {
var wg sync.WaitGroup
merged := make(chan Result)
output := func(c <-chan Result) {
defer wg.Done()
for r := range c {
select {
case merged <- r:
case <-ctx.Done():
return
}
}
}
wg.Add(len(channels))
for _, c := range channels {
go output(c)
}
go func() {
wg.Wait()
close(merged)
}()
return merged
}
// Channel direction typing (reduces bugs)
func producer(ch chan<- int) { // Send only
ch <- 42
}
func consumer(ch <-chan int) { // Receive only
v := <-ch
fmt.Println(v)
}
HTTP Server
package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
mux := http.NewServeMux()
// Go 1.22+ pattern matching
mux.HandleFunc("GET /api/users/{id}", handleGetUser)
mux.HandleFunc("POST /api/users", handleCreateUser)
mux.HandleFunc("GET /healthz", handleHealth)
// Wrap with middleware chain
handler := chain(
mux,
requestIDMiddleware,
loggingMiddleware(logger),
recoveryMiddleware,
)
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
ErrorLog: slog.NewLogLogger(logger.Handler(), slog.LevelError),
}
// Graceful shutdown
go func() {
logger.Info("starting server", "addr", server.Addr)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
logger.Error("server error", "error", err)
os.Exit(1)
}
}()
// Wait for shutdown signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Info("shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.Error("shutdown error", "error", err)
}
}
// Middleware chain helper
func chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
// Request ID middleware
func requestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = uuid.New().String()
}
ctx := WithRequestID(r.Context(), id)
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Recovery middleware
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
slog.Error("panic", "recover", rec, "stack", debug.Stack())
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
Database Patterns (pgx)
import (
"github.com/jackc/pgx/v5/pgxpool"
"github.com/jackc/pgx/v5"
)
type DB struct {
pool *pgxpool.Pool
}
func NewDB(ctx context.Context, dsn string) (*DB, error) {
config, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, fmt.Errorf("parse db config: %w", err)
}
config.MaxConns = 25
config.MinConns = 5
config.MaxConnLifetime = time.Hour
config.MaxConnIdleTime = 30 * time.Minute
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
return nil, fmt.Errorf("create pool: %w", err)
}
if err := pool.Ping(ctx); err != nil {
return nil, fmt.Errorf("ping db: %w", err)
}
return &DB{pool: pool}, nil
}
// Transaction helper
func (db *DB) WithTx(ctx context.Context, fn func(pgx.Tx) error) error {
tx, err := db.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
if err := fn(tx); err != nil {
_ = tx.Rollback(ctx)
return err
}
return tx.Commit(ctx)
}
// Usage
err = db.WithTx(ctx, func(tx pgx.Tx) error {
_, err := tx.Exec(ctx, "UPDATE accounts SET balance=$1 WHERE id=$2", newBalance, accountID)
if err != nil {
return fmt.Errorf("update balance: %w", err)
}
_, err = tx.Exec(ctx, "INSERT INTO transactions (...) VALUES (...)", ...)
return err
})
Testing
// table-driven tests
func TestGetUser(t *testing.T) {
tests := []struct{
name string
userID string
want *User
wantErr error
}{
{
name: "existing user",
userID: "user-1",
want: &User{ID: "user-1", Name: "Alice"},
},
{
name: "missing user",
userID: "missing",
wantErr: ErrNotFound,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// t.Parallel() // Add for independent tests
store := &mockUserStore{
users: map[string]*User{"user-1": {ID: "user-1", Name: "Alice"}},
}
svc := NewUserService(store)
got, err := svc.GetUser(context.Background(), tt.userID)
if !errors.Is(err, tt.wantErr) {
t.Errorf("GetUser() error = %v, wantErr %v", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetUser() = %v, want %v", got, tt.want)
}
})
}
}
// Benchmark
func BenchmarkProcessOrder(b *testing.B) {
svc := newTestService()
order := testOrder()
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = svc.ProcessOrder(context.Background(), order)
}
}
// Integration test with testcontainers
func TestPostgresRepo(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := context.Background()
container, err := postgres.RunContainer(ctx,
testcontainers.WithImage("postgres:16"),
postgres.WithDatabase("testdb"),
postgres.WithUsername("test"),
postgres.WithPassword("test"),
testcontainers.WithWaitStrategy(
wait.ForLog("database system is ready to accept connections"),
),
)
require.NoError(t, err)
defer container.Terminate(ctx)
dsn, _ := container.ConnectionString(ctx)
db, err := NewDB(ctx, dsn)
require.NoError(t, err)
// Run migrations
require.NoError(t, runMigrations(ctx, db))
repo := NewUserRepo(db)
// test against real database...
}
slog Structured Logging (Go 1.21+)
import "log/slog"
// Setup once
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
AddSource: true,
}))
slog.SetDefault(logger)
// Use anywhere
slog.Info("user created", "user_id", userID, "email", email)
slog.Error("failed to process", "error", err, "order_id", orderID)
// With context
slog.InfoContext(ctx, "request processed",
"request_id", RequestIDFrom(ctx),
"duration_ms", time.Since(start).Milliseconds(),
"status", statusCode,
)Skill Information
- Source
- MoltbotDen
- Category
- Coding Agents & IDEs
- Repository
- View on GitHub
Related Skills
system-design-architect
Design scalable, reliable distributed systems. Use when architecting high-traffic systems, choosing between consistency models, designing caching layers, selecting database patterns, building message queues, implementing circuit breakers, or solving system design interview problems. Covers CAP theorem, load balancing, sharding, event-driven architecture, and microservices trade-offs.
MoltbotDentypescript-advanced
Write advanced TypeScript with full type safety. Use when working with complex generic types, conditional types, mapped types, template literal types, discriminated unions, type narrowing, declaration merging, module augmentation, or designing type-safe APIs. Covers TypeScript 5.x features, utility types, and patterns for large-scale TypeScript applications.
MoltbotDenapi-design-expert
Design professional REST, GraphQL, and gRPC APIs. Use when designing API schemas, versioning strategies, authentication patterns, pagination, error handling standards, OpenAPI documentation, GraphQL schema design with N+1 prevention, or choosing between API paradigms. Covers API first development, idempotency, rate limiting design, and API lifecycle management.
MoltbotDenrust-systems
Write safe, performant Rust systems code. Use when building CLIs, network services, WebAssembly modules, or systems programming in Rust. Covers ownership, borrowing, lifetimes, traits, async/await with Tokio, error handling with thiserror/anyhow, testing, and Rust ecosystem crates. Idiomatic Rust patterns that pass code review.
MoltbotDengraphql-expert
Design and implement production GraphQL APIs. Use when designing GraphQL schemas, implementing resolvers, solving N+1 problems with DataLoader, implementing subscriptions, building GraphQL federation, generating types from schemas, or optimizing GraphQL performance. Covers Apollo Server, GraphQL Yoga, Pothos schema builder, and persisted queries.
MoltbotDen