Package design
Organise Go code into packages — naming, exported vs unexported identifiers, init(), and avoiding circular imports.
- Apply Go's naming conventions for packages
- Distinguish exported (uppercase) from unexported (lowercase) identifiers
- Explain what init() does and when to use it
- Describe what a circular import is and how to avoid it
- Sketch a typical Go project layout
Go organises code into packages — the unit of compilation, namespace, and reuse. A package is just a directory of .go files that all share the same package declaration at the top. Understanding how packages work shapes every architectural decision you make in Go.
Package naming
A package name should be:
- A short, lowercase, single word:
store,user,http,parser. - Descriptive of what it provides, not what it does to something else.
- Not a generic catch-all like
util,common, orhelpers.
The name appears at every call site — store.Get(id) reads cleanly, utilities.GetUser(id) does not. When callers use your package name as a qualifier, shorter and more specific is better.
// Good: one concept per package
package store // knows about persistence
// Avoid: mixing unrelated things
package utils // nobody knows what to expect hereExported vs unexported identifiers
Go's visibility mechanism is capitalisation, and it applies to everything: types, functions, variables, constants, struct fields, and interface methods.
package store
// Exported — callers in other packages can use these
type User struct {
ID int
Name string
email string // unexported — hidden from other packages
}
// Exported function
func Get(id int) (*User, error) { ... }
// Unexported helper — internal use only
func validate(u *User) bool { ... }Unexported identifiers are package-private. There is no public/private/protected — the single rule (uppercase = exported, lowercase = unexported) covers everything.
A struct field being unexported does not make the struct immutable. You can still provide exported methods that read or write the field. This is how you enforce invariants: the constructor (exported function) validates the value, the field stays unexported, and mutation happens only through methods that re-validate.
init()
Each package can declare one or more init() functions. They run automatically when the package is first imported, after all variable initialisations:
package store
var defaultTimeout time.Duration
func init() {
// Runs once, when the package is imported
defaultTimeout = 30 * time.Second
}Use init() sparingly. It runs hidden from callers and makes code harder to test in isolation. Prefer explicit initialisation through constructors or New… functions. Legitimate uses include registering drivers (database/sql), seeding random number generators, and self-registering plugins.
Avoid side effects in init() that depend on external state (environment variables, files, network). If initialisation can fail, it cannot return an error from init() — the only option is to panic, which is rarely correct at package load time.
Circular imports
Go prohibits circular imports at the compiler level. If package a imports b and b imports a, the build fails. This is a deliberate constraint: it keeps the dependency graph a DAG and prevents tangled codebases.
When you find yourself with a circular dependency, the usual fix is to extract the shared type or interface into a third package that both sides import:
Before: user ↔ store (circular)
After: user → types ← store (types holds the shared interface)The other common fix is to pass a function or interface as a parameter rather than importing the package that provides it — dependency injection over direct import.
A typical project layout
myapp/
├── go.mod
├── main.go // or cmd/myapp/main.go
├── internal/ // unexported to outside modules
│ └── store/
│ └── store.go
├── user/
│ └── user.go
└── config/
└── config.goThe internal/ directory is special: Go only allows packages inside it to be imported by code rooted at the parent directory. It is the standard way to share code within a module without making it a public API.
Check your understanding
Knowledge check
- 1.Which package name best follows Go conventions for a package that manages database connections?
- 2.Go allows circular imports as long as the cycle is not deeper than two packages.
- 3.You place code in myapp/internal/cache/. Which package can import it?
Do it yourself
Create a minimal two-package project: a greet package with an exported Hello(name string) string function and a main.go that imports and calls it.
mkdir -p hello/greet
# write greet/greet.go and main.go
go run .Where to go next
Now that you understand how packages are structured, the next lesson covers Go modules — how the go.mod and go.sum files manage dependencies and versioning across a project.