Idiomatic Go patterns, best practices, and conventions for robust, efficient, maintainable applications. USE WHEN writing new Go code, reviewing or refactoring Go, or designing Go packages and modules.
Idiomatic Go patterns, best practices, and conventions for robust, efficient, maintainable applications. USE WHEN writing new Go code, reviewing or refactoring Go, or designing Go packages and modules.
origin
ECC
cluster
systems-languages
version
1.0.0
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 directfuncGetUser(id string) (*User, error) {
user, err := db.FindUser(id)
if err != nil {
, fmt.Errorf(, id, err)
}
user,
}
(*User, ) {
(*User, ) {
u, e := db.FindUser(id); e == {
u,
} {
, e
}
}()
}
return
nil
"get user %s: %w"
return
nil
// Bad: Overly clever
funcGetUser(id string)
error
return
func()
error
if
nil
return
nil
else
return
nil
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.Mutex
count 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.Buffer
buf.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.
// 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
}
funcWriteAndFlush(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()
}
returnnil
}
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 http
package json
package user
// Bad: Verbose, mixed case, or redundantpackage httpHandler
package json_parser
package userService // Redundant 'Service' suffix
Avoid Package-Level State
// Bad: Global mutable statevar db *sql.DB
funcinit() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}
// Good: Dependency injectiontype Server struct {
db *sql.DB
}
funcNewServer(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)funcWithTimeout(d time.Duration) Option {
returnfunc(s *Server) {
s.timeout = d
}
}
funcWithLogger(l *log.Logger) Option {
returnfunc(s *Server) {
s.logger = l
}
}
funcNewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
timeout: 30 * time.Second, // default
logger: log.Default(), // default
}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
server := NewServer(":8080",
WithTimeout(60*time.Second),
WithLogger(customLogger),
)
// Bad: Creates many string allocationsfuncjoin(parts []string)string {
var result stringfor _, p := range parts {
result += p + ","
}
return result
}
// Good: Single allocation with strings.Builderfuncjoin(parts []string)string {
var sb strings.Builder
for i, p := range parts {
if i > 0 {
sb.WriteString(",")
}
sb.WriteString(p)
}
return sb.String()
}
// Best: Use standard libraryfuncjoin(parts []string)string {
return strings.Join(parts, ",")
}
Go Tooling Integration
Essential Commands
# Build and run
go build ./...
go run ./cmd/myapp
# Testing
go test ./...
go test -race ./...
go test -cover ./...
# Static analysis
go vet ./...
staticcheck ./...
golangci-lint run
# Module management
go mod tidy
go mod verify
# Formatting
gofmt -w .
goimports -w .
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 functionsfuncprocess() (result int, err error) {
// ... 50 lines ...return// What is being returned?
}
// Bad: Using panic for control flowfuncGetUser(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 param
ID string
}
// Good: Context as first parameterfuncProcessRequest(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.