Packages, modules and imports, against npm and ESM
You have been writing single-file programs.
You have been writing single-file programs. Real projects are many files, and Go's rules for organising them are stricter and simpler than what you are used to. This chapter is the one that makes a Go repository stop looking strange.
A package is a directory
Every .go file belongs to a package, declared on its first line. *Every file in a directory must declare the same package name*, and a directory is exactly one package. That is the whole rule.
myapp/โโโ go.mod module github.com/you/myappโโโ main.go package mainโโโ store/โ โโโ store.go package storeโ โโโ user.go package storeโโโ money/โโโ money.go package money
Files in store/ can see each other's identifiers with no import at all, even the lowercase ones. There is no equivalent of importing between files in the same folder, because they are already the same package.
If you know JavaScript
This is the biggest structural difference from JavaScript. In a Next.js project every file is its own module, and you import between them constantly. In Go, store/store.go and store/user.go share a namespace: a function declared in one is callable from the other with no ceremony. You import across directories, never within one.
Importing
package mainimport ("fmt" // standard library"net/http""github.com/google/uuid" // third party"github.com/you/myapp/store" // your own)func main() {s := store.New()fmt.Println(uuid.NewString())}
You refer to things by the package name, which is the last element of the path by convention but is really whatever the files declare. store.New, notNew.
The grouping above is conventional: standard library first, then third party, then your own module, separated by blank lines. gofmt sorts within groups andgoimports adds and removes imports as you type.
Watch out
An unused import is a compile error, exactly like an unused variable. This is why goimports in your editor is not optional: without it you will spend real time adding and removing import lines by hand.
The escape hatch, for a package imported only for its side effects, is the blank identifier:
import _ "github.com/lib/pq" // registers a database driver
The import path is the module path plus the directory
There is no baseUrl, no paths mapping, no @/components alias, and no relative imports. import "./store" is not valid Go.
If your module is github.com/you/myapp, then the directory store/ is imported as github.com/you/myapp/store. Always the full path.
If you know JavaScript
Verbose compared to @/store, and it buys one thing: an import path is globally unambiguous. You can paste it into a browser and often land on the source. There is no build-time configuration that has to agree with your editor, your bundler and your test runner, because there is no mapping at all.
internal/ is a real access rule
A directory named internal is enforced by the compiler: only code rooted at its parent may import it.
myapp/โโโ internal/โ โโโ db/ importable by myapp and anything under itโโโ pkg/โโโ client/ importable by anybody
If someone imports github.com/you/myapp/internal/db from a different module, the build fails. This is how a library publishes a small surface while keeping the rest private, and it is why nearly every serious Go project puts most of its code under internal/.
If you know JavaScript
The nearest npm equivalent is the exports field in package.json, which controls what consumers can reach. Go's version is a directory name, needs no configuration and cannot be worked around.
Naming, and the stutter rule
Package names are short, lowercase, single words. No underscores, no camelCase, no plurals.
The identifier is read with the package name, which leads to the most quoted style rule in Go:
// Bad: the package name is already there.store.NewStore()http.HTTPServermoney.MoneyAmount// Good:store.New()http.Servermoney.Amount
Watch out
Avoid util, helpers, common and shared. They are not packages, they are places things go to hide, and they grow into a dependency that everything imports and nobody understands. If a function does not belong anywhere, that usually means the thing it operates on wants a package of its own.
Modules, versions and go.mod
A module is a tree of packages with one go.mod at the root. You saw it in chapter two; here is what the rest of it means.
module github.com/you/myappgo 1.25require (github.com/gin-gonic/gin v1.10.0gorm.io/gorm v1.25.12)require (github.com/bytedance/sonic v1.12.0 // indirect)
The second require block is indirect dependencies: things your dependencies need. go mod tidy manages both blocks, adding what is imported and removing what is not. Run it before every commit.
If you know JavaScript
go.sum is the lockfile and must be committed. Unlike package-lock.json it is not a full dependency tree: it is a list of cryptographic hashes. Go usesminimal version selection, meaning it picks the lowest version that satisfies everyone, so builds are reproducible from go.mod alone and the sum file exists to verify integrity rather than to pin.
There is also no devDependencies. A tool you use at development time is either run with go run or tracked in a tools.go file.
Upgrading is explicit:
go get -u ./... # upgrade everything (minor and patch)go get github.com/gin-gonic/gin@v1.10.1 # one package, one versiongo mod tidy
Major versions are in the path
This surprises everyone. From v2 onward, the major version is part of the import path:
import "github.com/you/myapp/v2/store"
It looks strange and it solves a real problem: two major versions of the same library can coexist in one build, because as far as the compiler is concerned they are different packages. You will see this in Grit itself, which imports asgithub.com/MUKE-coder/grit/v3.
init, and why to avoid it
A package can declare func init(), which runs automatically before main.
func init() {// runs once, before main, in no order you control across packages}
Use it rarely. It makes startup order implicit, it cannot return an error, and it runs even when a test only wanted one function from the package. Explicit setup called from main is almost always better.
Project 8: split the inventory into packages
Take the inventory from chapter seven and give it a real project layout. The point is to feel the import rules rather than to add features.
inventory/โโโ go.mod module github.com/you/inventoryโโโ main.go package mainโโโ internal/โโโ inventory/โโโ item.go package inventoryโโโ store.go package inventory
internal/inventory/item.go:
package inventoryimport "errors"var ErrInsufficientStock = errors.New("insufficient stock")type Item struct {SKU stringName stringStock int}// low is unexported, so only this package can call it. item.go and// store.go// are the same package, so store.go can use it with no import.func (i *Item) low(threshold int) bool {return i.Stock < threshold}
internal/inventory/store.go:
package inventoryimport "fmt"type Store struct {items map[string]*Item}func New() *Store {return &Store{items: map[string]*Item{}}}func (s *Store) Add(sku, name string, stock int) {s.items[sku] = &Item{SKU: sku, Name: name, Stock: stock}}func (s *Store) Take(sku string, n int) error {it, ok := s.items[sku]if !ok {return fmt.Errorf("taking %s: not found", sku)}if it.Stock < n {return fmt.Errorf("taking %d of %s: %w",n, sku, ErrInsufficientStock)}it.Stock -= nreturn nil}// Low uses the unexported method from the other file. No import needed.func (s *Store) Low(threshold int) []*Item {var out []*Itemfor _, it := range s.items {if it.low(threshold) {out = append(out, it)}}return out}
main.go:
package mainimport ("errors""fmt""github.com/you/inventory/internal/inventory")func main() {inv := inventory.New()inv.Add("A1", "Blue widget", 10)if err := inv.Take("A1", 99); err != nil {// truefmt.Println(errors.Is(err, inventory.ErrInsufficientStock))}for _, it := range inv.Low(20) {fmt.Println("low:", it.Name, it.Stock)}}
Now run three experiments. Call inv.low(5) from main.go and read the error: lowercase means package-private, and the compiler enforces it. Move the whole internal directory to pkg and confirm it still builds, then put it back. And try import "./internal/inventory" to see that relative imports genuinely do not exist.
Your turn
Add a second package, internal/report, with a function that takes a*inventory.Store and prints a summary. You will hit a design question immediately: report needs to read every item, but Store.items is unexported. Solve it by adding an exported method to inventory rather than by exporting the field, and notice that the package boundary just forced you into a better API than you would have written in one file.
Assignment
Commit your work with the message Packages - Assignment One, exactly as written, so the leaderboard can score it.
Try it
Take one of your earlier programs apart.
Copy your chapter seven word counter into part1-language/09-packages and split it.
One. internal/count holds the counting, internal/report holds the printing, and main wires them. Nothing in internal/ may import main.
Two. Name the exported functions so the call site reads well:count.Words(r) rather than count.CountWords(r).
Three. Try to import your internal/count from a second module on disk, and keep the error in NOTES.md. That rule is enforced by the compiler rather than by review, which is the whole point.
Four. Add one init() and then remove it, writing one sentence on what you replaced it with.
Spot a typo? Have an idea?
Help us improve this lesson. One click opens a GitHub issue with the lesson URL pre-filled, suggest clearer wording, report a bug, or request more depth. The course keeps improving thanks to learners like you.
Suggest an improvement on GitHub