Skill v1.0.1
currentAutomated scan100/100+4 new
version: "1.0.1" name: golang-patterns description: Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications. Use when writing or reviewing Go code and idiomatic structure or conventions are in question. metadata: origin: ECC
Go Development Patterns
Idiomatic Go patterns and best practices for building robust, efficient, and maintainable applications.
When to Activate
- Writing new Go code
- Reviewing Go code
- Refactoring existing Go code
- Designing Go packages/modules
Core Principles
1. Simplicity and Clarity
Go favors simplicity over cleverness. Code should be obvious and easy to read.
// Good: Clear and directfunc 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}// Bad: Overly cleverfunc GetUser(id string) (*User, error) {return func() (*User, error) {if u, e := db.FindUser(id); e == nil {return u, nil} else {return nil, e}}()}
2. Make the Zero Value Useful
Design types so their zero value is immediately usable without initialization.
// Good: Zero value is usefultype Counter struct {mu sync.Mutexcount int // zero value is 0, ready to use}func (c *Counter) Inc() {c.mu.Lock()c.count++c.mu.Unlock()}// Good: bytes.Buffer works with zero valuevar buf bytes.Bufferbuf.WriteString("hello")// Bad: Requires initializationtype BadCounter struct {counts map[string]int // nil map will panic}
3. Accept Interfaces, Return Structs
Functions should accept interface parameters and return concrete types.
// Good: Accepts interface, returns concrete typefunc ProcessData(r io.Reader) (*Result, error) {data, err := io.ReadAll(r)if err != nil {return nil, err}return &Result{Data: data}, nil}// Bad: Returns interface (hides implementation details unnecessarily)func ProcessData(r io.Reader) (io.Reader, error) {// ...}
Error Handling Patterns
Error Wrapping with Context
// Good: Wrap errors with contextfunc LoadConfig(path string) (*Config, error) {data, err := os.ReadFile(path)if err != nil {return nil, fmt.Errorf("load config %s: %w", path, err)}var cfg Configif err := json.Unmarshal(data, &cfg); err != nil {return nil, fmt.Errorf("parse config %s: %w", path, err)}return &cfg, nil}
Custom Error Types
// Define domain-specific errorstype ValidationError struct {Field stringMessage string}func (e *ValidationError) Error() string {return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)}// Sentinel errors for common casesvar (ErrNotFound = errors.New("resource not found")ErrUnauthorized = errors.New("unauthorized")ErrInvalidInput = errors.New("invalid input"))
Error Checking with errors.Is and errors.As
func HandleError(err error) {// Check for specific errorif errors.Is(err, sql.ErrNoRows) {log.Println("No records found")return}// Check for error typevar validationErr *ValidationErrorif errors.As(err, &validationErr) {log.Printf("Validation error on field %s: %s",validationErr.Field, validationErr.Message)return}// Unknown errorlog.Printf("Unexpected error: %v", err)}
Never Ignore Errors
// Bad: Ignoring error with blank identifierresult, _ := doSomething()// Good: Handle or explicitly document why it's safe to ignoreresult, err := doSomething()if err != nil {return err}// Acceptable: When error truly doesn't matter (rare)_ = writer.Close() // Best-effort cleanup, error logged elsewhere
Concurrency Patterns
Worker Pool
func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {var wg sync.WaitGroupfor i := 0; i < numWorkers; i++ {wg.Add(1)go func() {defer wg.Done()for job := range jobs {results <- process(job)}}()}wg.Wait()close(results)}
Context for Cancellation and Timeouts
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {ctx, cancel := context.WithTimeout(ctx, 5*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("fetch %s: %w", url, err)}defer resp.Body.Close()return io.ReadAll(resp.Body)}
Graceful Shutdown
func GracefulShutdown(server *http.Server) {quit := make(chan os.Signal, 1)signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)<-quitlog.Println("Shutting down server...")ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)defer cancel()if err := server.Shutdown(ctx); err != nil {log.Fatalf("Server forced to shutdown: %v", err)}log.Println("Server exited")}
errgroup for Coordinated Goroutines
import "golang.org/x/sync/errgroup"func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {g, ctx := errgroup.WithContext(ctx)results := make([][]byte, len(urls))for i, url := range urls {i, url := i, url // Capture loop variablesg.Go(func() error {data, err := FetchWithTimeout(ctx, url)if err != nil {return err}results[i] = datareturn nil})}if err := g.Wait(); err != nil {return nil, err}return results, nil}
Avoiding Goroutine Leaks
// Bad: Goroutine leak if context is cancelledfunc leakyFetch(ctx context.Context, url string) <-chan []byte {ch := make(chan []byte)go func() {data, _ := fetch(url)ch <- data // Blocks forever if no receiver}()return ch}// Good: Properly handles cancellationfunc safeFetch(ctx context.Context, url string) <-chan []byte {ch := make(chan []byte, 1) // Buffered channelgo func() {data, err := fetch(url)if err != nil {return}select {case ch <- data:case <-ctx.Done():}}()return ch}
Interface Design
Small, Focused Interfaces
// Good: Single-method interfacestype Reader interface {Read(p []byte) (n int, err error)}type Writer interface {Write(p []byte) (n int, err error)}type Closer interface {Close() error}// Compose interfaces as neededtype ReadWriteCloser interface {ReaderWriterCloser}
Define Interfaces Where They're Used
// In the consumer package, not the providerpackage service// UserStore defines what this service needstype UserStore interface {GetUser(id string) (*User, error)SaveUser(user *User) error}type Service struct {store UserStore}// Concrete implementation can be in another package// It doesn't need to know about this interface
Optional Behavior with Type Assertions
type Flusher interface {Flush() error}func WriteAndFlush(w io.Writer, data []byte) error {if _, err := w.Write(data); err != nil {return err}// Flush if supportedif f, ok := w.(Flusher); ok {return f.Flush()}return nil}
Package Organization
Standard Project Layout
myproject/├── cmd/│ └── myapp/│ └── main.go # Entry point├── internal/│ ├── handler/ # HTTP handlers│ ├── service/ # Business logic│ ├── repository/ # Data access│ └── config/ # Configuration├── pkg/│ └── client/ # Public API client├── api/│ └── v1/ # API definitions (proto, OpenAPI)├── testdata/ # Test fixtures├── go.mod├── go.sum└── Makefile
Package Naming
// Good: Short, lowercase, no underscorespackage httppackage jsonpackage user// Bad: Verbose, mixed case, or redundantpackage httpHandlerpackage json_parserpackage userService // Redundant 'Service' suffix
Avoid Package-Level State
// Bad: Global mutable statevar db *sql.DBfunc init() {db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))}// Good: Dependency injectiontype Server struct {db *sql.DB}func NewServer(db *sql.DB) *Server {return &Server{db: db}}
Struct Design
Functional Options Pattern
type Server struct {addr stringtimeout time.Durationlogger *log.Logger}type Option func(*Server)func WithTimeout(d time.Duration) Option {return func(s *Server) {s.timeout = d}}func WithLogger(l *log.Logger) Option {return func(s *Server) {s.logger = l}}func NewServer(addr string, opts ...Option) *Server {s := &Server{addr: addr,timeout: 30 * time.Second, // defaultlogger: log.Default(), // default}for _, opt := range opts {opt(s)}return s}// Usageserver := NewServer(":8080",WithTimeout(60*time.Second),WithLogger(customLogger),)
Embedding for Composition
type Logger struct {prefix string}func (l *Logger) Log(msg string) {fmt.Printf("[%s] %s\n", l.prefix, msg)}type Server struct {*Logger // Embedding - Server gets Log methodaddr string}func NewServer(addr string) *Server {return &Server{Logger: &Logger{prefix: "SERVER"},addr: addr,}}// Usages := NewServer(":8080")s.Log("Starting...") // Calls embedded Logger.Log
Memory and Performance
Preallocate Slices When Size is Known
// Bad: Grows slice multiple timesfunc processItems(items []Item) []Result {var results []Resultfor _, item := range items {results = append(results, process(item))}return results}// Good: Single allocationfunc processItems(items []Item) []Result {results := make([]Result, 0, len(items))for _, item := range items {results = append(results, process(item))}return results}
Use sync.Pool for Frequent Allocations
var bufferPool = sync.Pool{New: func() interface{} {return new(bytes.Buffer)},}func ProcessRequest(data []byte) []byte {buf := bufferPool.Get().(*bytes.Buffer)defer func() {buf.Reset()bufferPool.Put(buf)}()buf.Write(data)// Process...return buf.Bytes()}
Avoid String Concatenation in Loops
// Bad: Creates many string allocationsfunc join(parts []string) string {var result stringfor _, p := range parts {result += p + ","}return result}// Good: Single allocation with strings.Builderfunc join(parts []string) string {var sb strings.Builderfor i, p := range parts {if i > 0 {sb.WriteString(",")}sb.WriteString(p)}return sb.String()}// Best: Use standard libraryfunc join(parts []string) string {return strings.Join(parts, ",")}
Go Tooling Integration
Essential Commands
# Build and rungo build ./...go run ./cmd/myapp# Testinggo test ./...go test -race ./...go test -cover ./...# Static analysisgo vet ./...staticcheck ./...golangci-lint run# Module managementgo mod tidygo mod verify# Formattinggofmt -w .goimports -w .
Recommended Linter Configuration (.golangci.yml)
linters:enable:- errcheck- gosimple- govet- ineffassign- staticcheck- unused- gofmt- goimports- misspell- unconvert- unparamlinters-settings:errcheck:check-type-assertions: truegovet:enable:- shadowissues:exclude-use-default: false
Quick Reference: Go Idioms
| Idiom | Description | |
|---|---|---|
| Accept interfaces, return structs | Functions accept interface params, return concrete types | |
| Errors are values | Treat errors as first-class values, not exceptions | |
| Don't communicate by sharing memory | Use channels for coordination between goroutines | |
| Make the zero value useful | Types should work without explicit initialization | |
| A little copying is better than a little dependency | Avoid unnecessary external dependencies | |
| Clear is better than clever | Prioritize readability over cleverness | |
| gofmt is no one's favorite but everyone's friend | Always format with gofmt/goimports | |
| Return early | Handle errors first, keep happy path unindented |
Anti-Patterns to Avoid
// Bad: Naked returns in long functionsfunc process() (result int, err error) {// ... 50 lines ...return // What is being returned?}// Bad: Using panic for control flowfunc GetUser(id string) *User {user, err := db.Find(id)if err != nil {panic(err) // Don't do this}return user}// Bad: Passing context in structtype Request struct {ctx context.Context // Context should be first paramID string}// Good: Context as first parameterfunc ProcessRequest(ctx context.Context, id string) error {// ...}// Bad: Mixing value and pointer receiverstype Counter struct{ n int }func (c Counter) Value() int { return c.n } // Value receiverfunc (c *Counter) Increment() { c.n++ } // Pointer receiver// Pick one style and be consistent
Remember: Go code should be boring in the best way - predictable, consistent, and easy to understand. When in doubt, keep it simple.