go-development
Develop, test, and maintain idiomatic Go applications following official Go conventions. Use when writing Go packages and modules, implementing explicit error handling with wrapping, designing small c
SKILL.mdskills/go-development/SKILL.md
GitHubIdiomatic Go Development
Go emphasizes simplicity, readability, explicit error handling, and lightweight concurrency. Idiomatic Go code adheres to the official Effective Go guide, avoids over-engineering, defines small consumer-driven interfaces, and propagates context throughout all I/O boundaries.
Core Rules
- Errors are values, not exceptions: Always check errors explicitly (
if err != nil). Never ignore returned errors with_unless explicitly intended and documented. - Accept interfaces, return structs: Functions should accept the smallest interface required to do their work, and return concrete struct pointers or values.
- Consumers define interfaces: Define interfaces in the package where they are consumed, not where the implementing struct is declared.
- Pass
context.Contextas the first parameter: Every function performing network, database, or long-running operations must acceptctx context.Contextas its first argument and respect cancellation. - Never leak goroutines: Every spawned goroutine must have a deterministic termination condition or cancellation path via
ctx.Done(). - Zero value must be useful: Design structs such that their default uninitialized state is ready for use (e.g.
sync.Mutex,bytes.Buffer).
1. Project and Package Structure
Standard Go repository layout:
my-service/
├── go.mod
├── go.sum
├── cmd/
│ └── server/
│ └── main.go # Entry point for the executable
├── internal/ # Code private to this module (cannot be imported by external repos)
│ ├── auth/
│ │ ├── auth.go
│ │ └── auth_test.go
│ └── storage/
│ └── store.go
└── pkg/ # Public library code safe for external consumption
└── client/
└── client.go
2. Explicit Error Handling and Wrapping
Wrap errors with context using %w and inspect them using errors.Is and errors.As:
package store
import (
"database/sql"
"errors"
"fmt"
)
var ErrUserNotFound = errors.New("user not found")
type Store struct {
db *sql.DB
}
func (s *Store) GetUserEmail(id int64) (string, error) {
var email string
query := "SELECT email FROM users WHERE id = $1"
err := s.db.QueryRow(query, id).Scan(&email)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", fmt.Errorf("lookup user %d: %w", id, ErrUserNotFound)
}
return "", fmt.Errorf("query user %d: %w", id, err)
}
return email, nil
}
Caller inspection:
email, err := s.GetUserEmail(42)
if err != nil {
if errors.Is(err, store.ErrUserNotFound) {
// Handle 404
} else {
// Handle 500 internal server error
}
}
3. Concurrency with Goroutines, Channels, and Context
Always bind worker lifecycles to context.Context to prevent goroutine leaks:
package worker
import (
"context"
"time"
)
func RunPeriodicSync(ctx context.Context, interval time.Duration) error {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
// Context was canceled or timed out; clean up and return
return ctx.Err()
case t := <-ticker.C:
if err := performSync(ctx, t); err != nil {
// Log error and continue, or abort if fatal
}
}
}
}
4. Consumer-Defined Interfaces
Do not create an interface in package database with 30 methods. Let callers declare what they need:
package service
import "context"
// The service only requires these two methods, regardless of database backend
type UserRepository interface {
FindUser(ctx context.Context, id string) (*User, error)
SaveUser(ctx context.Context, user *User) error
}
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
5. Common Mistakes and Anti-Patterns
| Anti-Pattern | Bug / Risk | Correct Practice |
|---|---|---|
go func() { ... }() without exit signal |
Goroutine leak; leaks memory and CPU indefinitely | Pass ctx and listen to <-ctx.Done() |
Ignoring returned errors: res, _ := fetch() |
Silent failures in production | Always handle error: if err != nil { return err } |
| Exporting concrete types with public interfaces | Tight coupling across modules | Return structs; let callers define small interfaces |
Mutex copy bug: passing sync.Mutex by value |
Copies lock state, causing deadlocks or race conditions | Always pass structs containing mutexes by pointer (*MyStruct) |
| Unbuffered channels causing deadlock | Goroutine hangs waiting for receiver that never arrives | Use appropriate buffering or guarantee concurrent reader |
6. Verification and Tooling
# Format code following official standard
gofmt -s -w .
# Run static analysis
go vet ./...
golangci-lint run
# Run tests with race condition detector
go test -race -v -cover ./...
# Verify dependencies in go.mod and go.sum
go mod tidy
git diff --exit-code go.mod go.sum
7. Verification Checklist
- All public functions, structs, and methods have godoc comments starting with the symbol name.
- No unhandled errors (
if err != nil). -
go test -race ./...runs without race condition warnings. - Goroutines have clear termination paths via
context.Context. -
go mod tidyleavesgo.modandgo.sumclean and synchronized.