Best Practices
Writing idiomatic Go code makes it readable, maintainable, and professional. In this final lesson, you'll learn Go's best practices, common idioms, and patterns used by experienced Go developers.
Code Style and Formatting
Use gofmt
# Format all Go files
gofmt -w .
# Or use goimports (also organizes imports)
go install golang.org/x/tools/cmd/goimports@latest
goimports -w .
Golden Rule: Always run
gofmt before committing.
Most editors
can auto-format on save. There's no debate about styleβgofmt is the standard.
Naming Conventions
// β
Good names
var userCount int
var maxRetries = 3
type UserService struct{}
func GetUserByID(id int) (*User, error)
// β Bad names
var usrCnt int // Unclear abbreviation
var MAX_RETRIES = 3 // Not Go style (use maxRetries)
type user_service struct{} // Use camelCase
func get_user_by_id(id int) // Use camelCase
// Acronyms
var userID int // β
ID, not Id
var httpServer // β
HTTP, not Http
var urlPath // β
URL, not Url
Error Handling Patterns
Check Errors Immediately
// β
Good - check immediately
file, err := os.Open("file.txt")
if err != nil {
return err
}
defer file.Close()
// β Bad - delayed check
file, err := os.Open("file.txt")
// ... other code ...
if err != nil { // Too late!
return err
}
Wrap Errors with Context
// β
Good - add context
user, err := db.GetUser(id)
if err != nil {
return fmt.Errorf("failed to get user %d: %w", id, err)
}
// β Bad - lose context
user, err := db.GetUser(id)
if err != nil {
return err // What failed? Which user?
}
Don't Panic
// β
Good - return error
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// β Bad - panic for expected errors
func divide(a, b float64) float64 {
if b == 0 {
panic("division by zero") // Don't panic!
}
return a / b
}
// β
OK - panic only for programmer errors
func mustCompile(pattern string) *regexp.Regexp {
re, err := regexp.Compile(pattern)
if err != nil {
panic(err) // OK in init or must* functions
}
return re
}
Interface Best Practices
Accept Interfaces, Return Structs
// β
Good - accept interface
func ProcessData(r io.Reader) error {
// Works with files, network, strings, etc.
}
// β Bad - accept concrete type
func ProcessData(f *os.File) error {
// Only works with files
}
// β
Good - return concrete type
func NewUser(name string) *User {
return &User{Name: name}
}
// β Bad - return interface (usually)
func NewUser(name string) UserInterface {
return &User{Name: name}
}
Keep Interfaces Small
// β
Good - small, focused interfaces
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Compose when needed
type ReadWriter interface {
Reader
Writer
}
// β Bad - large interface
type DataStore interface {
GetUser(id int) (*User, error)
SaveUser(user *User) error
DeleteUser(id int) error
GetProduct(id int) (*Product, error)
SaveProduct(product *Product) error
// ... 20 more methods
}
Concurrency Patterns
Use Context for Cancellation
// β
Good - use context
func ProcessWithTimeout(ctx context.Context, data []int) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
for _, item := range data {
select {
case <-ctx.Done():
return ctx.Err()
default:
process(item)
}
}
return nil
}
// β Bad - no cancellation
func Process(data []int) error {
for _, item := range data {
process(item) // Can't cancel!
}
return nil
}
Don't Start Goroutines Without Knowing When They Stop
// β
Good - controlled lifecycle
func StartWorker(ctx context.Context) {
go func() {
for {
select {
case <-ctx.Done():
return // Clean shutdown
default:
doWork()
}
}
}()
}
// β Bad - goroutine leak
func StartWorker() {
go func() {
for {
doWork() // Never stops!
}
}()
}
Package Organization
Package Names
// β
Good package names
package user
package http
package json
// β Bad package names
package user_service // No underscores
package utils // Too generic
package helpers // Too generic
package common // Too generic
Avoid Package-Level State
// β Bad - global state
package database
var DB *sql.DB
func init() {
DB, _ = sql.Open("postgres", "...")
}
// β
Good - explicit dependencies
package database
type Store struct {
db *sql.DB
}
func NewStore(connStr string) (*Store, error) {
db, err := sql.Open("postgres", connStr)
if err != nil {
return nil, err
}
return &Store{db: db}, nil
}
Performance Best Practices
Preallocate Slices
// β
Good - preallocate
users := make([]User, 0, 100)
for i := 0; i < 100; i++ {
users = append(users, User{ID: i})
}
// β Slow - grows dynamically
var users []User
for i := 0; i < 100; i++ {
users = append(users, User{ID: i})
}
Use strings.Builder for Concatenation
// β
Good - efficient
var sb strings.Builder
for i := 0; i < 100; i++ {
sb.WriteString("x")
}
result := sb.String()
// β Slow - creates many strings
result := ""
for i := 0; i < 100; i++ {
result += "x"
}
Avoid Unnecessary Allocations
// β
Good - reuse
var buf bytes.Buffer
for _, item := range items {
buf.Reset()
buf.WriteString(item)
process(buf.Bytes())
}
// β Bad - allocates every time
for _, item := range items {
buf := bytes.NewBuffer(nil)
buf.WriteString(item)
process(buf.Bytes())
}
Code Organization Tips
Project Structure:
- cmd/ - Main applications
- internal/ - Private application code
- pkg/ - Public library code
- api/ - API definitions
- web/ - Web assets
- scripts/ - Build and deployment scripts
- docs/ - Documentation
Group Related Code
// β
Good - grouped by feature
user/
user.go // User type and core logic
repository.go // Database operations
service.go // Business logic
handler.go // HTTP handlers
// β Bad - grouped by type
models/
user.go
product.go
repositories/
user_repository.go
product_repository.go
services/
user_service.go
product_service.go
Documentation
// β
Good - clear documentation
// Package user provides user management functionality.
package user
// User represents a user in the system.
// Users have a unique ID and email address.
type User struct {
ID int
Email string
}
// GetByEmail retrieves a user by their email address.
// It returns ErrNotFound if the user doesn't exist.
func GetByEmail(email string) (*User, error) {
// ...
}
// β Bad - no documentation
package user
type User struct {
ID int
Email string
}
func GetByEmail(email string) (*User, error) {
// ...
}
Testing Best Practices
Testing Guidelines:
- Write tests for exported functions
- Use table-driven tests
- Test behavior, not implementation
- Use interfaces for mocking
- Aim for 70-80% coverage
- Run tests in CI/CD
Go Proverbs
Wisdom from the Go Community:
- 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
- interface{} says nothing
- Gofmt's style is no one's favorite, yet gofmt is everyone's favorite
- 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
Essential Go Tools
# Format code
gofmt -w .
goimports -w .
# Lint code
go install golang.org/x/lint/golint@latest
golint ./...
# Static analysis
go vet ./...
# Security check
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...
# Detect race conditions
go test -race ./...
# Generate documentation
godoc -http=:6060
π Congratulations!
You've Completed the Go Tutorial!
You've learned:
- β Go fundamentals and syntax
- β Data structures and algorithms
- β Pointers and interfaces
- β Error handling patterns
- β Concurrency with goroutines and channels
- β Package organization and modules
- β File I/O and JSON
- β HTTP clients and servers
- β Testing and benchmarking
- β Best practices and idioms
What's Next?
Continue Your Go Journey:
- Build Projects - Create real applications
- Read Effective Go - Official guide
- Explore Standard Library - Read package documentation
- Join the Community - Gophers Slack, Reddit r/golang
- Contribute to Open Source - Find Go projects on GitHub
- Learn Advanced Topics - Reflection, unsafe, cgo
- Study Go Source Code - Learn from the masters
Recommended Resources
Summary
- Use gofmt for consistent formatting
- Check errors immediately and add context
- Accept interfaces, return structs
- Keep interfaces small and focused
- Use context for cancellation
- Avoid global state and package-level variables
- Preallocate slices when size is known
- Document exported items
- Write tests for all exported functions
- Clear is better than clever
π You're Now a Go Developer! π
Go forth and build amazing things!
Enjoying these tutorials?