πŸ“¦ Chapter 13 of 20

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.

⏱ β€”πŸ“Š Intermediate

🎯 Learning Objectives

βœ… After this chapter you will:
  • Understand what a package is and how package naming works
  • Control visibility with exported vs unexported identifiers
  • Use init() functions correctly
  • Work with internal packages for controlled access
  • Write and publish a Go module
  • Use replace directives 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.

text
myproject/
β”œβ”€β”€ go.mod
β”œβ”€β”€ main.go           // package main (entry point)
└── math/
    β”œβ”€β”€ math.go       // package math
    └── math_test.go  // package math_test (external test package)
go
// 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)
go
// 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.

go
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)
go
// 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
)
⚠️ Use init() Sparingly

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/.

text
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.

shell
# 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

PackageKey Types/FunctionsUse for
fmtPrintln, Printf, Sprintf, Fprintf, ErrorfFormatted I/O
osOpen, Create, ReadFile, WriteFile, Getenv, ArgsFile system, environment
ioReader, Writer, Copy, ReadAll, PipeI/O abstractions
bufioScanner, Reader, WriterBuffered I/O
stringsContains, Split, Join, Replace, Builder, NewReaderString manipulation
strconvAtoi, Itoa, ParseFloat, FormatBoolType conversion
encoding/jsonMarshal, Unmarshal, Encoder, DecoderJSON handling
net/httpGet, Post, ListenAndServe, Handler, ServeMuxHTTP client/server
contextBackground, WithTimeout, WithCancel, WithValueCancellation, deadlines
syncMutex, RWMutex, WaitGroup, Once, MapSynchronisation
timeNow, Since, After, Timer, Ticker, DurationTime handling
mathSqrt, Abs, Min, Max, Inf, NaN, PiMath functions
sortSlice, Strings, Ints, SearchSorting
regexpMustCompile, MatchString, FindAllStringRegular expressions
logPrint, Fatal, Panic, SetFlags, NewLogging

🎀 Interview Questions

1
How does Go control visibility? What's the rule?
β–Ά
Detailed Answer

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.

2
What is the init() function? When does it run?
β–Ά
Detailed Answer

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

🧩
Packages & Modules
4 questions
1. How is a Go identifier made publicly accessible (exported)?
2. What does placing packages in an internal/ directory do?
3. Which statement about init() is TRUE?
4. What does import _ "github.com/lib/pq" do?
β€”

πŸ“Œ Cheat Sheet

πŸ“Œ Packages & Modules
Exported
Uppercase first letter
Unexported
lowercase first letter
Import
import "module/path/pkg"
Alias
import mypkg "module/path"
Side-effect
import _ "pkg" // runs init only
Internal
internal/ // restricted access
init()
func init() { } // auto-runs on import
Module init
go mod init module/path
Add dep
go get pkg@version
Remove unused
go mod tidy
Local replace
replace mod => ../local
Workspace
go work init ./a ./b