Go Best Practices and Idioms
Writing idiomatic Go means following community conventions and the language's philosophy.
Code Style
Use go fmt
Always format your code. No debates about style.
go fmt ./...
# Or use gofmt -w .
Use goimports
Automatically manage imports:
goimports -w .
Naming Conventions
// Package names: short, lowercase, no underscores
package http
package user
package userservice // Not user_service
// Variables and functions: camelCase
var userName string
func getUserByID(id int) *User
// Exported names: PascalCase
type User struct {}
func NewUser() *User
// Acronyms: keep consistent case
var userID string // Not userId
var httpClient // Not hTTPClient
type HTTPHandler // Not HttpHandler
// Interface names: often end in -er
type Reader interface
type Validator interface
type UserService interface // OK for larger interfaces
// Getter/Setter: just Name(), not GetName()
func (u *User) Name() string // Not GetName()
func (u *User) SetName(n string) // Setter needs Set prefix
Comments
// Package-level comments
// Package http provides HTTP client and server implementations.
package http
// Exported function comments start with the function name
// NewServer creates a new HTTP server with the given address.
func NewServer(addr string) *Server
// Good comment: explains WHY
// We retry 3 times because the upstream API has transient failures
for i := 0; i < 3; i++ {
// Bad comment: explains WHAT (obvious from code)
// Increment i by 1
i++
Error Handling
Always Handle Errors
// Bad: ignoring errors
data, _ := json.Marshal(user)
// Good: handle or propagate
data, err := json.Marshal(user)
if err != nil {
return fmt.Errorf("marshaling user: %w", err)
}
Handle Once
// Bad: handling multiple times
if err != nil {
log.Printf("error: %v", err) // Logged here
return err // And caller logs again
}
// Good: handle at the top
func main() {
if err := run(); err != nil {
log.Fatal(err) // Single place
}
}
func run() error {
if err := doThing(); err != nil {
return fmt.Errorf("doing thing: %w", err) // Just propagate
}
return nil
}
Add Context to Errors
// Bad: loses context
return err
// Good: adds context
return fmt.Errorf("reading config file %s: %w", path, err)
Check Specific Errors
// Good: use errors.Is for sentinel errors
if errors.Is(err, os.ErrNotExist) {
// File doesn't exist
}
// Good: use errors.As for error types
var pathErr *os.PathError
if errors.As(err, &pathErr) {
log.Printf("path: %s", pathErr.Path)
}
Function Design
Accept Interfaces, Return Structs
// Good: flexible input
func Process(r io.Reader) error
// Good: concrete output
func NewBuffer() *bytes.Buffer
Keep Functions Focused
// Bad: does too much
func ProcessUserAndSendEmail(u *User) error
// Good: single responsibility
func ProcessUser(u *User) error
func SendWelcomeEmail(email string) error
Use Named Parameters for Clarity
// Confusing
createUser(true, false, 100)
// Better: use named types
type CreateUserOptions struct {
IsAdmin bool
SendEmail bool
Quota int
}
createUser(CreateUserOptions{IsAdmin: true, Quota: 100})
// Or for simple cases, use named variables
isAdmin := true
sendEmail := false
createUser(isAdmin, sendEmail, 100)
Struct Design
Use Constructors
type Server struct {
addr string
timeout time.Duration
}
// Don't allow direct construction if validation needed
func NewServer(addr string) (*Server, error) {
if addr == "" {
return nil, errors.New("addr required")
}
return &Server{
addr: addr,
timeout: 30 * time.Second, // Default
}, nil
}
Make Zero Values Useful
// Good: zero value is ready to use
type Counter struct {
mu sync.Mutex
count int // Zero is valid
}
// Good: sync.Mutex zero value is an unlocked mutex
var mu sync.Mutex // Ready to use
// Good: bytes.Buffer zero value is empty buffer
var buf bytes.Buffer // Ready to use
Embed for Behavior, Not Just Fields
// Good: embedding adds behavior
type CountingWriter struct {
io.Writer
count int
}
// Now CountingWriter has all Writer methods
Concurrency
Use Channels for Communication
// Good: share by communicating
resultChan := make(chan Result)
go func() {
result := expensiveOperation()
resultChan <- result
}()
result := <-resultChan
// Avoid: communicating by sharing
var result Result
var done bool
go func() {
result = expensiveOperation()
done = true
}()
for !done { /* spin */ } // Bad!
Always Cancel Contexts
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // Always call cancel
Avoid Goroutine Leaks
// Bad: goroutine leaks if nobody receives
func leaky() {
ch := make(chan int)
go func() {
ch <- expensiveOperation() // Blocked forever
}()
return // Leaked goroutine!
}
// Good: use context for cancellation
func safe(ctx context.Context) {
ch := make(chan int)
go func() {
select {
case ch <- expensiveOperation():
case <-ctx.Done():
return
}
}()
}
Use sync.WaitGroup for Goroutine Coordination
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(item Item) {
defer wg.Done()
process(item)
}(item)
}
wg.Wait()
Testing
Use Table-Driven Tests
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -1, -1, -2},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.a, tt.b); got != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
Use Interfaces for Testability
// Production
type UserStore interface {
GetUser(id int) (*User, error)
}
type Service struct {
store UserStore
}
// Test
type MockStore struct {
users map[int]*User
}
func (m *MockStore) GetUser(id int) (*User, error) {
return m.users[id], nil
}
Use t.Helper() for Test Helpers
func assertEqual(t *testing.T, got, want interface{}) {
t.Helper() // Points errors to caller, not this function
if got != want {
t.Errorf("got %v; want %v", got, want)
}
}
Performance
Preallocate Slices When Possible
// Bad: grows slice multiple times
var result []int
for i := 0; i < n; i++ {
result = append(result, i)
}
// Good: preallocate
result := make([]int, 0, n)
for i := 0; i < n; i++ {
result = append(result, i)
}
Use strings.Builder for Concatenation
// Bad: creates many intermediate strings
s := ""
for _, word := range words {
s += word + " "
}
// Good: efficient string building
var b strings.Builder
for _, word := range words {
b.WriteString(word)
b.WriteString(" ")
}
s := b.String()
Avoid Premature Optimization
// Start simple
func process(items []Item) {
for _, item := range items {
handle(item)
}
}
// Only optimize after profiling shows it's a bottleneck
// go test -bench=. -cpuprofile=cpu.out
// go tool pprof cpu.out
Common Patterns
Functional Options
type Server struct {
addr string
timeout time.Duration
}
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{addr: addr, timeout: 30 * time.Second}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
server := NewServer("localhost:8080", WithTimeout(60*time.Second))
Dependency Injection
// Define interfaces where used
type UserService struct {
db Database
cache Cache
logger Logger
}
func NewUserService(db Database, cache Cache, logger Logger) *UserService {
return &UserService{db: db, cache: cache, logger: logger}
}
Graceful Shutdown
func main() {
server := &http.Server{Addr: ":8080"}
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatal(err)
}
}
Anti-Patterns to Avoid
Don't Use init() for Complex Logic
// Bad: hidden initialization
func init() {
db = connectToDatabase() // What if this fails?
}
// Good: explicit initialization
func main() {
db, err := connectToDatabase()
if err != nil {
log.Fatal(err)
}
}
Don't Panic in Libraries
// Bad: library panics
func ParseConfig(data []byte) *Config {
var c Config
if err := json.Unmarshal(data, &c); err != nil {
panic(err) // Caller can't handle this!
}
return &c
}
// Good: return error
func ParseConfig(data []byte) (*Config, error) {
var c Config
if err := json.Unmarshal(data, &c); err != nil {
return nil, err
}
return &c, nil
}
Don't Overuse Empty Interface
// Bad: loses type safety
func Process(data interface{}) interface{}
// Good: use generics or specific types
func Process[T any](data T) T
func ProcessUser(u *User) *Result
Don't Return Nil Interfaces
// Surprising behavior
type Writer interface {
Write([]byte) (int, error)
}
func GetWriter() Writer {
var w *bytes.Buffer // nil
return w // Returns non-nil interface with nil value!
}
w := GetWriter()
if w != nil { // true!
w.Write([]byte("oops")) // panic!
}
// Good: return nil explicitly
func GetWriter() Writer {
var w *bytes.Buffer
if w == nil {
return nil // Nil interface
}
return w
}
Go Proverbs
From Rob Pike's Go Proverbs:
- Don't communicate by sharing memory, share memory by communicating
- Concurrency is not parallelism
- Channels orchestrate; mutexes serialize
- The bigger the interface, the weaker the abstraction
- Make the zero value useful
- A little copying is better than a little dependency
- Clear is better than clever
- Errors are values
- Don't just check errors, handle them gracefully
- Documentation is for users
Resources
Where to Go From Here
You have the language. The next step is building something real. A few projects that exercise everything in this tutorial:
- A CLI tool with subcommands using
flagorcobra. Forces you to think about packaging and error messages. - A small REST API on
net/httpwith JSON, structured logging vialog/slog, and graceful shutdown. Touches concurrency, interfaces, and the standard library at once. - A worker pool that fans out jobs to goroutines, collects results through channels, and respects a
contextfor cancellation. Where Go's concurrency model earns its keep. - A package others can import, with a clean API, table-driven tests, and at least one
Examplefunction. Teaches you what the language wants you to write.
Read code, not just articles. The Go standard library and well-regarded projects (Caddy, Hugo, kubectl) are how idioms travel. When something looks strange, hold it up against gofmt, go vet, and the proverbs in the previous section. They almost always answer the question.