golang · level 7

Packages & Modules

go.mod, internal/, exported names, the toolchain.

100 XP

Packages & Modules

Go's unit of compilation is the package. A directory is a package. A repo full of packages is a module, declared by a go.mod at the root.

Package basics

Every Go file starts with a package declaration:

package auth

import (
    "errors"
    "fmt"
)

func Login(name, password string) (string, error) {
    if name == "" || password == "" {
        return "", errors.New("missing credentials")
    }
    return fmt.Sprintf("session:%s", name), nil
}

All files in the same directory must declare the same package name. The package name is what consumers import:

import "myapp/auth"

token, err := auth.Login("alice", "...")

Exported vs unexported

Identifiers starting with a capital letter are exported (visible to other packages). Lowercase = package-private.

package auth

func Login(...) (...)        // exported
func validate(...) (...)     // unexported — only auth/*.go can see it

type User struct { ... }     // exported type
type session struct { ... }  // unexported type

No public/private keywords. Visibility is encoded in the name.

Importing

import (
    "fmt"                            // stdlib
    "net/http"                       // stdlib subpackage
    "github.com/google/go-cmp/cmp"   // third-party
    "myapp/auth"                     // local

    log "github.com/sirupsen/logrus" // alias the import
    _ "github.com/lib/pq"            // import for side effects (e.g. driver register)
)

The blank import (_) runs the package's init() for its side effects without binding any names. SQL drivers and image codecs use this pattern.

Modules and go.mod

A module is a directory tree with a go.mod at the root:

// go.mod
module github.com/myname/myapp

go 1.22

require (
    github.com/google/go-cmp v0.6.0
    github.com/lib/pq v1.10.9
)

go.mod is the manifest: module path + Go version + dependencies. go.sum is a checksum database that pins exact versions for reproducible builds.

Common workflow

go mod init github.com/myname/myapp     # create go.mod
go get github.com/lib/pq                # add a dependency
go get -u                               # upgrade all
go mod tidy                             # add missing, drop unused
go build ./...                          # build every package
go test ./...                           # test every package
go install                              # build + install the binary

./... is shorthand for "the current dir and every subdirectory" — the canonical way to operate on a whole module.

Internal packages

A package under any path containing a directory named internal/ can only be imported by packages rooted at the parent of that internal/:

myapp/
    cmd/server/
    internal/
        secrets/        # only myapp/* can import this
    pkg/
        public/         # anyone can import this

It's the closest Go gets to enforced visibility scoping above the package level.

main package

The package named main produces an executable. It must have a main() function:

package main

import "fmt"

func main() {
    fmt.Println("hello")
}

By convention these live under cmd/<name>/main.go so a single repo can ship multiple binaries.

init() once per file

Every file may have a func init() that runs once when the package is loaded — before main(). Multiple init() functions across the package run in undefined order, so don't depend on order between them. Use sparingly — explicit setup at the call site is easier to test.

Don't import "main" or cycles

main cannot be imported by other packages. And Go forbids import cycles entirely — A imports B imports A is a compile error. Cycles usually point at a bad split: one of the things should live in a third package both depend on, or you should merge them.