Back to Notes

Go Packages & Modules

Overview

Go code is organized into packages (a directory of .go files that share a package declaration) and modules (a collection of related packages defined by a go.mod file). Every file belongs to a package. Packages are the unit of code reuse; modules are the unit of versioning and distribution.


Packages

package declaration

Every .go file starts with a package declaration. All files in the same directory must use the same package name.

package main    // executable — must have func main()
package utils   // library — imported by other packages
package http    // convention: package name = directory name

The main Package — Entry Point

The main package is special: it's the only package that compiles to an executable. It must contain exactly one func main().

// main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello, Go!")
}
go run main.go    # run without building
go build          # compile to binary (./main or ./myapp)
./myapp           # run the binary

Exported vs Unexported Names

Exported names start with a capital letter — they are accessible outside the package. Lowercase names are package-private (unexported).

package mymath

// Exported — visible to any package that imports mymath
func Add(a, b int) int  { return a + b }
const Pi = 3.14159
var MaxRetries = 5

type Calculator struct {
    Memory float64   // exported field
    cache  float64   // unexported field — package-private
}

// Unexported — only usable within mymath
func helper(x int) int { return x * 2 }
var internal = "secret"
// In another package
import "mymath"

mymath.Add(1, 2)    // ✅
mymath.Pi           // ✅
mymath.helper()     // ❌ compile error: unexported

Imports

import brings other packages into scope. All imported packages must be used — unused imports are compile errors.

Single import

import "fmt"

Factored import — preferred for multiple packages

import (
    "fmt"
    "math"
    "net/http"
    "strings"
)

Import path vs package name

The import path is the directory path. The package name (how you use it in code) is the package declaration inside that directory — usually they match, but not always.

import "math/rand"   // import path
rand.Intn(100)       // package name: rand (from package rand declaration)

import "crypto/rand" // different package, same short name

Aliasing imports

import (
    mrand "math/rand"     // alias to disambiguate
    crand "crypto/rand"
    _ "image/png"         // blank import — runs init(), no name in scope
    . "fmt"               // dot import — use Println directly (avoid — confusing)
)

Modules

A module is a tree of Go packages rooted at a go.mod file. Modules replaced $GOPATH in Go 1.11 and are the standard way to manage dependencies today.

go.mod — the module manifest

module github.com/vatsal/myapp   ← module path (also your import prefix)

go 1.21                          ← minimum Go version

require (
    github.com/gin-gonic/gin v1.9.1
    github.com/lib/pq        v1.10.7
)

go.sum — cryptographic checksums

Auto-generated. Contains the expected hash of every dependency. Always commit go.sum — it ensures reproducible builds.

github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPpmClSfM...

Module Commands

go mod init <module-path>    # create go.mod — do this once per project
go mod tidy                  # add missing deps, remove unused deps
go get <pkg>@latest          # add or upgrade a dependency
go get <pkg>@v1.2.3          # pin to a specific version
go get <pkg>@none            # remove a dependency
go mod download              # download all deps to local cache
go mod verify                # verify checksums in go.sum
go list -m all               # list all dependencies (direct + indirect)
go mod graph                 # print dependency graph

Project Structure

A well-organized Go project looks like this:

myapp/
├── go.mod              ← module definition
├── go.sum              ← dependency checksums (commit this)
├── main.go             ← package main, entry point
├── cmd/
│   └── server/
│       └── main.go     ← alternative entry point (multiple binaries)
├── internal/           ← code only importable within this module
│   ├── auth/
│   │   └── auth.go     ← package auth
│   └── db/
│       └── db.go       ← package db
└── pkg/                ← code intended for external use
    └── utils/
        └── utils.go    ← package utils

Importing your own packages

// main.go
import (
    "github.com/vatsal/myapp/internal/auth"
    "github.com/vatsal/myapp/pkg/utils"
)

auth.Login(...)
utils.FormatDate(...)

internal/ — Enforced Package Privacy

Any package inside an internal/ directory can only be imported by code in the parent directory tree. The Go compiler enforces this — external modules cannot import internal packages.

myapp/
├── internal/
│   └── secret/      ← only importable by packages inside myapp/
└── main.go          ✅ can import myapp/internal/secret

init() Function

Each package can declare one or more init() functions. They run automatically before main(), after all variable initialisations in the package. Used for one-time setup.

package db

import "database/sql"

var DB *sql.DB

func init() {
    var err error
    DB, err = sql.Open("postgres", "host=localhost dbname=myapp")
    if err != nil {
        panic("database init failed: " + err.Error())
    }
}

Rules:

  • init() cannot be called explicitly
  • A package can have multiple init() functions (even in the same file)
  • They run in the order they appear, files processed alphabetically
  • Blank import _ "pkg" runs the package's init() without importing names
import _ "github.com/lib/pq"   // registers the postgres driver via init()

Standard Library Highlights

Know these — they come up constantly in real Go code.

PackagePurposeKey items
fmtFormatted I/OPrintf, Sprintf, Errorf, Println
osOS interfaceOpen, Create, Getenv, Exit, Args
ioI/O primitivesReader, Writer, ReadAll, Copy
bufioBuffered I/OScanner, NewReader, NewWriter
stringsString operationsContains, Split, Join, TrimSpace, ToLower
strconvString↔numberAtoi, Itoa, ParseFloat, FormatInt
mathMath functionsSqrt, Abs, Floor, Ceil, Max, Min
math/randRandom numbersIntn, Float64, Shuffle
timeTime & durationNow, Since, Sleep, Format, Parse
net/httpHTTP client/serverGet, Post, ListenAndServe, HandleFunc
encoding/jsonJSONMarshal, Unmarshal, NewEncoder, NewDecoder
errorsError handlingNew, Is, As, Unwrap
sortSortingInts, Strings, Slice, Search
syncConcurrencyMutex, RWMutex, WaitGroup, Once
contextCancellationBackground, WithTimeout, WithCancel
testingUnit testsT, B, Run, Errorf, Fatal
logLoggingPrintln, Printf, Fatal, Fatalf
regexpRegular expressionsMustCompile, MatchString, FindString
path/filepathFile pathsJoin, Dir, Base, Glob

Visibility Summary

NameScope
ExportedNameVisible anywhere that imports the package
unexportedNameVisible only within the same package
internal/pkgVisible only within the module's parent tree

Gotchas

GotchaDetail
Unused importsCompile error — remove or use _ alias
Circular importsNot allowed — two packages cannot import each other. Fix by extracting shared types into a third package
Package name ≠ directory nameImport path uses directory; code uses the package declaration. By convention they match, but not always (package main can be in any directory)
Commit go.sumWithout go.sum, builds on other machines may pull different versions
init() is implicitCannot be called manually — don't put logic that needs testing in init()
internal/ enforcementTrying to import an internal package from outside the module tree fails at compile time