Packages, modules and tests
How a Go project is arranged on disk, what the capital letter on a name means, and the test style the whole language uses: a table of cases, one loop, no framework.
A package is a folder
Every Go file names its package at the top, and one folder holds one package — the files beside each other are the same program, seeing each other's names without imports. The import path is the folder: a path like shop/cart is the cart package inside the shop module.
Whether a name is visible outside its package is written in the name itself. Capitalised names are exported; anything lowercase is private to the package. There is no other visibility keyword in Go, so the difference between Total and total is the whole of the public surface, visible as you read.
A module is the unit you build
go mod init shop writes a go.mod file naming the module, and from then on that file lists the dependencies and the Go version the project expects. go build and go test work from it, and go mod tidy reconciles it with what the code actually imports.
An internal folder is Go's other boundary: code inside it can be imported by the module but not from outside, which is how a project keeps a piece of itself to itself without copying it into every package.
Tests are functions in the same package
A file ending in _test.go holds tests, and a test is a function starting with Test that takes a pointer to a testing.T. It fails by calling t.Errorf or t.Fatalf, and go test ./... runs every test in the module. There is no framework to install — the standard library is the framework.
The style you will see everywhere is table-driven: a slice of cases, one loop, one set of assertions. Adding a case means adding a row, which is exactly how a test suite grows well — and because the test lives in the same package, it can reach the unexported names it is checking.
What a test file looks like
package cart
import "testing"
func Total(prices []int) int {
sum := 0
for _, price := range prices {
sum += price
}
return sum
}
func TestTotal(t *testing.T) {
cases := []struct {
name string
prices []int
want int
}{
{"empty", nil, 0},
{"one", []int{5}, 5},
{"many", []int{5, 4, 3}, 12},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Total(tc.prices); got != tc.want {
t.Errorf("Total(%v) = %d, want %d", tc.prices, got, tc.want)
}
})
}
}A table of cases, run by hand
Run it, then add a case of your own — a price list with a zero in it — and give one case a deliberately wrong answer to see what a failure looks like.
This site runs one file, so the run button below exercises the same table of cases without the testing package. On your machine, go test ./... is what turns that table into a verdict.
How the toolchain reads a project
An import path is resolved through the module: shop/cart means the cart directory under the module root named shop. The compiler builds packages in dependency order — each imported package is initialised first, then the package's variables, then its init functions, before main runs.
That order makes init a sharp tool. It runs before main, cannot return an error, and is easy to trigger from a test that only wanted one function, so most packages should have none. A blank import, _ "pkg", exists for packages whose only job is a side effect — registering a driver, for instance.
Tests live in the same directory. A test file declared package cart is inside the package and can reach unexported names; package cart_test is the external view and sees only what a real importer could. go test compiles both, runs the TestXxx functions, and caches a result until an input changes.
A worked import path
Follow an import through the module. With module shop in go.mod and a folder cart/, the package declared at the top of cart/cart.go is imported as shop/cart. The compiler resolves that prefix to the module root, then the rest to the folder, and compiles the package before the file that imports it. A dependency outside the module is resolved from go.mod, downloaded once into the module cache, and recorded with a version. Nothing is found by searching the disk: the import path and the module file are the whole address.
A test in package cart is compiled as part of the package, so it can call total and touch unexported fields. A test in package cart_test is compiled as a separate package that imports cart, so it sees only the exported names — exactly what a real caller sees. Both live in the same folder; the package clause, not the folder, decides the view. Use the internal one for a tricky helper, the external one for the behaviour you promise.
An import cycle is a compile error, not a warning: if A imports B and B imports A, there is no valid build order, and the fix is usually to move the shared type into a third package. An init that opens a file or a connection makes a unit test need the outside world. And a test in package cart_test cannot see total — only Total — which is the point of the external view.
Try these next
- On your machine, run
go mod init example/shop, add acartpackage, and import it frommain. - Write one test in
package cartand one inpackage cart_test, and note what each can reach. - Run
go test ./...twice and watch the second run report(cached).
A tiny test runner
Run it, then add a case for `"two spaces"` and change `slug` to collapse runs of whitespace with `strings.Fields` and `strings.Join`. Give one case a deliberately wrong answer first.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What makes a name usable from another package?
