CHAPTER 18 · ORGANIZING CODE

Packages & Modules

A package is how Go groups related code. A module is how Go groups related packages into a single shippable unit with versioned dependencies. You touched both in Chapter 4; this chapter goes deep.

Learning objectives

  • Organize code into packages and import them.
  • Export names via capitalization, and understand why.
  • Use init functions correctly (and sparingly).
  • Create a module with go mod init, add dependencies, and understand go.sum.
  • Know the role of internal/ and vendor/.

Packages

A package is a directory of .go files that all share the same package X declaration at the top. There are two kinds:

  • package main: produces an executable.
  • Any other name: produces a library, importable by others.

Typical project layout:

myapp/
├── go.mod
├── go.sum
├── main.go                 ← package main
├── cmd/
│   └── tool/
│       └── main.go         ← second binary
├── internal/
│   └── store/
│       └── store.go        ← package store (internal)
└── pkg/
    └── util/
        └── util.go         ← package util (public)

Exports, capitalization is the rule

A name (function, type, variable, constant, method, field) is exported (visible outside the package) iff its first letter is uppercase:

package mathutil

func Add(a, b int) int { return a + b }     // exported
func sub(a, b int) int { return a - b }     // unexported

From another package: mathutil.Add(1, 2) works, mathutil.sub(1, 2) is a compile error.

main and init

main() is the program's entry point, in package main only.

init() runs automatically when a package is loaded, before main. Use it to initialize package-level state or register things:

package store

import "database/sql"
import _ "github.com/lib/pq"    // side-effect import: registers the driver

var db *sql.DB

func init() {
    // runs once, before any exported function is called
    db = openDB()
}

Multiple files in a package can each have their own init; they all run (in file name order). Use init sparingly; implicit magic makes tests harder and dependencies muddier.

Imports

import (
    "fmt"
    "strings"

    "github.com/user/project/internal/store"

    . "math"                    // dot import, makes names accessible unqualified (avoid)
    log "log/slog"              // alias import
    _ "image/png"               // side-effect only, registers format
)

Go groups imports into standard library first, then third party, separated by a blank line. goimports (Chapter 4) does this for you automatically.

Modules, go.mod

A module is declared by a go.mod file at its root:

module github.com/cyrus2281/learn-go

go 1.22

require (
    github.com/lib/pq v1.10.9
    golang.org/x/text v0.14.0
)

Four things are encoded:

  1. module path: the canonical import path consumers use.
  2. go version: minimum Go version.
  3. require: direct dependencies and their versions.
  4. go.sum (sibling file), cryptographic hashes of every dependency for reproducibility.

Adding a dependency

go get github.com/lib/pq@latest   # adds/upgrades to latest
go mod tidy                        # remove unused, fill in missing

Or just import and run go build: Go will fetch the dependency automatically and update go.mod.

To upgrade one dep:

go get github.com/lib/pq@v1.11.0

Semantic versioning

Go modules require semver-style tags: v1.2.3.

  • v0.x: "anything goes", used for early development.
  • v1.x: stable API; minor/patch bumps don't break.
  • v2+: major version bump changes the module path: github.com/user/lib/v2. This lets different major versions coexist.

internal/ and vendor/

internal/ is magic: packages under an internal/ directory can only be imported by code in the module that contains it. Great for hiding implementation details even in an open-source project.

vendor/ stores copies of your dependencies in the repo. go mod vendor creates it. Uncommon in modern Go, module proxy caching makes it mostly unnecessary. Still used when you need reproducible builds without network access.

Workspaces (go work)

When you're working on multiple modules at once (e.g. a library and an app that uses it, both in local directories), a workspace lets Go resolve imports to your local copies without replace directives:

go work init ./myapp ./mylib
# creates go.work at the parent level

A go.work file lives outside version control, your workspace choice is personal.

Check your understanding

Practice exercises

EXERCISE 1

Split a hello world into two packages

Take the Hello World from Chapter 4. Move the greeting logic into a new package greet (in a greet/ subdirectory) with an exported Hello(name string) string function. Import and call it from main.

Show solution
myapp/
├── go.mod          (module example.com/myapp)
├── main.go
└── greet/
    └── greet.go

greet/greet.go:

package greet

import "fmt"

func Hello(name string) string {
    return fmt.Sprintf("Hello, %s!", name)
}

main.go:

package main

import (
    "fmt"

    "example.com/myapp/greet"
)

func main() {
    fmt.Println(greet.Hello("World"))
}

Further reading

Code organized and shippable.