Packages & Modules
Packages are Go's unit of code organisation. Modules are the dependency management system. Understanding these deeply lets you build scalable, well-structured Go applications and publish reusable libraries.
π― Learning Objectives
- Understand what a package is and how package naming works
- Control visibility with exported vs unexported identifiers
- Use
init()functions correctly - Work with
internalpackages for controlled access - Write and publish a Go module
- Use
replacedirectives for local development - Understand Go workspace mode (go.work)
π Package Basics
Every Go file begins with a package declaration. All files in the same directory must have the same package name.
myproject/
βββ go.mod
βββ main.go // package main (entry point)
βββ math/
βββ math.go // package math
βββ math_test.go // package math_test (external test package)// math/math.go
package math
// Exported: starts with uppercase β accessible outside the package
func Add(a, b int) int {
return a + b
}
func Multiply(a, b int) int {
return multiply(a, b) // delegates to unexported function
}
// Unexported: starts with lowercase β only accessible within this package
func multiply(a, b int) int {
return a * b
}
// Exported type
type Calculator struct {
History []string // exported field
precision int // unexported field
}
// Exported constant
const Pi = 3.14159
// Exported variable (use sparingly β prefer functions/methods)
var DefaultBase = 10
// Unexported package-level variable (private state)
var cache = make(map[string]int)// main.go
package main
import (
"fmt"
"myproject/math" // import by module path + directory
)
func main() {
result := math.Add(3, 4)
fmt.Println("3 + 4 =", result) // 7
fmt.Println("Pi =", math.Pi) // 3.14159
// math.multiply(2, 3) // β compile error: multiply not exported
}π The init() Function
Each package can have one or more init() functions. They run automatically when the package is first imported, before main(). You cannot call them directly.
package database
import (
"fmt"
"os"
)
var db *DB
// init() runs automatically when this package is imported.
// Multiple init() functions are allowed (in the same file or across files).
func init() {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
dsn = "postgres://localhost/dev"
}
var err error
db, err = Connect(dsn)
if err != nil {
// Can't return error from init β must panic or handle here
panic(fmt.Sprintf("database init failed: %v", err))
}
fmt.Println("Database connected")
}
type DB struct{}
func Connect(dsn string) (*DB, error) { return &DB{}, nil }
// Package initialisation order:
// 1. Package-level variables (in declaration order)
// 2. init() functions (in source file order, depth-first for dependencies)
// 3. main() (in the main package only)// Side-effect import: import just for init() β registers a driver, plugin, etc.
import (
_ "github.com/lib/pq" // registers PostgreSQL driver
_ "github.com/mattn/go-sqlite3" // registers SQLite driver
_ "myapp/internal/plugins/csv" // registers CSV export plugin
)Overusing init() makes code hard to test and debug. The order of init execution across packages can be surprising. Prefer explicit initialisation via constructor functions or dependency injection. Reserve init() for: registering database drivers, setting up global state that genuinely cannot be deferred, and package-level validation.
π Internal Packages
The internal directory is a Go convention that enforces access control at the file system level. Packages inside internal/ can only be imported by code in the parent of internal/.
myproject/
βββ go.mod (module: github.com/user/myproject)
βββ main.go β can import internal/auth
βββ api/
β βββ handler.go β can import internal/auth
βββ internal/
βββ auth/
β βββ auth.go β package internal/auth
βββ cache/
βββ cache.go β package internal/cache
// External packages CANNOT import github.com/user/myproject/internal/auth
// Only code within myproject/ can import internal packages
// This enforces package-private APIs at the module boundaryπ’ Go Workspaces (go.work)
Go 1.18 introduced workspaces for developing multiple related modules simultaneously without publishing them.
# Create a workspace
go work init ./moduleA ./moduleB
# go.work file:
# go 1.22
# use (
# ./moduleA
# ./moduleB
# )
# Add another module
go work use ./moduleC
# go.mod replace directive (alternative for single-module)
# go.mod:
# require github.com/user/lib v1.0.0
# replace github.com/user/lib => ../local-libπ Essential Standard Library Packages
| Package | Key Types/Functions | Use for |
|---|---|---|
fmt | Println, Printf, Sprintf, Fprintf, Errorf | Formatted I/O |
os | Open, Create, ReadFile, WriteFile, Getenv, Args | File system, environment |
io | Reader, Writer, Copy, ReadAll, Pipe | I/O abstractions |
bufio | Scanner, Reader, Writer | Buffered I/O |
strings | Contains, Split, Join, Replace, Builder, NewReader | String manipulation |
strconv | Atoi, Itoa, ParseFloat, FormatBool | Type conversion |
encoding/json | Marshal, Unmarshal, Encoder, Decoder | JSON handling |
net/http | Get, Post, ListenAndServe, Handler, ServeMux | HTTP client/server |
context | Background, WithTimeout, WithCancel, WithValue | Cancellation, deadlines |
sync | Mutex, RWMutex, WaitGroup, Once, Map | Synchronisation |
time | Now, Since, After, Timer, Ticker, Duration | Time handling |
math | Sqrt, Abs, Min, Max, Inf, NaN, Pi | Math functions |
sort | Slice, Strings, Ints, Search | Sorting |
regexp | MustCompile, MatchString, FindAllString | Regular expressions |
log | Print, Fatal, Panic, SetFlags, New | Logging |
π€ Interview Questions
Go uses a simple, consistent rule: identifiers that start with an uppercase letter are exported (public) and accessible outside the package. Identifiers starting with a lowercase letter are unexported (private/package-internal).
This applies to: functions, methods, types, struct fields, constants, and variables. There is no public/private/protected keyword β just the capitalisation rule.
The visibility boundary is the package, not the file. Multiple files in the same package can access each other's unexported identifiers. Files in different packages cannot access unexported identifiers even within the same module (except via internal packages).
The internal/ directory provides additional access control: packages within internal/ can only be imported by code in the parent directory tree.
init() function? When does it run?init() is a special function that runs automatically when a package is first imported, before main(). You cannot call it manually or reference it. Multiple init() functions can exist in the same package (even in the same file).
Execution order: (1) Package-level variable declarations in declaration order; (2) init() functions in source file order; (3) Dependencies run their init before the importing package.
Use cases: registering database drivers (_ "github.com/lib/pq"), setting up global state, running package-level checks, seeding random number generators.
Gotchas: init() failures must panic (can't return errors); init() order across packages can be hard to reason about; init() makes packages harder to test in isolation.
π§ͺ Quiz
internal/ directory do?init() is TRUE?import _ "github.com/lib/pq" do?π Cheat Sheet
Uppercase first letterlowercase first letterimport "module/path/pkg"import mypkg "module/path"import _ "pkg" // runs init onlyinternal/ // restricted accessfunc init() { } // auto-runs on importgo mod init module/pathgo get pkg@versiongo mod tidyreplace mod => ../localgo work init ./a ./b