Every Go program is built from packages, and every package lives in a module. In this lesson you’ll learn what the package line means, how capital letters decide what other packages can see, how to split a program into several packages, and how to add third-party code with go get.
The package clause
Every Go file starts with a package clause: the word package and a name. You’ve written package main since your first program. A package is simply a folder of .go files that all have the same package clause. They share everything: a function in one file can call a function in another file of the same package without any import.
The name main is special. A main package with a func main() builds into a program you can run. Every other package is a library: code that other packages import and use.
You’ve been importing library packages all along. Here’s a program that uses three from the standard library:
package main
import (
"fmt"
"math"
"strings"
)
func main() {
fmt.Println(strings.ToUpper("gopher"))
fmt.Println(math.Sqrt(16), math.Pi)
}Code language: Go (go)
GOPHER
4 3.141592653589793Code language: plaintext (plaintext)
The string in the import is the import path. In your code you use the package name, which by convention is the last part of the path. So import "math/rand/v2" gives you a package named rand, used as rand.IntN(10).
Go is strict about imports. An import you don’t use is a compile error, not a warning:
package main
import (
"fmt"
"os"
)
func main() {
fmt.Println("hello")
}Code language: Go (go)
./main.go:5:2: "os" imported and not usedCode language: plaintext (plaintext)
That feels annoying at first, but it keeps every file’s list of dependencies honest. Most editors add and remove imports for you as you type.
Exported and unexported names
Go has no public or private keywords. Instead, the first letter of a name decides who can see it:
- A name that starts with a capital letter (
ToUpper,Pi,Println) is exported: other packages can use it. - A name that starts with a lowercase letter is unexported: only code inside the same package can use it.
That’s why every function you call from another package starts with a capital letter. Get the case wrong and the compiler says the name doesn’t exist (and helpfully suggests the right one):
package main
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.pi)
}Code language: Go (go)
./main.go:9:19: undefined: math.pi (but have Pi)Code language: plaintext (plaintext)
The rule applies to everything at package level: functions, types, variables, constants. It also applies to struct fields and methods. A struct can have exported fields that other packages read and write, next to unexported fields that only its own package touches. That’s how a package hides its internals and keeps the freedom to change them later.
Modules and go.mod
A module is a collection of packages that are versioned and shipped together. It’s defined by a file named go.mod in the module’s root folder. Usually one project (one Git repository) is one module.
You create one with go mod init and a module path. Let’s start a small weather tool:
mkdir weather
cd weather
go mod init example.com/weatherCode language: Bash (bash)
go: creating new go.mod: module example.com/weatherCode language: plaintext (plaintext)
The new go.mod is two lines:
module example.com/weather
go 1.27.3Code language: plaintext (plaintext)
The module line is the module path. It’s the prefix of the import path of every package inside the module. The go line is the minimum Go version the code needs; go mod init fills in the version you have installed.
The module path doesn’t have to exist on the internet. For a program you keep to yourself, example.com/weather or just weather is fine. If you plan to publish the module, use the address of its repository, such as github.com/yourname/weather, so go get can find it.
Splitting a program into packages
A small program can live in a single main.go, and many do. Once a file gets long, or you have code that makes sense on its own, move it into a package. Here’s the weather tool with two packages next to main:
weather/
├── go.mod
├── main.go
├── convert/
│ └── convert.go
└── internal/
└── format/
└── format.goCode language: plaintext (plaintext)
One folder, one package. The convert folder holds package convert:
// convert/convert.go
// Package convert converts temperatures between units.
package convert
import "math"
// CToF converts degrees Celsius to degrees Fahrenheit.
func CToF(c float64) float64 {
return round(c*9/5 + 32)
}
// FToC converts degrees Fahrenheit to degrees Celsius.
func FToC(f float64) float64 {
return round((f - 32) * 5 / 9)
}
// round rounds to one decimal place. It starts with a
// lowercase letter, so only package convert can use it.
func round(x float64) float64 {
return math.Round(x*10) / 10
}Code language: Go (go)
CToF and FToC are exported; round is a helper that stays private. The comments directly above the package clause and each exported function are doc comments. go doc and pkg.go.dev show them as documentation, so it’s worth writing them.
The second package formats a temperature for display:
// internal/format/format.go
// Package format turns temperatures into display strings.
package format
import "fmt"
// Temp formats a temperature with its unit.
func Temp(value float64, unit string) string {
return fmt.Sprintf("%.1f°%s", value, unit)
}Code language: Go (go)
And main.go imports both by their full import path, the module path plus the folder:
// main.go
package main
import (
"fmt"
"example.com/weather/convert"
"example.com/weather/internal/format"
)Code language: Go (go)
func main() {
for _, c := range []float64{-10, 0, 21.5, 100} {
f := convert.CToF(c)
fmt.Println(format.Temp(c, "C"), "=", format.Temp(f, "F"))
}
}Code language: Go (go)
(Both blocks go in the same main.go; I split them only to show the imports on their own.) Notice how I grouped the imports: standard library first, then a blank line, then packages from modules. Standard library paths have no dot in their first part (fmt, net/http); everything else starts with a domain-like module path. gofmt keeps each group sorted.
Run the whole module from its root folder. The . means “the package in this folder”, and the go tool compiles the imported packages along with it:
go run .Code language: Bash (bash)
-10.0°C = 14.0°F
0.0°C = 32.0°F
21.5°C = 70.7°F
100.0°C = 212.0°FCode language: plaintext (plaintext)
Now try to call the helper from main, for example convert.round(c), and build:
# example.com/weather
./main.go:13:16: name round not exported by package convertCode language: plaintext (plaintext)
Here’s how the pieces fit together:
The internal directory
Exported names are visible to every package that imports yours, including code in other modules once you publish. Sometimes you want a package that your own module can share but nobody else can depend on. Put it under a folder named internal.
The go tool enforces the rule: a package under internal/ can only be imported by code rooted at the parent of that internal folder. Our main package can import example.com/weather/internal/format. A different module that tries the same import fails to build:
main.go:6:2: use of internal package example.com/weather/internal/format not allowedCode language: plaintext (plaintext)
That gives you room to change format any way you like without breaking anyone. When I’m unsure whether a package should be public, I start it in internal/. Moving it out later is easy; taking back a public package is not.
Adding a dependency with go get
The standard library covers a lot, but sometimes you want a third-party package. As an example, I’ll use github.com/google/uuid, a widely used package for generating unique IDs, in a new module:
mkdir hello
cd hello
go mod init example.com/helloCode language: Bash (bash)
Write a main.go that imports it:
// main.go
package main
import (
"fmt"
"github.com/google/uuid"
)Code language: Go (go)
func main() {
id := uuid.New()
fmt.Println("new id:", id)
fmt.Println("version:", id.Version())
}Code language: Go (go)
If you run it now, Go refuses and tells you what to do:
main.go:7:2: no required module provides package github.com/google/uuid; to add it:
go get github.com/google/uuidCode language: plaintext (plaintext)
Go never downloads code behind your back. You add a dependency on purpose, with go get:
go get github.com/google/uuidCode language: Bash (bash)
go: downloading github.com/google/uuid v1.6.0
go: added github.com/google/uuid v1.6.0Code language: plaintext (plaintext)
Now go run . works (your ID will differ, it’s random):
new id: a3373a89-7c52-41ff-8a58-4dc857e2038b
version: VERSION_4Code language: plaintext (plaintext)
go get picked the latest release and recorded it. To ask for a specific version, add it after an @, like go get github.com/google/[email protected]. Running go get on a module you already have, with @latest, upgrades it.
go.mod and go.sum
go get changed two files. go.mod got a require line:
module example.com/hello
go 1.27.3
require github.com/google/uuid v1.6.0 // indirectCode language: plaintext (plaintext)
And there’s a new file, go.sum:
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=Code language: plaintext (plaintext)
They have different jobs:
go.modsays which modules and versions your module needs. You read it, and sometimes edit it.go.sumholds a cryptographic checksum of each of those downloads. If the code behind a version ever changes, the checksum won’t match and the go tool stops. You don’t edit it by hand.
Commit both files to version control. Then anyone who clones your project builds it with exactly the same dependency versions.
Cleaning up with go mod tidy
See the // indirect comment above? That’s go get being cautious: it marks a requirement as indirect until something confirms your own code imports it. go mod tidy does that check. It reads all your source files and makes go.mod and go.sum match: it adds requirements you import but haven’t listed, removes the ones you no longer use, and fixes the markers.
go mod tidyCode language: Bash (bash)
Afterwards go.mod looks like this:
module example.com/hello
go 1.27.3
require github.com/google/uuid v1.6.0Code language: plaintext (plaintext)
I run go mod tidy whenever I add or remove imports, and always before a commit. A dependency your code needs only because another dependency needs it stays in go.mod with an // indirect comment, and that’s correct.
Finding packages on pkg.go.dev
pkg.go.dev is the official site for Go package documentation. It shows the docs for the standard library and for any public module, generated from the same doc comments you just wrote. You can search it for packages too.
Before adding a dependency, I check a few things on its page: the license, when the latest version was published, whether it has reached v1 (a promise of a stable API), and how many packages import it. Every dependency is code you now rely on, so check the standard library first. It’s large, well documented and never needs go get.
For everything modules can do (versions, replacing a dependency with a local copy, workspaces), the Go modules reference is the full manual, and Organizing a Go module shows common project layouts.
Practice
1. Fix the imports. This program has two problems related to this lesson. Find them by reading the compiler errors, then fix them.
package main
import (
"fmt"
"os"
"strings"
)
func main() {
fmt.Println(strings.toUpper("gopher"))
}Code language: Go (go)
Show a solution
os is imported but never used, and toUpper isn’t exported. The function is ToUpper.
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.ToUpper("gopher"))
}Code language: Go (go)
GOPHERCode language: plaintext (plaintext)2. Extend the weather module. Add an exported function CToK to package convert that converts Celsius to kelvins (add 273.15) and rounds with the existing round helper. Print the kelvin value in main too.
Show a solution
Add this to convert/convert.go. It can call round because it’s in the same package:
// CToK converts degrees Celsius to kelvins.
func CToK(c float64) float64 {
return round(c + 273.15)
}Code language: Go (go)
And change the loop in main.go:
func main() {
for _, c := range []float64{-10, 0, 21.5, 100} {
f := convert.CToF(c)
k := convert.CToK(c)
fmt.Println(format.Temp(c, "C"), "=", format.Temp(f, "F"), "=", k, "K")
}
}Code language: Go (go)
go run . prints:
-10.0°C = 14.0°F = 263.2 K
0.0°C = 32.0°F = 273.2 K
21.5°C = 70.7°F = 294.7 K
100.0°C = 212.0°F = 373.2 KCode language: plaintext (plaintext)Key takeaways
- A package is a folder of
.gofiles with the same package clause.package mainbuilds a program; other packages are libraries. - Names starting with a capital letter are exported; lowercase names stay inside their package.
- A module is defined by
go.mod(go mod init). Import paths are the module path plus the package’s folder. - Packages under
internal/can only be imported from inside the tree they belong to. go getadds a dependency,go mod tidykeepsgo.modandgo.sumin sync with your imports. Commit both files.