12. Modules & Packages
📋 Jump to Takeaways🎁 Every language has a story about how it organizes code. Some use header files, others rely on fragile relative imports. Go sidesteps all of that with a module system that's explicit, flat, and refreshingly predictable. If you can name a directory, you can structure a project.
Initializing a Module with go mod init
A module is the top-level unit of code distribution in Go. You create one with a single command.
go mod init github.com/yourname/myprojectThis generates a go.mod file that declares your module path and the Go version you're targeting. Every Go project starts here — no exceptions.
module github.com/yourname/myproject
go 1.22The module path is how other packages import your code. For personal projects, use your repo URL. For local experiments, even go mod init myapp works.
Package Declaration
Every .go file begins with a package declaration. This tells the compiler which package the file belongs to.
package main
import "fmt"
func main() {
fmt.Println("Hello from package main")
}
// Output: Hello from package mainThe main package is special — it's the entry point for executables. Library code uses any other name.
Package vs Directory
In Go, a package is a directory. All .go files in the same directory must declare the same package name. The package name typically matches the directory name.
myproject/
├── go.mod
├── main.go // package main
└── math/
└── calc.go // package math// math/calc.go
package math
func Add(a, b int) int {
return a + b
}You import by directory path, not by file name. One directory = one package. No mixing allowed.
Exported vs Unexported Names
Go uses a beautifully simple rule: if a name starts with a capital letter, it's exported (public). Lowercase means unexported (private to the package).
package user
// Exported — accessible from other packages
func Validate(name string) bool {
return len(name) > 0
}
// unexported — only visible within this package
func sanitize(name string) string {
return strings.TrimSpace(name)
}No keywords like public or private. Just capitalization. You'll internalize this fast.
Importing Packages
You import packages by their full module path. Go's tooling handles the rest.
package main
import (
"fmt"
"github.com/yourname/myproject/math"
)
func main() {
result := math.Add(3, 7)
fmt.Println(result)
}
// Output: 10Grouped imports use parentheses. The compiler enforces that you use every import — unused imports are a compile error, not a warning.
Internal Packages
Go has a built-in mechanism for code you want to share between your own packages but hide from external consumers. Place it in a directory named internal.
myproject/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ └── auth/
│ └── token.go // only importable within myproject
└── go.modAnything under internal/ can only be imported by code rooted at the parent of internal. The compiler enforces this — no configuration needed.
go.mod and go.sum
Your go.mod file tracks your module's dependencies and their minimum versions. When you add a dependency, Go also generates go.sum, which contains cryptographic hashes to verify integrity.
// go.mod
module github.com/yourname/myproject
go 1.22
require github.com/gin-gonic/gin v1.9.1Commit both files to version control. go.mod declares what you need; go.sum ensures reproducible builds.
Adding Dependencies with go get
To add an external package, use go get:
go get github.com/gin-gonic/gin@latestThis downloads the package, updates go.mod, and records checksums in go.sum. You can pin specific versions by replacing @latest with a tag like @v1.9.1.
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "hello"})
})
r.Run() // listens on :8080
}The Standard Library Tour
Go ships with a rich standard library. You'll reach for these constantly:
import (
"fmt" // formatted I/O (Println, Sprintf, Errorf)
"os" // file system, environment variables, exit
"io" // Reader/Writer interfaces, Copy, ReadAll
"net/http" // HTTP client and server
"encoding/json" // JSON marshal/unmarshal
"strings" // string manipulation (Contains, Split, Replace)
"strconv" // string ↔ number conversion (Atoi, Itoa)
)package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
n, _ := strconv.Atoi("42")
upper := strings.ToUpper("hello")
fmt.Println(n, upper)
}
// Output: 42 HELLOThe standard library covers most needs. Reaching for third-party packages is a conscious choice, not a necessity.
Multi-File Packages
A package can span multiple files. All files in the same directory share access to each other's unexported names.
utils/
├── strings.go // package utils
└── numbers.go // package utils// utils/strings.go
package utils
func Capitalize(s string) string {
return strings.ToUpper(s[:1]) + s[1:]
}
// utils/numbers.go
package utils
func Double(n int) int {
return n * 2
}Split packages by logical concern, not by arbitrary line counts. Both functions are importable as utils.Capitalize and utils.Double.
The init() Function
Each file can define an init() function that runs automatically before main(). It takes no arguments and returns nothing.
package main
import "fmt"
var config string
func init() {
config = "loaded"
fmt.Println("init ran first")
}
func main() {
fmt.Println("main:", config)
}
// Output:
// init ran first
// main: loadedUse init() sparingly — for registering drivers, validating configuration, or setting up package-level state. Multiple init() functions in the same package execute in source file order, but relying on that order is fragile.
Organizing a Project
The Go community has settled on conventions for structuring larger projects:
myproject/
├── cmd/
│ ├── api/
│ │ └── main.go // builds the API binary
│ └── worker/
│ └── main.go // builds the worker binary
├── internal/
│ ├── auth/ // private business logic
│ └── database/
├── pkg/
│ └── validator/ // reusable library code (public)
├── go.mod
└── go.sum- cmd/ — each subdirectory is an executable with its own
mainpackage - internal/ — private application code, not importable by external modules
- pkg/ — library code you intentionally expose for other projects to import
For small projects, a flat structure is perfectly fine. Adopt these conventions when your codebase outgrows a single directory.
Key Takeaways
- Every project starts with
go mod init— this creates yourgo.mod - One directory = one package; the package name matches the directory
- Capital letter = exported; lowercase = unexported — no keywords needed
go.sumensures reproducible, verifiable builds — commit it- Use
go getto add dependencies with explicit versioning - The standard library (
fmt,os,io,net/http,encoding/json,strings,strconv) handles most tasks - Multiple files in a directory share the same package scope
init()runs beforemain()— use it for setup, not business logic- Structure larger projects with
cmd/,internal/, andpkg/conventions internal/enforces encapsulation at the compiler level
🎁 You now know how to organize code into packages and modules. But what happens when a function becomes a value — something you can pass around, return, and even let carry its own state? Next up: closures, where functions remember the world they were born in.