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'sinit()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.
| Package | Purpose | Key items |
|---|---|---|
fmt | Formatted I/O | Printf, Sprintf, Errorf, Println |
os | OS interface | Open, Create, Getenv, Exit, Args |
io | I/O primitives | Reader, Writer, ReadAll, Copy |
bufio | Buffered I/O | Scanner, NewReader, NewWriter |
strings | String operations | Contains, Split, Join, TrimSpace, ToLower |
strconv | String↔number | Atoi, Itoa, ParseFloat, FormatInt |
math | Math functions | Sqrt, Abs, Floor, Ceil, Max, Min |
math/rand | Random numbers | Intn, Float64, Shuffle |
time | Time & duration | Now, Since, Sleep, Format, Parse |
net/http | HTTP client/server | Get, Post, ListenAndServe, HandleFunc |
encoding/json | JSON | Marshal, Unmarshal, NewEncoder, NewDecoder |
errors | Error handling | New, Is, As, Unwrap |
sort | Sorting | Ints, Strings, Slice, Search |
sync | Concurrency | Mutex, RWMutex, WaitGroup, Once |
context | Cancellation | Background, WithTimeout, WithCancel |
testing | Unit tests | T, B, Run, Errorf, Fatal |
log | Logging | Println, Printf, Fatal, Fatalf |
regexp | Regular expressions | MustCompile, MatchString, FindString |
path/filepath | File paths | Join, Dir, Base, Glob |
Visibility Summary
| Name | Scope |
|---|---|
ExportedName | Visible anywhere that imports the package |
unexportedName | Visible only within the same package |
internal/pkg | Visible only within the module's parent tree |
Gotchas
| Gotcha | Detail |
|---|---|
| Unused imports | Compile error — remove or use _ alias |
| Circular imports | Not allowed — two packages cannot import each other. Fix by extracting shared types into a third package |
| Package name ≠ directory name | Import path uses directory; code uses the package declaration. By convention they match, but not always (package main can be in any directory) |
Commit go.sum | Without go.sum, builds on other machines may pull different versions |
init() is implicit | Cannot be called manually — don't put logic that needs testing in init() |
internal/ enforcement | Trying to import an internal package from outside the module tree fails at compile time |