11 - Packages and Modules¶
What this session is¶
About an hour. You'll learn how Go code is organized (packages), how Go projects are versioned and shared (modules), how to bring in code other people wrote (go get), and the rule that decides what's visible outside a package (capitalization). This is the page that bridges you from "I write small programs" to "I work with real codebases."
Two related ideas¶
- A package is a folder of Go files that work together. Every Go file declares which package it belongs to (
package foo). - A module is a tree of packages with a
go.modfile at the root, declaring an import path and dependencies. Modules are how Go projects are versioned and shared.
You met a module briefly in page 10 (go mod init mathutils). Time to look properly.
Building a small multi-file package¶
Start fresh:
That creates go.mod:
Now create a sub-folder greet/ with greet.go:
package greet
import "fmt"
func Hello(name string) string {
return fmt.Sprintf("Hello, %s!", name)
}
func goodbye(name string) string {
return fmt.Sprintf("Bye, %s.", name)
}
And at the top level, create main.go:
package main
import (
"fmt"
"greetapp/greet"
)
func main() {
fmt.Println(greet.Hello("Alice"))
// fmt.Println(greet.goodbye("Alice")) // would not compile
}
Run from the top level:
Output: Hello, Alice!.
What's new:
- The
greet/greet.gofile starts withpackage greet. That's the name code uses to refer to it. main.gosaysimport "greetapp/greet"- the full path is<module>/<folder>. The first part comes fromgo.mod; the rest is the folder path.greet.Hello(...)- to use something from another package, prefix with the package name.
Exported vs unexported: the capitalization rule¶
This is one of Go's defining rules and one of the easiest to forget:
A name starting with a capital letter is exported (visible from outside the package). A name starting with a lowercase letter is not.
In greet.go:
- Hello is exported. main.go can call greet.Hello(...).
- goodbye is not exported. If you uncomment greet.goodbye(...) in main, the compiler will refuse with goodbye is not exported.
This rule applies to everything you can name: functions, types, struct fields, variables, constants, methods. Capitalize what should be visible from outside; lowercase what's an internal detail.
You don't pick this convention. The compiler enforces it. There's no public/private keyword in Go because of this rule.
The import block¶
When you import multiple packages, group them:
By convention: - Standard-library packages first (alphabetical). - A blank line. - Local and third-party packages second (alphabetical).
The tool goimports does this automatically; most editors run it on save.
Modules: a real example with a third-party dependency¶
Real projects don't just use the standard library. They bring in code from GitHub and elsewhere. Go's tool for this is go get.
Let's make a program that prints colored text. The library github.com/fatih/color is popular and small.
mkdir -p ~/code/go-learning/coloredhello && cd ~/code/go-learning/coloredhello
go mod init coloredhello
go get github.com/fatih/color
After go get, go.mod has a new line listing the dependency, and a go.sum file appears with checksums. Both should be committed if you put this in git.
Create main.go:
package main
import "github.com/fatih/color"
func main() {
color.Green("hello in green")
color.Red("hello in red")
color.Cyan("hello in cyan")
}
Run go run .. You should see three colored lines (assuming your terminal supports color, which most do).
go.mod and go.sum explained¶
After the above, go.mod looks roughly like:
module coloredhello- the import path of this module. If you publish to GitHub, this should begithub.com/yourname/yourrepo.go 1.22- the minimum Go version this code expects.require ...- direct dependencies, with versions.
go.sum has cryptographic hashes for every dependency (and their dependencies). It's how Go verifies you got exactly the same bytes everyone else gets when they fetch the same versions.
Both files are always committed to git. Together they make builds reproducible.
Useful module commands¶
| Command | What it does |
|---|---|
go mod init <path> |
Create a new module. |
go get <pkg> |
Add or upgrade a dependency. |
go get <pkg>@v1.2.3 |
Pin to a specific version. |
go mod tidy |
Add missing dependencies, remove unused ones. Run this before commits. |
go list -m all |
List all dependencies (direct + transitive). |
go mod why <pkg> |
Explain why a transitive dependency is in your graph. |
go mod tidy is the most useful. Run it whenever you've changed imports.
The standard library¶
The standard library ships with Go. You've already used it: fmt, errors, strings, strconv, time, sync, testing, math, os, sort.
A few more you'll meet:
- os - files, environment variables, command-line args.
- io - readers, writers (the universal abstractions for "stream of bytes").
- bufio - buffered I/O.
- net/http - HTTP client and server. Yes, Go ships a production-grade web server in the standard library.
- encoding/json - turn structs into JSON and back.
- path/filepath - manipulate file paths portably.
- log/slog - structured logging.
- context - pass cancellation and deadlines through call chains.
Browse the index at pkg.go.dev/std. The standard library is excellent and you should reach for it before any third party.
Interfaces (the briefest possible introduction)¶
You'll see the word interface in Go code constantly. Quick version:
An interface is a named set of methods. Any type that has those methods automatically satisfies the interface - no need to declare it.
Anything with a String() string method automatically "is a" Stringer. No implements keyword. The matching happens silently. This is called structural typing or duck typing with a type system.
The most common interface you'll see is io.Reader (anything with a Read method) and io.Writer (anything with a Write method). They're the reason files, network connections, gzip streams, and bytes-in-memory all look the same to code that just wants to read or write bytes.
Interfaces are a big topic. For a first read of OSS code, this is enough: when you see func foo(r io.Reader), the function works on anything that can be read from. Don't get bogged down.
Exercise¶
Two parts.
Part 1 - your own multi-file package:
- Start a new module:
mkdir ~/code/go-learning/bank && cd ~/code/go-learning/bank && go mod init bank. - Create
account/account.gowithpackage account. Put yourAccountstruct from page 08 (withOwner,Balance,Deposit,Withdraw) in it. Make the right things exported (capital first letter) and the right things unexported. - Create
main.goat the top level. Importbank/account. Create an account, deposit, withdraw, print results. - Run
go run .. - Try referencing an unexported name from
main.go. Read the error. Fix it.
Part 2 - a third-party library:
- In a new folder, start a module.
- Run
go get github.com/fatih/color. - Use one of the color functions (
color.Green,color.Red, etc.) to print something. - Run
go mod tidy. See what changes ingo.mod(probably nothing - you imported what you needed). - Look at
go.modandgo.sum. Identify which lines are about the library you added.
What you might wonder¶
"What's the difference between a package and a module?"
A package is a folder of related Go files (package foo at the top of each). A module is a versioned bundle of packages, with a go.mod at the root. One module typically has many packages.
"Why is the import path github.com/fatih/color?"
Go's convention is that the import path matches where the source lives - usually a URL. go get knows how to clone from github.com, gitlab.com, bitbucket.org, and others, just from the path.
"What's internal/?"
Any package inside a folder named internal/ is only importable by code in the same parent module. It's how libraries hide implementation details from their users. You'll see internal/ folders in almost every real Go project.
"How do I publish my own library?"
Push to a public git repo, tag a release like v1.0.0. Others can go get github.com/you/repo and use it. The full story is more nuanced (semver, breaking changes, replace directives) but that's the gist.
Done¶
You can now:
- Organize code into packages (folder = package).
- Understand the capital-letter export rule.
- Initialize a module with go mod init.
- Add third-party dependencies with go get.
- Read and reason about go.mod and go.sum.
- Recognize standard library imports vs third-party ones.
- Have a passing acquaintance with interfaces.
You've now covered every fundamental Go concept a beginner needs. The remaining pages are about applying them - reading real code, picking a project, contributing.
Next page: how to read code other people wrote without panicking.
Next: Reading other people's code → 12-reading-other-peoples-code.md