{adamcoding}
Part III
15
Chapter 15

Packages

The problem

main.go is 2,000 lines. Finding anything means scrolling. Two unrelated things share a variable name and you didn't notice.

One directory, one package

Go's rule is simple: a directory is a package. Every .go file in it declares the same package name at the top.

tradebot/
├── go.mod
├── main.go                      package main
├── market/
│   ├── bar.go                   package market
│   ├── ring.go                  package market
│   └── indicators.go            package market
├── strategy/
│   ├── strategy.go              package strategy
│   └── emacross.go              package strategy
└── backtest/
    ├── engine.go                package backtest
    └── result.go                package backtest

Files in the same package see each other's names directly - no imports between bar.go and ring.go. Across packages you import.

Starting a module

mkdir tradebot && cd tradebot
go mod init github.com/yourname/tradebot

That creates go.mod holding the module path and Go version. The module path prefixes every import inside the project:

go
package main

import (
	"fmt"

	"github.com/yourname/tradebot/market"
	"github.com/yourname/tradebot/strategy"
)

func main() {
	bars := market.LoadCSV("prices.csv")
	s := strategy.NewEmaCross(13, 21)
	fmt.Println(s.Name(), len(bars))
}

You reach things through the package name: market.Bar, strategy.NewEmaCross. It doesn't have to be a real GitHub URL unless you publish it - but using that form means you can, later, without renaming everything.

(That snippet won't compile on its own, of course: it imports packages you haven't written yet. It's showing the shape, and exercise 15.1 has you build the real thing.)

Standard-library imports go in one group, yours in another, separated by a blank line. gofmt maintains this.

Capitals decide what's visible

Chapter 10 mentioned this; here's where it earns its keep.

go
package market

type Bar struct {
	Open  float64      // visible outside
	Close float64      // visible outside
	rawLine string     // private to package market
}

func LoadCSV(path string) ([]Bar, error) { ... }   // visible
func parseRow(row []string) (Bar, error) { ... }   // private

Outside code can call market.LoadCSV but not market.parseRow. That's your API boundary, expressed with the shift key.

Design tip: start everything lowercase. Export a thing only when something outside genuinely needs it. A small public surface is a package you can change later without breaking anyone.

The internal directory

There's one more level of privacy. Anything under a directory named internal can only be imported by code within the same module:

tradebot/
├── internal/
│   └── testdata/         importable only inside tradebot

Useful for helpers you want to share across your own packages but never expose publicly.

No circular imports

If market imports strategy, then strategy cannot import market. Go refuses to compile it.

This feels restrictive until you realise what it prevents. A circular dependency means two packages are really one package that hasn't admitted it, and untangling them later is miserable. Go makes you deal with it immediately.

When you hit one, the usual fixes are: move the shared type into a third package that both import, or define an interface in the consumer so the dependency only points one way. That second option is Chapter 14 solving an architecture problem - the backtester defines Strategy, so backtest never needs to import strategy.

Documentation is just comments

A comment immediately above a declaration, starting with its name, is its documentation:

go
// Bar is a single OHLCV price bar. Timestamps are always UTC.
type Bar struct { ... }

// MovingAverage computes a simple moving average over a fixed window
// using a ring buffer, at constant cost per update regardless of
// window size.
type MovingAverage struct { ... }

See It Work: read your own docs

go doc ./market
go doc ./market Bar
go doc ./market.MovingAverage.Update

Your comments come back formatted. This is the same mechanism behind every Go package's documentation online - there's no separate doc system, no annotations, just comments in the right place.

Try it after writing a package. Seeing your own API listed back at you is a surprisingly good way to notice that you exported something you shouldn't have.

See It Work: what depends on what

go list -deps ./... | grep tradebot

Prints your packages in dependency order. If the output surprises you - market depending on something you didn't expect - you've found accidental coupling.

A layout for the capstone

tradebot/
├── go.mod
├── cmd/
│   └── backtest/
│       └── main.go              the runnable program
├── market/                       Bar, ring buffer, indicators, CSV loading
├── strategy/                     Strategy implementations
└── backtest/                     the engine, costs, sizing, results

cmd/ holding the entry point is a widespread convention, and it scales: add cmd/livetrade/ later and both share every other package.

Direction of dependency: cmdbacktestmarket, and strategymarket. Nothing points back up. If you find yourself wanting an arrow the other way, that's the signal to extract an interface.

Exercises

15.1 Split your existing code into market, strategy and backtest packages with a cmd/backtest/main.go. Get it compiling.

15.2 Go through market and make private everything that doesn't need to be public. How much of your API disappears?

15.3 Write doc comments for every exported name in market, then read them back with go doc. Fix any that don't make sense out of context.

15.4 Deliberately create a circular import and read the error. Then fix it by defining an interface in the consuming package.

15.5 Run go list -deps ./... and draw the dependency graph on paper. Any surprises?

15.6 Harder. Add internal/testdata with a small fixed CSV, and have your tests load from it. Confirm that code outside the module can't import it.