Go Module 9 – Packages, modules and dependencies
Packages, exported names and initialisation
Split Go code into packages, export names with a capital letter, keep a package private with internal/, break import cycles and predict init order.
What you will learn
- Split a program into packages and choose what each one exports with capital letters
- Limit which packages may import a package by placing it under internal/
- Resolve an import cycle with a small interface or a third package
- Predict the order in which package variables, init functions and main run
- Name packages for what they provide instead of util or common
Before you start
What you’ll learn · Before you start
What you will learn
- Split a program into packages and choose what each one exports with capital letters
- Limit which packages may import a package by placing it under internal/
- Resolve an import cycle with a small interface or a third package
- Predict the order in which package variables, init functions and main run
- Name packages for what they provide instead of util or common
Before you start
On this page
In the kirana module of this lesson, pricing.Total(bill) works from another package and pricing.tax(4800) stops
the build, and the only difference between them is the case of the first letter. Go has no public or private
keyword: folders make packages, a capital letter exports a name, and two more rules decide who may import whom, the go
command’s rule for folders named internal and the specification’s ban on packages that import each other.
You will split a program into packages, decide what each one exports, keep a package inside your own tree, break an
import cycle, and predict the order in which a program’s packages start before main runs. Every message quoted here
was printed by Go 1.27.1.
A package is one folder of files that share a name
The kirana module bills a customer of a kirana, a neighbourhood grocery shop in India, in paise. Its go.mod declares the module path
example.com/kirana, and each folder below it that holds .go files is one package. A package is the set of .go files
in one folder, compiled together: every file starts with the same package clause, and anything declared at the top
level of one file, a function, a type, a variable or a constant, can be used by the other files of that folder without
an import. Here are two of the packages:
Go (pricing.go) · kirana/pricing/pricing.go
// Package pricing works out what a customer pays, in paise.
package pricing
// Line is one line of a bill: what was bought, and how many.
type Line struct {
Name string
Qty int
pricePaise int64 // the price of one item, set only by NewLine
}
// NewLine makes a bill line for qty items at pricePaise each.
func NewLine(name string, qty int, pricePaise int64) Line {
return Line{Name: name, Qty: qty, pricePaise: pricePaise}
}
// Total adds up the lines and the tax on them, in paise.
func Total(lines []Line) int64 {
var sum int64
for _, l := range lines {
sum += l.pricePaise * int64(l.Qty)
}
return sum + tax(sum)
}
// tax is 5 per cent of amount in this example, rounded to the nearest paisa.
func tax(amount int64) int64 {
return (amount*5 + 50) / 100
} Go (main.go) · kirana/cmd/billing/main.go
// Command billing prints one customer's bill.
package main
import (
"fmt"
"example.com/kirana/internal/ledger"
"example.com/kirana/pricing"
)
func main() {
bill := []pricing.Line{
pricing.NewLine("rice 1 kg", 2, 6400),
pricing.NewLine("toor dal 500 g", 1, 8950),
}
total := pricing.Total(bill)
ledger.Record(total)
fmt.Printf("total: Rs %d.%02d\n", total/100, total%100)
fmt.Println(ledger.Summary())
} The program in cmd/billing says package main; the other folders say the name their code is used by, such as
package pricing. A package’s import path: The string an import declaration names, such as example.com/kirana/pricing: the module path followed by the package's folder in the module. Standard-library packages, such as fmt, have no module path in front. is the
module path: The name on the module line of a go.mod file, such as example.com/kirana. It begins the import path of every package of the module and tells the go command where the module can be downloaded. joined with its folder, and go list ./... prints the import path of every package in the
module:
from session import run
# The packages of the kirana module and their import paths, then the program that uses two of them.
run("""
cat kirana/go.mod
cd kirana && go list ./...
cd kirana && go run ./cmd/billing
""") Output
$ cat kirana/go.mod module example.com/kirana go 1.27 $ cd kirana && go list ./... example.com/kirana/cmd/billing example.com/kirana/cmd/peek example.com/kirana/internal/ledger example.com/kirana/pricing $ cd kirana && go run ./cmd/billing total: Rs 228.38 takings so far: Rs 228.38
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 layout_session.py
The four paths are the four folders with example.com/kirana/ in front, and go run built the program from three of
them and printed a bill of Rs 228.38. Step through the build of cmd/billing and watch each import line turn into a
folder:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The module root. go.mod says module example.com/kirana. Every folder below it that holds .go files is one package, and that package's import path is the module path followed by the folder.
- The program. cmd/billing holds the files that say package main, so its import path is example.com/kirana/cmd/billing, and building it makes a program.
- An import is a path. billing imports example.com/kirana/pricing. The path starts with the module path, so the go command looks in the folder that follows it, pricing, and compiles every .go file there together.
- A deeper folder. example.com/kirana/internal/ledger works the same way and becomes the folder internal/ledger. Its last element, ledger, is also the name the code uses, as in ledger.Record.
- No dot, no module. fmt has no dot in its first element, so it is a standard-library package from the Go installation, never a folder of this module.
- Built. Each package is compiled once, with all its files, before the packages that import it. Then the program is linked and runs, and prints the bill of Rs 228.38.
By convention a package is named after the last element of its import path, so example.com/kirana/internal/ledger
is used as ledger.Record. Only main breaks the convention, because the program needs that name.
Checkpoint
Check: A package is one folder
A capital first letter exports a name, and nothing else does
cmd/peek is a second program in the same module that reaches for two names pricing keeps to itself, the field
pricePaise and the function tax. It does not compile:
from session import run
# cmd/peek reaches for an unexported field and an unexported function of package pricing.
run(
"""
cd kirana && go build ./cmd/peek
""",
expect_failure=["cd kirana && go build ./cmd/peek"],
) Output
$ cd kirana && go build ./cmd/peek # example.com/kirana/cmd/peek cmd/peek/main.go:12:48: cannot refer to unexported field pricePaise in struct literal of type pricing.Line cmd/peek/main.go:13:22: name tax not exported by package pricing [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 exports_session.py
The compiler names both mistakes: cannot refer to unexported field pricePaise in struct literal of type pricing.Line, and name tax not exported by package pricing. The rule is short. A name is exported name: A name that other packages can use. A Go name is exported when its first character is an upper-case letter and it is declared at the package level, or when it names a field or a method. when its first character is an upper-case letter, Unicode category Lu, and it is declared at the top level of the
package, or is the name of a field or a method. Line, NewLine, Total, Name and Qty pass both tests; tax and
pricePaise fail the first, so they are visible in every file of the pricing folder and in no other package.
Key idea
A name is exported when it starts with an upper-case letter and is declared at the package level, or names a field or method; everything else is shared only by the files of its own folder.
The unexported field is deliberate. Other packages can read and set Name and Qty, but only code in pricing, such as
NewLine, can read or set the price, so pricing decides how a price gets into a line, and it can change how it stores prices without breaking any
caller.
Exporting a helper to silence the error
Renaming tax to Tax makes the build pass and turns a private detail into a promise: every caller can now depend
on how the tax is computed, and you can no longer change it freely. Ask what the caller needs instead. Here it needs
a bill’s total, which Total already gives.
Checkpoint
Check: Exported and unexported names
internal/ keeps a package inside your own tree
An exported name can be used by any code that imports its package, even code in another module. Sometimes you want a
package that every part of your module may share and nobody else may use, such as kirana’s ledger, which keeps the day’s takings. The go
command has a rule for that, and no keyword is involved: an internal package: A package in or below a folder named internal. The go command lets only code whose import path starts with the path of the folder above internal import it., one in or below a folder named internal, can be
imported only by code whose import path starts with the path of the folder above internal.
rival is a second module next to kirana. Its go.mod points example.com/kirana at the folder beside it with a
replace line, so the build reads kirana from disk, and its main.go imports the ledger:
from session import run
# rival is another module. Its go.mod points example.com/kirana at the folder next to it, and its main.go imports
# kirana's internal package.
run(
"""
cat rival/go.mod
cd rival && go build .
""",
expect_failure=["cd rival && go build ."],
) Output
$ cat rival/go.mod module example.com/rival go 1.27 require example.com/kirana v0.0.0 replace example.com/kirana => ../kirana $ cd rival && go build . package example.com/rival main.go:7:2: use of internal package example.com/kirana/internal/ledger not allowed [exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 internal_session.py
The build stops at the import: use of internal package example.com/kirana/internal/ledger not allowed. The ledger’s
parent folder is the module root, example.com/kirana, and example.com/rival does not start with it. Step through
three packages that import the ledger:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The parent path. ledger sits in internal/, whose parent is the module root, example.com/kirana. Only code whose import path starts with that parent may import it.
- Inside the tree. cmd/billing's path, example.com/kirana/cmd/billing, starts with example.com/kirana, so its import of ledger is allowed, and the bill builds.
- A sibling package. pricing could import ledger too, for the same reason. Any package of the module may; ledger's exported names are shared inside the tree and nowhere else.
- Outside the tree. example.com/rival does not start with example.com/kirana, so the go command stops the build with use of internal package example.com/kirana/internal/ledger not allowed.
- Paths, not modules. The rule compares import paths. A folder kirana/cmd/billing/internal/fmtx would be importable only by cmd/billing and the packages below it, not by pricing.
This makes internal the place for code you share inside a module but do not want to support for others: other
modules cannot import it, so you can rename, split or delete it without breaking anyone outside your tree.
Checkpoint
Check: The internal/ rule
An import cycle does not compile, so make the imports go one way
In the cafe module, Order’s Receipt method calls invoice.Render, and Render takes an orders.Order, so orders
imports invoice and invoice imports orders, an import cycle: Packages that import one another, directly or through other packages. Go refuses to build them, so the imports of a program always form a graph without loops.. The specification forbids that: a package may not import itself, directly
or indirectly. cyclefix is the same module with one change, an interface declared in invoice:
Go (invoice.go) · cyclefix/invoice/invoice.go
// Package invoice formats receipts for anything that can be billed.
package invoice
import (
"fmt"
"strings"
)
// Billable is what invoice needs to know about an order, and no more.
type Billable interface {
Lines() []string
TotalPaise() int64
}
// Render formats a bill as one line.
func Render(b Billable) string {
p := b.TotalPaise()
return fmt.Sprintf("%s: Rs %d.%02d", strings.Join(b.Lines(), ", "), p/100, p%100)
} Go (orders.go) · cyclefix/orders/orders.go
// Package orders holds what a customer ordered.
package orders
import "example.com/cafe/invoice"
// Order is one customer's order.
type Order struct {
Items []string
Paise int64
}
// Lines and TotalPaise make Order an invoice.Billable.
func (o Order) Lines() []string { return o.Items }
func (o Order) TotalPaise() int64 { return o.Paise }
// Receipt formats the order as a receipt.
func (o Order) Receipt() string { return invoice.Render(o) } The session builds the module with the cycle, then runs the fixed one and lists what each package imports:
from session import run
# orders and invoice import each other in cycle/; in cyclefix/ invoice declares the interface it needs and imports
# nothing of the module, so the imports go one way.
run(
"""
cd cycle && go build ./...
cd cyclefix && go run ./cmd/cafe
cd cyclefix && go list -f '{{.ImportPath}} imports {{join .Imports ", "}}' ./...
""",
expect_failure=["cd cycle && go build ./..."],
) Output
$ cd cycle && go build ./...
package example.com/cafe/invoice
imports example.com/cafe/orders from invoice.go
imports example.com/cafe/invoice from orders.go: import cycle not allowed
[exit status 1]
$ cd cyclefix && go run ./cmd/cafe
masala dosa, filter coffee: Rs 140.00
$ cd cyclefix && go list -f '{{.ImportPath}} imports {{join .Imports ", "}}' ./...
example.com/cafe/cmd/cafe imports example.com/cafe/orders, fmt
example.com/cafe/invoice imports fmt, strings
example.com/cafe/orders imports example.com/cafe/invoice
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cycle_session.py
The first build failed with the whole loop spelled out, imports example.com/cafe/orders from invoice.go, then
imports example.com/cafe/invoice from orders.go: import cycle not allowed. After the fix, invoice imports only fmt
and strings, and the receipt printed. Step through the change:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- One direction. orders imports invoice, because Order's Receipt method calls invoice.Render to format the receipt.
- And back. invoice imports orders, because Render takes an orders.Order. Now each package needs the other to be compiled first.
- The build stops. go build names the loop, invoice imports orders imports invoice, and compiles neither. Go has no way to build half of a cycle.
- Ask for less. In cyclefix, invoice declares the interface Billable, two methods it needs, Lines and TotalPaise, and Render takes a Billable. invoice now imports nothing of the module.
- Satisfied without a word. Order already has Lines and TotalPaise, so it is a Billable without saying so, and Receipt can pass itself to Render.
- One way only. cmd/cafe imports orders, orders imports invoice, and nothing points back. The program builds and prints masala dosa, filter coffee for Rs 140.00.
There are two dependable fixes. The one shown asks for less: the package that needs something declares a small interface with only the methods it uses, and any type with those methods satisfies it without importing anything. The other moves what both packages share, here the Order type, into a third package that both import and that imports neither. Choose the interface when one package only calls a few methods of the other, and the third package when both really work on the same data.
Checkpoint
Check: Breaking an import cycle
Variables first, then init, package by package, before main
Before main runs, Go initialises every package of the program. Inside one package the
package-level variable: A variable declared outside every function. It exists for the whole run, and Go gives it its value before main starts, setting each one after the variables its value depends on. More in “Variables, short declarations and zero values” come first, then the
init function: A function named init, with no parameters and no results, declared at the package level. It runs once, after the package's variables have their values, and no code can call it..
The variables of this program are declared in the order total, price, qty:
// initvars.go: the order in which one package starts.
package main
import "fmt"
var total = show("total", price*qty)
var price = show("price", 4800)
var qty = show("qty", 3)
func show(name string, v int) int {
fmt.Println("variable", name, "=", v)
return v
}
func init() { fmt.Println("first init: total is", total) }
func init() { fmt.Println("second init") }
func main() { fmt.Println("main") } Output
variable price = 4800 variable qty = 3 variable total = 14400 first init: total is 14400 second init main
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run initvars.go
It set price, then qty, then total, 14400. The specification’s rule is to initialise, step by step, the variable that comes first in declaration order among those whose values no longer depend on an uninitialised variable. total depends on price and qty, so it waits for both. Then the two init functions ran in the order they are written, and main last. When a package has several files, the specification encourages build tools to hand them to the compiler in file-name order, so that the order of declarations across files is the same on every build.
Across packages, a package starts only after every package it imports. The shopinit module has three: cart imports prices, greeting and prices import nothing of the module, and main imports cart and greeting:
from session import run
# Which package imports which, then the order in which they start when the program runs.
run("""
cd initorder && go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./...
cd initorder && go run .
""") Output
$ cd initorder && go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./...
example.com/shopinit: example.com/shopinit/cart example.com/shopinit/greeting fmt
example.com/shopinit/cart: example.com/shopinit/prices fmt
example.com/shopinit/greeting: fmt
example.com/shopinit/prices: fmt
$ cd initorder && go run .
greeting: variable Text
greeting: init
prices: variable List
prices: init
cart: variable Items, sees 2 prices
cart: init
main: variable started
main: init
main: main runs; Namaste! 2 items
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 startup_session.py
greeting started first, then prices, then cart, then main, although main.go imports cart before greeting. Since Go 1.21 the specification defines the order exactly: sort all packages by import path, then repeatedly initialise the first package in that list whose imports have all been initialised. Step through it:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- The sorted list. The packages are sorted by import path. example.com/shopinit comes first among the module's, then cart, greeting and prices; fmt and the other standard-library packages sit at their own places in the alphabet.
- The standard library. Each step starts the first package in the list whose imports have all started. Every package of the module imports fmt, so fmt and the packages it needs start before any of them.
- greeting. main and cart come earlier in the list but wait for packages that have not started. greeting needs only fmt, so it goes first; its variable Text, then its init.
- prices. The next step looks from the top of the list again. cart still waits for prices, which is now the first one ready, so List is loaded and prices runs its init.
- cart. Now every import of cart has started. Its variable Items is set, and it sees the 2 prices that prices loaded a step earlier, then its init runs.
- package main. The main package starts last, like any package, once cart and greeting have started; its variable started, then its init.
- main runs. Only now does func main run, with every package of the program ready. It prints Namaste! 2 items.
Because the order is defined, a package can rely on the packages it imports being ready, as cart relied on the price list. Nothing else is promised, so never rely on a package that you do not import having started.
An import with the name _, the blank identifier: The name _ (an underscore), which takes a value and throws it away. It lets code ignore a result on purpose, or import a package only for what it does when it starts. More in “Your first Go program: go run and go build”, imports a package only for its initialisation. The image package’s documentation names
the standard case: decoding a PNG needs the decoder that image/png registers in its init, so a program writes
import _ "image/png". The two programs below differ only in that line:
Go (register_png.go) · register_png.go
// register_png.go: a blank import runs the init of image/png.
package main
import (
"bytes"
"encoding/hex"
"fmt"
"image"
_ "image/png" // its init registers the "png" format with package image
)
// A grey image of 2 by 1 pixels, encoded as PNG (73 bytes).
const pngHex = "89504e470d0a1a0a0000000d4948445200000002000000010800000000d149205600" +
"00001049444154789c000300fcff02ff00030002070102e07a5cf80000000049454e44ae426082"
func main() {
data, _ := hex.DecodeString(pngHex)
cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
fmt.Printf("format %q, %dx%d, error: %v\n", format, cfg.Width, cfg.Height, err)
} Output
format "png", 2x1, error: <nil>
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run register_png.go
Go (register_none.go) · register_none.go
// register_none.go: the same program without the blank import.
package main
import (
"bytes"
"encoding/hex"
"fmt"
"image"
)
// A grey image of 2 by 1 pixels, encoded as PNG (73 bytes).
const pngHex = "89504e470d0a1a0a0000000d4948445200000002000000010800000000d149205600" +
"00001049444154789c000300fcff02ff00030002070102e07a5cf80000000049454e44ae426082"
func main() {
data, _ := hex.DecodeString(pngHex)
cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
fmt.Printf("format %q, %dx%d, error: %v\n", format, cfg.Width, cfg.Height, err)
} Output
format "", 0x0, error: image: unknown format
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run register_none.go
With the blank import, image.DecodeConfig recognised the format, png, 2x1; without it, the same bytes gave image: unknown format. That is the kind of work init is for: cheap, local registration that cannot fail. An init function
takes no arguments, returns nothing and cannot be called, so it can report a failure only by panicking, and it runs in
every program and test binary that imports its package, needed or not. Open files, network connections and anything
that can fail belong in a function that the program calls from main and that returns an error.
Key idea
A package starts after every package it imports: its variables in dependency order, then its init functions as written, and main runs only when every package has started.
Checkpoint
Check: The order in which packages start
Name a package for what it gives its callers
A package’s name appears at every place it is used, in front of each name it exports, so it is part of every call. The Go blog’s guidance on package names comes down to a few rules: short, lower case, no under_scores or mixedCaps, a name that says what the package provides, and exported names that do not repeat it:
| Instead of | Write | Why |
|---|---|---|
util.FormatRupees |
rupees.Format |
util says nothing about what is inside, and it collects unrelated code over time |
pricing.PricingTotal |
pricing.Total |
the caller already wrote pricing |
package priority_queue |
package queue |
under_scores and mixedCaps are not used in package names |
package api, types or interfaces |
a package per responsibility, such as orders or invoice | one package of everything is imported by everything, which invites cycles |
package buf |
package bufio |
do not take a name callers want for their variables |
The standard library follows the same rules, as in http.Server, bufio.Reader and time.Now. If no short name
fits what a package does, the package probably does two things and should be split. A quick test is to write the call
site first: when pricing.Total(bill) reads like a phrase, the package and its names fit together.
Checkpoint
Check: Naming a package
Practise
Most questions use packages you have not met here, such as mail, cart or numwords, so each one asks you to apply a rule rather than recall the kirana example. The session opens with the idea your earlier answers mark as weakest:
Practice that adapts to you
Practise this lesson
14 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher): What makes a name in a Go package exported?
Its first character is an upper-case letter and it is declared at the package level, or it is the name of a field or a method. Total in package pricing is exported and other packages call it as pricing.Total; tax and a field named pricePaise are not, so only the files of the pricing folder can use them. There is no public or private keyword, and a folder named internal adds a separate rule about which packages may import it.
Two longer questions of this module build on this lesson:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
Your module has a package named helpers, meant only for its own commands. Another team now imports it and complains whenever you change it. What would you have done differently, and how do you fix it now without breaking them overnight?
-
Fresher to mid
A package opens its database connection in an init function and panics when the database is down. Since then, the unit tests of unrelated packages fail on every laptop without a database. Explain why, and how you would restructure the package.
Key takeaways
- A package is the .go files of one folder, compiled together, and its import path is the module path followed by its
folder, as
go list ./...printed for kirana. - A name is exported when its first letter is upper case and it is declared at the package level or names a field or method; unexported names are shared only inside the package’s folder.
- A package under
internal/can be imported only by code whose path starts with the folder aboveinternal, so example.com/rival could not import kirana’s ledger. - Two packages may not import each other, directly or through others; a small interface in the package that needs it, or a third package for shared data, makes the imports go one way.
- Each package starts after the packages it imports, its variables in dependency order and then its init functions, and main runs last; init is for cheap registration that cannot fail.
- Name a package for what it provides, short and lower case, and never repeat the package name in its exported names.
Terms in this lesson
- exported name
- A name that other packages can use. A Go name is exported when its first character is an upper-case letter and it is declared at the package level, or when it names a field or a method.
- import cycle
- Packages that import one another, directly or through other packages. Go refuses to build them, so the imports of a program always form a graph without loops.
- import path
- The string an import declaration names, such as example.com/kirana/pricing: the module path followed by the package's folder in the module. Standard-library packages, such as fmt, have no module path in front.
- init function
- A function named init, with no parameters and no results, declared at the package level. It runs once, after the package's variables have their values, and no code can call it.
- internal package
- A package in or below a folder named internal. The go command lets only code whose import path starts with the path of the folder above internal import it.
References
- The Go Programming Language Specification: Packages (The Go Authors)
- The Go Programming Language Specification: Exported identifiers (The Go Authors)
- The Go Programming Language Specification: Package initialization (The Go Authors)
- The Go Programming Language Specification: Program initialization (The Go Authors)
- Command go: Internal packages (The Go Authors)
- How to Write Go Code (The Go Authors)
- Go 1.21 Release Notes: Changes to the language (The Go Authors)
- Package image: decoding and registered formats (The Go Authors)
- Package names (The Go Authors)
Report a problem with this lesson
Kept only in this browser. Your Learn progress