Best Practices and Code Organization

The 10,000-Line main.go That Made Me Cry

It was my third month at the company when I inherited "the project." A microservice that had grown from a weekend prototype to a critical production system. The entire codebase was in one file: 10,427 lines of main.go.

package main

import (
    // 47 imports...
)

// 23 global variables
var db *sql.DB
var cache *redis.Client
var logger *log.Logger
// ... 20 more

// 147 functions in no particular order
func handleUserCreate(w http.ResponseWriter, r *http.Request) { }
func validateEmail(email string) bool { }
func connectDatabase() error { }
func processPayment(amount float64) error { }
// ... 143 more functions

Adding a feature meant scrolling through thousands of lines. Finding related functions was impossible. Testing? Forget it - everything depended on global state.

I spent two weeks refactoring. The result:

The impact:

  • 47-line main.go (down from 10,427)

  • 90% test coverage (up from 0%)

  • 3-minute onboarding for new developers (down from days)

  • Zero merge conflicts (everyone works in different packages)

That refactoring taught me: code organization isn't optional. This article covers the patterns that saved the project.


Standard Go Project Layout

The Go community has converged on a standard project structure.

Small Project

Medium Project

Large Project (Standard Layout)

Directory Purposes

cmd/: Application entry points. Each subdirectory is a separate executable.

internal/: Private application code. Cannot be imported by other projects.

pkg/: Public library code. Can be imported by other projects.

api/: API definitions, protocol definitions, OpenAPI specs.

web/: Web application assets (templates, static files).

scripts/: Build, install, analysis scripts.

deployments/: Deployment configs (Docker, Kubernetes, etc.).

docs/: Design documents, user documentation.


Package Organization Principles

Organize by Feature, Not Layer

Bad (organized by layer):

Good (organized by feature):

Benefits:

  • Related code stays together

  • Easier to find everything for a feature

  • Clear boundaries between domains

  • Easier to extract into microservices later

Small, Focused Packages


Naming Conventions

Package Names

Rules:

  • Short, lowercase, single word

  • No underscores or camelCase

  • Match the directory name

  • Be descriptive but concise

File Names

Variable Names

Short names for short scope:

Descriptive names for longer scope:

Function Names

Exported functions (public):

Unexported functions (private):

Constants


Go Tools

gofmt - Format Code

Automatically formats your code:

Never commit unformatted code. Use a pre-commit hook:

go vet - Check for Bugs

Analyzes code for common mistakes:

Catches:

  • Unreachable code

  • Printf format errors

  • Struct tag mistakes

  • Shadowed variables

golint - Style Checker

Checks for:

  • Exported types without comments

  • Incorrect naming conventions

  • Package comments

staticcheck - Advanced Linter

More thorough than go vet. Catches:

  • Unused code

  • Inefficient code

  • Potential bugs

golangci-lint - Meta Linter

Runs multiple linters at once:


Code Review Checklist

Error Handling

Resource Management

Goroutine Leaks

Nil Checks

Interface Segregation


Effective Go Principles

Accept Interfaces, Return Structs

Handle Errors, Don't Panic

Panic only for:

  • Truly unrecoverable errors

  • Initialization failures

  • Programmer errors (not user errors)

Use Context for Cancellation

Prefer Composition Over Inheritance

Go doesn't have inheritance. Use composition:


Common Mistakes to Avoid

Mistake 1: Goroutines Without Exit

Mistake 2: Ignoring Error Returns

Mistake 3: Pointer to Loop Variable

Mistake 4: Not Closing Channels

Mistake 5: Race Conditions

Detect races:


Real Example: Refactored Project Structure

Before (10,000 lines in main.go)

After (organized structure)

cmd/server/main.go:

internal/user/handler.go:

internal/user/service.go:

Benefits:

  • Each file < 200 lines

  • Clear separation of concerns

  • Easy to test (no global state)

  • Easy to find code

  • Easy to onboard new developers


Your Challenge

Refactor a messy codebase:


Key Takeaways

  1. Standard layout: Use cmd/, internal/, pkg/ structure

  2. Organize by feature: Not by layer (handlers, services, etc.)

  3. Small packages: Focused, single responsibility

  4. Naming conventions: Short for short scope, descriptive for long scope

  5. Use go fmt: Always format before commit

  6. Run go vet: Catch common mistakes

  7. Accept interfaces: Return concrete types

  8. Test with -race: Detect race conditions


What I Learned

That 10,000-line main.go refactoring taught me that organization matters:

  • 47-line main.go made the project approachable

  • Feature-based packages eliminated merge conflicts

  • 90% test coverage caught bugs before production

  • 3-minute onboarding for new developers

Coming from Python's "flat is better than nested" and JavaScript's "anything goes," Go's conventions felt prescriptive. But after 2 years and 50+ microservices, the consistency across teams has been invaluable.

The time saved navigating organized code? Weeks.


Next: Building a Real CLI Tool

In the next article, we'll build a complete CLI tool using cobra and viper. You'll learn the patterns I used to build automation tools that saved hundreds of hours.

Last updated