Foundations Updated 2026-09 View as Markdown

Packages and Modules

How Go code is organised, why a capital letter is the only access modifier you get, and what go.mod actually does.

Every Go file starts by declaring its package:

go
package main

A package is a directory. Every .go file in that directory must declare the same package name, and together they form one unit — files in the same package can use each other's identifiers with no import.

package main is special: it produces an executable, and needs a func main().

Exported means capitalised#

Go's only access control is the case of the first letter:

go
package pricing

const TaxRate = 0.2          // exported — usable as pricing.TaxRate
const baseMargin = 0.15      // unexported — package-private

func Total(items []Item) int { }    // exported
func applyMargin(n int) int { }     // unexported

type Item struct {
    SKU      string          // exported field
    costCents int            // unexported field
}

That is the whole mechanism. No public, no private. It applies to constants, variables, functions, types, methods and struct fields alike.

The consequence worth knowing early: an unexported field is invisible to encoding/json, so a struct field you forgot to capitalise silently disappears from your API responses.

Modules#

A module is a collection of packages versioned together, defined by go.mod at its root:

shell
go mod init github.com/you/myapp
go.mod
module github.com/you/myapp

go 1.23

require (
    github.com/google/uuid v1.6.0
    golang.org/x/sync v0.8.0
)

The module path is the import prefix for everything inside it. A package in internal/pricing/ is imported as github.com/you/myapp/internal/pricing.

shell
go get github.com/google/uuid    # add a dependency
go mod tidy                      # add what is used, remove what is not
go mod why github.com/x/y        # why is this in my graph?

go.sum records a cryptographic hash of every module version, verified on every build. Commit both files.

Imports#

go
import (
    "fmt"                                  // standard library
    "net/http"

    "github.com/google/uuid"               // third party

    "github.com/you/myapp/internal/pricing" // your own
)

gofmt groups and sorts these. Two forms worth knowing:

go
import mrand "math/rand"       // alias, to disambiguate
import _ "github.com/lib/pq"   // blank — for side effects only (driver registration)

An unused import is a compile error, not a warning. That is a small thing that keeps generated Go tidy in a way other languages are not.

The internal directory#

A package under a directory named internal can only be imported by code within the same module subtree:

github.com/you/myapp/
  internal/pricing/     <- importable only inside myapp
  pkg/client/           <- importable by anyone

This is enforced by the compiler and is the right default: put everything in internal/ unless you intend other projects to import it. Once something is publicly importable you own its API.

A standard layout#

myapp/
  go.mod
  go.sum
  cmd/
    server/main.go        # one directory per binary
    worker/main.go
  internal/
    pricing/             # domain logic
      pricing.go
      pricing_test.go
    storage/             # database access
    httpapi/             # handlers
  pkg/                   # only if genuinely reusable by others

cmd/ holds thin main packages that wire things together; the real code lives in internal/. For a small tool, a single main.go at the root is perfectly fine — do not build this structure before you need it.

init and package state#

go
var registry = make(map[string]Handler)

func init() {
    registry["default"] = defaultHandler
}

init() runs once, after package-level variables are initialised, before main. A package can have several, and they run in file order.

Use it rarely. Initialisation order across packages is hard to reason about, init cannot return an error, and it makes testing awkward. An explicit constructor called from main is almost always better.

Naming#

Go's naming conventions are unusually strong, and following them makes code look native:

  • Package names are short, lowercase, single words: http, pricing, strconv. No underscores, no camelCase, no plurals.
  • Do not stutter. In package pricing, name the function Total, not PricingTotal — callers write pricing.Total.
  • Short names for short scopes. i, r, w, err are idiomatic. Long descriptive names are for package-level identifiers.
  • Interfaces that hold one method end in -er: Reader, Writer, Stringer.

Exercise#

go
// Sketch the layout for a URL-shortener module `github.com/you/shortly`:
//   - a binary at cmd/shortly
//   - domain logic (creating and resolving short codes) that other repos must NOT import
//   - a storage package, also private
//   - one exported type and one unexported helper in the domain package
// Write the go.mod, and the package + import lines for each file.

Common questions#

internal/ or pkg/?#

internal/ by default. It is compiler-enforced privacy, and it means you can refactor freely because nothing outside your module can depend on it. Move something to pkg/ only when you have decided to support it as a public API.

Why is an unused import an error rather than a warning?#

Deliberate strictness — it keeps builds clean and stops dead imports accumulating. It is occasionally annoying while debugging, which is what the blank identifier _ is for as a temporary measure.

Should I use init()?#

Rarely. It cannot return an error, its ordering across packages is subtle, and it makes tests harder to isolate. Prefer an explicit New... constructor called from main, where failures can be handled.

Get the Go agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for Go. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.