This document serves as the Source of Truth for all developers working on this Go project. It outlines idiomatic practices aligned with Go’s philosophy of simplicity, readability, and efficiency. As a Principal Software Engineer with over a decade of experience in building large-scale distributed systems, I emphasize that adherence to these standards ensures maintainable, performant code. Deviations must be justified in code reviews and documented.
Follow “Effective Go” and the Go Proverbs as foundational principles: “A little copying is better than a little dependency,” and “Clear is better than clever.”
1. Naming Conventions & Code Style
Go favors brevity and clarity in naming. Names should be descriptive yet concise, using mixedCaps or MixedCaps (camelCase) for unexported identifiers and PascalCase for exported ones.
Guidelines for Naming
- Variables and Functions: Use short, meaningful names. Prefer single-letter variables (e.g., i for index) in small scopes. Functions should describe actions (e.g., ParseConfig not ParserForConfiguration).
- Interfaces: Keep them small (1-2 methods). Name them based on behavior, ending with “-er” (e.g., io.Reader, Logger).
- Packages: Short, lowercase names without underscores (e.g., config, not configuration_manager). Avoid generic names like util; prefer domain-specific (e.g., auth).
Exported vs. Unexported Identifiers:
- Export only what’s necessary for the package’s API. Unexported (lowercase) for internal use.
- Example: In package user, export UserService but keep userRepo unexported.
Bad Example (Verbose, non-idiomatic):
Go
// Bad: Too verbose, Java-like
func ProcessUserRegistrationInformation(userInfo map[string]string) error {
// ...
}
Good Example (Brevity with clarity):
Go
// Good: Concise, descriptive
func RegisterUser(info map[string]string) error {
// ...
}
Enforced Linting Tools
- gofmt and goimports: All code must be formatted with gofmt. Use goimports to manage imports automatically. Run goimports -w . before commits.
- golangci-lint: Mandatory for all PRs. Configure with .golangci.yml enabling linters like govet, staticcheck, errcheck, and gosec. Disable opinionated ones if they conflict with project needs, but justify in the config.
- Integrate into CI/CD: Fail builds on lint errors.
2. Project Structure (Vertical Slice & Clean Architecture)
Organize code in “vertical slices” by feature (e.g., user authentication as a slice), not by type (e.g., all controllers in one dir). This promotes modularity and aligns with Clean Architecture: layers like entities, usecases, repositories, and handlers.
Standards for Organizing Files
- Root structure: cmd/ (entry points), internal/ (unexported packages), pkg/ (exported if reusable), api/ (if RESTful).
- Per slice: Group related files (e.g., internal/user/user.go, user_repository.go, user_handler.go).
- Avoid god packages; split when >500 LOC.
Handling Dependencies
- Dependency Injection (DI): Use constructor functions for wiring. No frameworks like Wire; manual DI keeps it simple.
- Constructor Patterns: Provide NewX functions returning interfaces.
Bad Example (Global dependencies):
Go
// Bad: Hidden global dependency
var db *sql.DB
func GetUser(id int) (*User, error) {
// Uses global db
}
Good Example (DI via constructor):
Go
// Good: Explicit dependency
type UserRepo interface {
Get(id int) (*User, error)
}
func NewUserService(repo UserRepo) *UserService {
return &UserService{repo: repo}
}
Strict Rules on Import Cycles
- Avoid cycles by depending on interfaces, not concrete types. Place interfaces in the dependent package.
- Detect with go list -f ‘{{.ImportPath}}’ ./… | xargs go list -f ‘{{.Deps}}’ | grep cycle or linters.
- Rule: Higher layers (handlers) depend on lower (repos) via interfaces. Never import up the stack.
3. Error Handling (The Go Way)
Errors are values in Go—handle them explicitly. No try-catch; return errors as last value.
Wrapping Errors
- Use fmt.Errorf with %w for wrapping to preserve stack traces (since Go 1.13).
- Custom error types: Use when adding context or behavior (e.g., Is method for checking).
Bad Example (Lost context):
Go
// Bad: No wrapping, loses original error
if err != nil {
return fmt.Errorf("failed to save")
}
Good Example (Wrapped with context):
Go
// Good: Wraps for propagation
if err != nil {
return fmt.Errorf("save user: %w", err)
}
Panic vs. Error
- Panic only for unrecoverable programmer errors (e.g., nil dereference in init). Recover in main or handlers.
- Errors for expected failures (e.g., invalid input, DB timeout). Never panic on user input.
Standardized Error Response Structures for API
- Use a struct like:
Go
type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
- HTTP handlers: Return JSON with status (e.g., 400 for bad request). Log full errors internally.
4. Concurrency & Performance
Go’s concurrency is lightweight—use it judiciously to avoid complexity.
Best Practices for Goroutines and Channels
- Always wait for goroutines (use sync.WaitGroup). Avoid leaks: Bound goroutines with semaphores if unbounded.
- Channels: Prefer buffered for performance; close when done to prevent deadlocks.
Bad Example (Leak-prone):
Go
// Bad: No wait, potential leak
go func() {
// work
}()
Good Example (Safe with WaitGroup):
Go
// Good: Ensures completion
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// work
}()
wg.Wait()
Use of context.Context
- Always pass context.Context as first arg for cancellation, timeouts, and values (sparingly).
- Use context.WithTimeout for I/O operations.
Guidelines for sync.Pool or Mutex
- Use sync.Mutex for simple shared state protection. Prefer channels for coordination.
- sync.Pool for temporary objects (e.g., buffers) to reduce GC pressure. Avoid for long-lived objects.
- Detect races with -race flag in tests.
5. Database & Integrity
Use repository pattern to abstract DB access. Prefer SQL (e.g., database/sql) over ORMs for control.
Standards for Repository Patterns and Transactions
- Repos return domain types, not DB models.
- Transactions: Use db.BeginTx with context; commit/rollback explicitly.
Bad Example (Leaky abstraction):
Go
// Bad: Exposes sql.Rows
func GetUsers() *sql.Rows {
// ...
}
Good Example (Abstracted):
Go
// Good: Returns domain slice
func (r *UserRepo) List(ctx context.Context) ([]User, error) {
// ...
}
Naming Conventions for DB Columns and Migrations
- Columns: snake_case (e.g., user_id), consistent with Go’s brevity.
- Migrations: Use tools like golang-migrate. Name files YYYYMMDDHHMMSS_description.up.sql.
6. Testing & Quality Assurance
Tests are first-class in Go. Write them concurrently safe.
Rules for Table-Driven Tests
- Mandatory for functions with multiple inputs. Structure as:
Go
tests := []struct {
name string
input int
want int
}{
{"case1", 1, 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// test
})
}
Requirements for Mocking
- Use gomock or testify/mock for interfaces. Generate with mockery.
- Mock only external deps (e.g., DB, HTTP clients).
Minimum Code Coverage and Integration Testing
- Aim for 80%+ coverage (enforced in CI with go test -cover).
- Integration tests: Use Docker for DB mocks. Tag with //go:build integration.
7. Documentation & Comments
Comments explain “why,” not “what.” Code should be self-documenting.
Standards for GoDoc Comments
- Exported identifiers: Comment with full sentences, starting with the name (e.g., // ParseConfig parses the config file.).
- Use godoc or go doc for viewing.
When to Comment “Why” vs. “What”
- “What”: Avoid; let code speak.
- “Why”: For non-obvious decisions (e.g., “// Use mutex here to avoid race on shared map.”).
Bad Example (Redundant):
Go
// Bad: States the obvious
// Add two numbers
func Add(a, b int) int {
return a + b
}
Good Example (Explains why):
Go
// Good: ParseConfig loads from file for flexibility in deployments.
// Why: Allows overriding defaults without recompiling.
func ParseConfig(path string) (*Config, error) {
// ...
}
Adhere to these standards to build robust, Go-idiomatic software. Review and update this document as the project evolves, with team consensus.