| name | golang-patterns |
| description | Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications. Use when writing Go code, reviewing Go patterns, setting up Go project structure, or implementing concurrency, error handling, or interfaces in Go. |
| user-invocable | true |
| triggers | ["Go best practices","idiomatic Go code","Go concurrency pattern","Go error handling","Go interface pattern","write idiomatic Go","Go project structure","Go channel pattern"] |
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.
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
}
func 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.
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Inc() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
var buf bytes.Buffer
buf.WriteString("hello")
type BadCounter struct {
counts map[string]int
}
3. Accept Interfaces, Return Structs
Functions should accept interface parameters and return concrete types.
func ProcessData(r io.Reader) (*Result, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return &Result{Data: data}, nil
}
func ProcessData(r io.Reader) (io.Reader, error) {
}
Error Handling Patterns
Error Wrapping with Context
func 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 Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config %s: %w", path, err)
}
return &cfg, nil
}
Custom Error Types
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
var (
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) {
if errors.Is(err, sql.ErrNoRows) {
log.Println("No records found")
return
}
var validationErr *ValidationError
if errors.As(err, &validationErr) {
log.Printf("Validation error on field %s: %s",
validationErr.Field, validationErr.Message)
return
}
log.Printf("Unexpected error: %v", err)
}
Never Ignore Errors
result, _ := doSomething()
result, err := doSomething()
if err != nil {
return err
}
_ = writer.Close()
Concurrency Patterns
Worker Pool
func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for 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)
<-quit
log.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
g.Go(func() error {
data, err := FetchWithTimeout(ctx, url)
if err != nil {
return err
}
results[i] = data
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
Avoiding Goroutine Leaks
func leakyFetch(ctx context.Context, url string) <-chan []byte {
ch := make(chan []byte)
go func() {
data, _ := fetch(url)
ch <- data
}()
return ch
}
func safeFetch(ctx context.Context, url string) <-chan []byte {
ch := make(chan []byte, 1)
go func() {
data, err := fetch(url)
if err != nil {
return
}
select {
case ch <- data:
case <-ctx.Done():
}
}()
return ch
}
Interface Design
Small, Focused Interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Define Interfaces Where They're Used
package service
type UserStore interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
type Service struct {
store UserStore
}
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
}
if 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
package http
package json
package user
package httpHandler
package json_parser
package userService
Avoid Package-Level State
var db *sql.DB
func init() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}
type Server struct {
db *sql.DB
}
func NewServer(db *sql.DB) *Server {
return &Server{db: db}
}
Struct Design
Functional Options Pattern
type Server struct {
addr string
timeout time.Duration
logger *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,
logger: log.Default(),
}
for _, opt := range opts {
opt(s)
}
return s
}
server := 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
addr string
}
func NewServer(addr string) *Server {
return &Server{
Logger: &Logger{prefix: "SERVER"},
addr: addr,
}
}
s := NewServer(":8080")
s.Log("Starting...")
Memory and Performance
Preallocate Slices When Size is Known
func processItems(items []Item) []Result {
var results []Result
for _, item := range items {
results = append(results, process(item))
}
return results
}
func 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)
return buf.Bytes()
}
Avoid String Concatenation in Loops
func join(parts []string) string {
var result string
for _, p := range parts {
result += p + ","
}
return result
}
func join(parts []string) string {
var sb strings.Builder
for i, p := range parts {
if i > 0 {
sb.WriteString(",")
}
sb.WriteString(p)
}
return sb.String()
}
func join(parts []string) string {
return strings.Join(parts, ",")
}
Go Tooling Integration
Essential Commands
go build ./...
go run ./cmd/myapp
go test ./...
go test -race ./...
go test -cover ./...
go vet ./...
staticcheck ./...
golangci-lint run
go mod tidy
go mod verify
gofmt -w .
goimports -w .
Recommended Linter Configuration (.golangci.yml)
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports
- misspell
- unconvert
- unparam
linters-settings:
errcheck:
check-type-assertions: true
govet:
check-shadowing: true
issues:
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
func process() (result int, err error) {
return
}
func GetUser(id string) *User {
user, err := db.Find(id)
if err != nil {
panic(err)
}
return user
}
type Request struct {
ctx context.Context
ID string
}
func ProcessRequest(ctx context.Context, id string) error {
}
type Counter struct{ n int }
func (c Counter) Value() int { return c.n }
func (c *Counter) Increment() { c.n++ }
Remember: Go code should be boring in the best way - predictable, consistent, and easy to understand. When in doubt, keep it simple.