Recommended resources
The freely available resources this lesson will direct you to.
Hello! Last lesson, you considered which errors a function should let its callers inspect. Packages pose a related design question: which names and dependencies should other code be allowed to rely on?
This is the final lesson in Go foundations. You’ll organize a small module into packages, choose boundaries that make sense to callers, and fix an import cycle. Allow about 40 minutes for the video, readings, and code walkthrough.
A package is a boundary
A Go module can contain several packages. Typically, one directory contains one package, and the files in that directory share a package declaration. A package’s import path identifies where to find it; its package name is the qualifier you use in code.
For example, if go.mod declares module example.com/journal, code in a directory named note can be imported as "example.com/journal/note". If that directory declares package note, a caller writes note.Normalize(...). The path and name serve different purposes, though the final path element conventionally matches the package name.
Watch the opening portions of Understanding Packages, Exports, and Imports in Go with Code Examples from Code & Learn. Focus on the distinction between a package, its import path, and the names it exposes.
Understanding Packages, Exports, and Imports in Go with Code Examples
This demonstration builds a small package and then imports it from another. Watch it to see Go's package rules applied to files rather than just described.
Watch packages and exports for package declarations and capitalized exported names. Then watch imports and naming for module-based import paths and the convention of matching package and directory names. Notice that an import belongs to the source file using it.
An identifier beginning with an uppercase letter is exported from its package; one beginning with a lowercase letter remains available within that package, including across its files. Exporting is therefore a decision about what callers can use, not merely a spelling convention.
Choose a responsibility, then choose a name
A useful package has a purpose you can explain without listing everything it happens to contain. Its name should also work as a prefix at call sites: note.Normalize says more than util.NormalizeNote, without repeating “note” in both names.
Read Sameer Ajmani’s Package names on the Go Blog. Pay particular attention to the advice about designing names from the caller’s point of view.
The Go Blog connects package naming to package design: a vague name can be a sign that unrelated responsibilities have been grouped together.
In “Package names,” read the naming guidance. In “Naming package contents,” read the caller's view. Finally, in “Bad package names,” read the warning signs, especially the discussion of util, common, and catch-all API packages.
Cohesion does not mean creating a new directory for every function. Several files can form one package if they serve the same responsibility. Conversely, placing unrelated functions in util hides what belongs together and makes that package an increasingly tempting dependency for everything else.
Read the relevant portions of Andrew Gerrand’s Organizing Go code for the other side of that judgment: splitting too aggressively also has a cost.
Organizing Go code - The Go Programming Language
This Go Blog article explains why both oversized grab-bag packages and excessive splitting make code harder to maintain.
In “Minimize the exported interface,” begin at the export advice. Continue into “What to put into a package,” ending at the granularity discussion. You can stop there; the article's older discussion of GOPATH-based layouts is not needed for this module-based example.
A practical test is to look at the calling code. Do the package name and exported names read clearly together? Do the declarations in the package serve one recognizable responsibility? If not, reconsider the boundary before adding another exported name.
Import cycles reveal a boundary problem
Suppose a note package provides a Create function that calls storage.Save. The storage package, meanwhile, calls note.Normalize before writing. Each package now imports the other. Go rejects this arrangement, regardless of whether those functions would call each other at runtime: package imports themselves cannot form a cycle.
The diagram below is a useful caution. Both sides show cycles. The three-package example on the right is still cyclic, even though no two packages directly depend on each other in both directions.

To remove the note–storage cycle, decide who should coordinate the two operations. Here, the executable can normalize a note and then ask storage to save it. Neither supporting package needs to know about the other.
Create this layout in a new directory, or adapt the import paths to match an existing module:
journal/
go.mod
cmd/journal/main.go
note/note.go
storage/storage.go
From journal/, run go mod init example.com/journal. Then put the following in note/note.go:
package note
import (
"errors"
"strings"
)
var ErrEmpty = errors.New("empty note")
func Normalize(raw string) (string, error) {
text := strings.TrimSpace(raw)
if text == "" {
return "", ErrEmpty
}
return text, nil
}
note owns the rule for acceptable note text. It exports Normalize for callers and ErrEmpty for callers that need to recognize this condition, as you learned in the previous lesson. It has no reason to import storage.
Put this in storage/storage.go:
package storage
import (
"fmt"
"os"
)
func Save(path, text string) error {
if err := os.WriteFile(path, []byte(text+"\n"), 0o600); err != nil {
return fmt.Errorf("save note: %w", err)
}
return nil
}
storage writes the text it receives. Its contract assumes the caller has decided what to write; it need not know the rules for normalizing notes. Here, Save replaces the file at path if it exists.
Finally, cmd/journal/main.go coordinates the two packages:
package main
import (
"fmt"
"os"
"example.com/journal/note"
"example.com/journal/storage"
)
func main() {
text, err := note.Normalize(" meeting notes ")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if err := storage.Save("note.txt", text); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
From the module root, run:
go list ./...
go build ./...
go test ./...
go run ./cmd/journal
go list ./... shows the packages the module contains. go build ./... checks that they compile, including their imports; go test ./... checks them even if you have not added tests yet. The final command runs the executable and creates note.txt.
The important change is not the directory layout by itself. It is the allocation of responsibility: note handles note text, storage handles writing, and main knows how this program uses both.
Repair the dependency, not just the compiler error
When Go reports an import cycle not allowed error, trace the imports until you return to a package you have already visited. Then ask why each package needs the other:
- If the code serves one responsibility, consider keeping it in one package. Files within that package can share unexported helpers without importing each other.
- If one package is doing coordination rather than its own job, move that coordination to a caller, as
maindoes above. - If both packages genuinely need a small shared concept, consider placing that concept in a focused package they can both import. Avoid creating a generic
commonpackage just to silence the error.
Do not assume that moving a file to a deeper directory fixes a cycle. Go follows imports, not the visual nesting of folders. A clear import structure is a consequence of clear responsibilities.
Takeaways
A package groups code around a cohesive responsibility and gives callers a deliberately limited set of exported names. Its import path locates it; its package name makes its API readable at call sites. Import cycles are forbidden, including indirect cycles, and usually signal that responsibilities or coordination need to move—not that you need more folders.
In the next module, you’ll model data with structs. Those types will give you another reason to decide carefully which package owns a concept and which names it should expose.