Packages and Project Layout
Every exercise in this book so far has been a single package main file. That is a teaching convenience, not how Go is written. Real programs are several packages, and where you draw the lines between them is the design — a package is the unit of naming, of compilation, and of what the rest of your program is allowed to know.
Here is the whole idea in one module:
// main.go
package main
import (
"fmt"
"myapp/internal/store"
)
func main() {
s := store.New()
s.Add("write the lesson")
fmt.Println(s.All())
}
// internal/store/store.go
package store
type Store struct {
items []string // lowercase: nobody outside this package can touch it
}
func New() *Store { return &Store{} }
func (s *Store) Add(v string) { s.items = append(s.items, v) }
The module path is the import prefix
Your go.mod opens with module myapp. That name is the root of every import path in the project: the directory internal/store is imported as myapp/internal/store, not ./internal/store and not store. Go has no relative imports. Move a directory and every importer's path changes — which sounds annoying until you realize it means an import path always tells you exactly where the code lives.
The last element of the path is conventionally the package name, but it's the package clause in the files that actually decides it. Keep them the same. A directory named store declaring package storage is legal and will confuse everyone, including you, in six weeks.
Capitalization is the access control
There is no public or private keyword. An identifier starting with an uppercase letter is exported — visible to any package that imports yours. Lowercase is package-private, and that privacy is enforced by the compiler, not by convention.
This is why items above is lowercase. Callers get Add and All; they cannot reach in and corrupt the slice. That single decision is the difference between a type you can change later and one whose internals are now everybody's problem.
internal/ is a wall the compiler enforces
A directory named internal anywhere in your tree creates a hard rule: only code rooted at internal's parent may import it. myapp/internal/store is importable from anywhere inside myapp, and from nowhere else — not even by another module that depends on yours.
Exported-but-internal is the sweet spot for application code. store.Store is capitalized so your own packages can use it, while internal/ guarantees no outside module can build against it. You keep the freedom to change it.
Try it
Build a two-package module: a main that uses a store package you write under internal/. The tests live inside internal/store — next to the code they exercise, which is where Go tests always go.
Practice
1. A Two-Package Task Store
IntermediateBuild a two-package module. The
mainpackage is written for you; you implement thestorepackage it imports.In
internal/store/store.go,Newis written for you. Complete:(*Store) Add(task string)— appends a task.(*Store) Len() int— how many tasks are held.(*Store) All() []string— returns the tasks as a copy, so a caller cannot mutate the store's internals through the returned slice.
The field holding the tasks must stay unexported. The hidden tests live in
internal/store/store_test.go— inside the package they exercise — and one of them mutates the sliceAllreturns to prove it is a copy.The module is
submission, somain.goimportssubmission/internal/store.internal/store/store.go
// Package store holds the task list. It lives under internal/, so only code // inside this module may import it. package store // Store holds tasks in memory. // // The field below is lowercase on purpose: it is package-private, so callers // can only reach the tasks through the methods you write. type Store struct { tasks []string } // New returns an empty Store. Written for you, so the rest of your methods // have a real receiver to work with. func New() *Store { return &Store{} } // Add appends a task. func (s *Store) Add(task string) { // TODO: implement } // Len reports how many tasks are held. func (s *Store) Len() int { // TODO: implement return 0 } // All returns the tasks as a COPY, so mutating the result cannot change the // store's internals. func (s *Store) All() []string { // TODO: implement return nil }main.go
// Package main is the entry point. It is deliberately thin: the work lives in // the store package, and main only wires things together. // // You do not need to change this file. package main import ( "fmt" "submission/internal/store" ) func main() { s := store.New() s.Add("write the lesson") s.Add("ship it") fmt.Println(s.Len(), s.All()) }Sign in to write and run your solution in the browser.
Sign in for hints and the solution.