Goal: Experience the complete dependency-management lifecycle in a real Go project: initialize a module, add dependencies, inspect
go.modandgo.sum, pin and change versions, understand direct and indirect dependencies, clean the module graph, verify downloads, use the module cache, explore vendoring, and configure private-module behavior.
By the end of this lab, you will be able to:
- Explain the difference between a package, module, and dependency.
- Create a new Go module with
go mod init. - Add dependencies by importing packages and running
go mod tidy. - Add or change an explicit dependency version with
go get. - Read and interpret
go.mod. - Explain the purpose of
go.sum. - Distinguish direct and indirect dependencies.
- Inspect the selected module graph.
- Discover available dependency versions.
- Upgrade and downgrade a dependency safely.
- Remove unused dependencies with
go mod tidy. - Download and verify module dependencies.
- Inspect Go module environment settings such as
GOPROXY,GOSUMDB,GOPRIVATE, andGOMODCACHE. - Build using vendored dependencies.
- Understand how private modules are configured.
- Apply a production-ready dependency workflow before committing code.
You need:
- Go installed.
- A terminal or command prompt.
- A text editor or IDE.
- Internet access for downloading public Go modules.
- Git is recommended, but not required for the core exercises.
The core module commands in this lab are suitable for modern module-aware Go releases. If your environment is managed by your organization, keep its proxy and private-module settings instead of overwriting them.
Before starting, keep these terms clear.
A package is a directory of Go source files compiled together.
Example import:
import "github.com/google/uuid"Here, your program imports the uuid package.
A module is a collection of related Go packages that are versioned together.
A module is identified by a module path in a go.mod file.
Example:
module example.com/order-service
A dependency is another module or package your code requires.
Your project can depend on a package directly, while that package's module may itself depend on other modules.
The directory containing go.mod is the module root.
A simple project might look like this:
order-service/
├── go.mod
├── go.sum
└── main.go
Open a terminal.
Run:
go versionExample output:
go version go1.xx.x linux/amd64
Your exact Go version and operating system will differ.
Now inspect the Go environment:
go envYou do not need to understand every value yet.
Check a few module-related settings specifically:
go env GOMOD GOPATH GOMODCACHE GOPROXY GOSUMDB GOPRIVATEGOMOD— path to the activego.mod, if you are inside a module.GOPATH— Go workspace/cache base location.GOMODCACHE— location where downloaded modules are cached.GOPROXY— where Go looks for modules.GOSUMDB— checksum database configuration for public modules.GOPRIVATE— module-path patterns treated as private.
At this point, GOMOD may point to a null-device path or indicate that no module is active. That is expected if you are not inside a Go module yet.
Create a project directory:
mkdir order-service
cd order-serviceConfirm your current directory:
pwdOn Windows PowerShell, you can use:
Get-LocationRun:
go mod init example.com/order-serviceExpected result resembles:
go: creating new go.mod: module example.com/order-service
List the files:
lsWindows PowerShell:
Get-ChildItemYou should see:
go.mod
Open go.mod.
It should resemble:
module example.com/order-service
go 1.xx.xThe exact
godirective is based on the Go toolchain used when the module is initialized.
Answer these questions before continuing:
- What is the module path?
- Where is the module root?
- Does
go.sumexist yet? - Why might
go.sumnot exist yet?
There are no external dependencies yet, so there may be nothing for Go to record in go.sum.
Create main.go:
package main
import "fmt"
func main() {
fmt.Println("Order service started")
}Run it:
go run .Expected output:
Order service started
Build it:
go build .Run tests across all packages:
go test ./...Because we have not written tests, the result may resemble:
? example.com/order-service [no test files]
fmt is part of the Go standard library. It does not become an external requirement in go.mod.
We will use github.com/google/uuid to generate order IDs.
Replace main.go with:
package main
import (
"fmt"
"github.com/google/uuid"
)
func main() {
orderID := uuid.New()
fmt.Println("Order ID:", orderID)
}At this moment, the source imports a package that is not yet recorded as a module requirement.
Try:
go run .Depending on your Go version and module state, Go may tell you which package requirement is missing and suggest a command to add it.
Now deliberately synchronize dependencies with the source code:
go mod tidyThen run:
go run .Expected output resembles:
Order ID: 2d4b0d2b-....
The UUID will be different each time.
Open go.mod again.
You should now see a requirement for the UUID module, similar to:
module example.com/order-service
go 1.xx.x
require github.com/google/uuid v1.6.0The exact selected version is determined by the module resolution process and may differ if a newer compatible version exists when you run the lab.
go mod tidy examined the packages imported by your module and made go.mod consistent with your source code.
It can:
- add requirements that are needed but missing;
- remove requirements that are no longer needed;
- update
go.sumentries required by the resulting dependency graph.
Run:
go list -mExpected output:
example.com/order-service
Now run:
go list -m allYou should see your main module and its selected dependencies.
List the files:
lsYou should now have something similar to:
go.mod
go.sum
main.go
Open go.sum.
You will see checksum entries resembling:
github.com/google/uuid v1.6.0 h1:...
github.com/google/uuid v1.6.0/go.mod h1:...
go.sum records cryptographic hashes used by the Go toolchain when authenticating downloaded module content.
- Commit
go.sumto version control for normal applications and services. - Do not treat it as a lock file in the exact sense used by some other ecosystems.
- Do not manually edit checksum lines.
- Let Go commands maintain it.
There are two common workflows:
Add import in source
↓
go mod tidy
↓
go.mod / go.sum updated
go get module@version
↓
go.mod / go.sum updated
↓
Import and use the package
Use go get when you intentionally want to add, upgrade, or downgrade a module requirement.
For example, pin UUID explicitly:
go get github.com/google/uuid@v1.6.0Then inspect:
go list -m github.com/google/uuidExpected output:
github.com/google/uuid v1.6.0
Run:
go mod tidyThen verify the program still works:
go test ./...
go run .Ask Go which tagged versions it knows about:
go list -m -versions github.com/google/uuidOutput will list available versions, for example:
github.com/google/uuid v1.0.0 ... v1.5.0 v1.6.0
Do not blindly guess dependency versions.
Before changing a dependency, you can inspect available versions and intentionally select one.
Change UUID to an older known version:
go get github.com/google/uuid@v1.5.0Inspect the selected version:
go list -m github.com/google/uuidExpected:
github.com/google/uuid v1.5.0
Inspect go.mod.
You should see the changed version.
Now test the application:
go test ./...
go run .If the code still works, the older version remains API-compatible with what this program uses.
A successful module downgrade does not automatically prove the application is correct. Always run tests and relevant integration checks after dependency changes.
Return to version v1.6.0:
go get github.com/google/uuid@v1.6.0Confirm:
go list -m github.com/google/uuidRun validation:
go mod tidy
go test ./...
go run .A safe version-change loop is:
Choose version
↓
go get module@version
↓
go mod tidy
↓
go test ./...
↓
Review go.mod + go.sum diff
↓
Commit
You can ask Go to report newer versions for modules in the current build list:
go list -m -u allThis is useful for review because it separates discovering upgrades from applying upgrades.
To move one module to its latest available version:
go get github.com/google/uuid@latestFor production work, targeted upgrades are usually easier to review than changing the entire graph at once. Broad commands such as:
go get -u ./...can update multiple modules that provide packages used by your project. Use broad upgrades only when you intend to review and test the larger change.
Go builds a selected module list from the requirements in the module graph. If multiple parts of the graph require different versions of the same module path, Go's Minimal Version Selection (MVS) algorithm selects a version that satisfies the graph's requirements rather than relying on a traditional lock-file solver.
This is one reason go.mod, the dependency graph, and tests all matter during an upgrade.
So far, UUID is a very small dependency. We will now add a library that commonly brings additional module requirements so you can observe direct and indirect dependencies.
Run:
go get github.com/spf13/cobra@v1.8.1Replace main.go with:
package main
import (
"fmt"
"os"
"github.com/google/uuid"
"github.com/spf13/cobra"
)
func main() {
rootCmd := &cobra.Command{
Use: "orders",
Short: "Generate a new order ID",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Order ID:", uuid.New())
},
}
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}Synchronize:
go mod tidyRun:
go run .Try help output:
go run . --helpInspect go.mod.
It may now resemble:
module example.com/order-service
go 1.xx.x
require (
github.com/google/uuid v1.6.0
github.com/spf13/cobra v1.8.1
)
require (
// additional modules may appear here with // indirect
)The exact transitive modules can change with dependency versions.
A module is a direct dependency when your module directly imports one or more packages provided by that module.
In this lab:
github.com/google/uuid
github.com/spf13/cobra
are direct dependencies because main.go imports them.
An indirect dependency is needed by the module graph even though your own source does not directly import a package from that module.
Go may annotate such requirements with:
// indirect
Run:
go list -m allObserve that the module graph may contain more modules than the two packages your code directly imports.
Use go mod why to understand why a module is needed.
For Cobra:
go mod why -m github.com/spf13/cobraFor UUID:
go mod why -m github.com/google/uuidFor an indirect module listed in your go.mod, substitute its module path:
go mod why -m MODULE_PATHExample pattern:
# github.com/spf13/cobra
example.com/order-service
github.com/spf13/cobra
Your exact output can differ.
Can you trace a path from your application to the indirect dependency?
Run:
go mod graphYou will see lines resembling:
moduleA moduleB@version
Each line means the module on the left has a requirement relationship involving the module on the right.
For a large real-world project, this graph can be extensive.
Find:
- Your main module.
- Cobra.
- UUID.
- At least one transitive module.
Add a dependency that your source code does not use:
go get github.com/pkg/errors@v0.9.1Confirm it appears in the module information:
go list -m allNow run:
go mod tidyCheck again:
go list -m allInspect go.mod.
If no package in your module needs github.com/pkg/errors, go mod tidy should remove the unnecessary requirement from the dependency graph described by your module files.
go get can intentionally change a dependency requirement, but go mod tidy reconciles module metadata with what the source and package graph actually require.
Run:
go mod downloadThis downloads modules needed by the module graph into the module cache without requiring you to run the application first.
This is useful in workflows such as:
- CI build preparation;
- container image layer caching;
- prefetching dependencies;
- reproducible build environments.
Inspect the cache location:
go env GOMODCACHEDo not manually modify files inside the module cache.
Run:
go mod verifyExpected successful output:
all modules verified
This checks downloaded module data against expected cryptographic hashes.
Dependency management is not only about version selection. It also includes validating that module content has not unexpectedly changed.
Print the module cache directory:
go env GOMODCACHEYou can list it if desired.
Linux/macOS example:
ls "$(go env GOMODCACHE)"PowerShell example:
Get-ChildItem (go env GOMODCACHE)Only do this if you understand that Go will need to download modules again. Avoid doing this unnecessarily on slow or restricted networks.
go clean -modcacheThen restore the dependencies:
go mod downloadVerify again:
go mod verifyRun:
go test ./...This demonstrates that the project can restore its dependencies from module metadata rather than relying permanently on one developer's local cache.
Check the current module proxy configuration:
go env GOPROXYA common public configuration resembles:
https://proxy.golang.org,direct
Do not overwrite your organization's proxy configuration just to match this example.
Go can retrieve modules through configured module proxies and, depending on the setting, fall back to direct version-control access.
This improves dependency availability and can help organizations control how external code is retrieved.
Check:
go env GOSUMDBFor public modules, Go can use a checksum database to help authenticate module content.
Do not globally disable checksum verification simply to work around a private-module problem.
For private modules, configure the appropriate private-module settings instead.
Suppose your company hosts private modules under:
github.com/mycompany/*
You can tell Go that these module paths are private:
go env -w GOPRIVATE='github.com/mycompany/*'Check the value:
go env GOPRIVATEIt tells the Go command that matching module paths are private. Among other effects, it provides defaults that keep matching private module paths away from the public module proxy and public checksum database.
You still need valid authentication for the private Git or module server.
If you changed GOPRIVATE only for this exercise, remove the value:
go env -u GOPRIVATEIf your machine already had an organization-specific GOPRIVATE value before the lab, restore that original value instead of unsetting it.
Go's module versioning has an important rule for major versions v2 and higher.
A module at major version 2+ normally includes the major version in its module path.
Conceptual example:
import "example.com/library/v2"The /v2 is part of the import/module path, not just decoration.
This allows incompatible major versions to exist as distinct module paths.
Which looks like a normal import path for version 3 of a module?
A. example.com/payments@v3
B. example.com/payments/v3
C. example.com/v3/payments
Answer: B
Vendoring creates a local vendor/ directory containing dependency source needed for builds.
Run:
go mod vendorInspect the project:
order-service/
├── go.mod
├── go.sum
├── main.go
└── vendor/
Inspect vendor metadata:
cat vendor/modules.txtPowerShell:
Get-Content vendor/modules.txtBuild explicitly from vendored dependencies:
go build -mod=vendor .Run tests using the vendor directory:
go test -mod=vendor ./...Examples include:
- highly controlled build environments;
- environments where dependency source must be checked into the repository;
- certain offline or restricted-network build processes;
- compliance or source-review workflows.
Vendoring is optional. Go Modules do not require every project to commit a vendor/ directory.
Linux/macOS:
rm -rf vendorPowerShell:
Remove-Item -Recurse -Force vendorThe replace directive is useful when you need your main module to temporarily use a local module during development.
Move to the directory containing order-service:
cd ..Create another module:
mkdir greeting-lib
cd greeting-lib
go mod init example.com/greeting-libCreate greeting.go:
package greeting
func Message() string {
return "Dependency loaded from local module"
}Return to the application:
cd ../order-serviceAdd a local replacement:
go mod edit -replace=example.com/greeting-lib=../greeting-libAdd an explicit local-only requirement:
go mod edit -require=example.com/greeting-lib@v0.0.0The placeholder version works here because the replace directive tells Go to use the local directory instead of resolving that module version from a remote repository.
Now modify main.go:
package main
import (
"fmt"
"os"
"example.com/greeting-lib"
"github.com/google/uuid"
"github.com/spf13/cobra"
)
func main() {
rootCmd := &cobra.Command{
Use: "orders",
Short: "Generate a new order ID",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(greeting.Message())
fmt.Println("Order ID:", uuid.New())
},
}
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}Run:
go mod tidy
go run .Expected output includes:
Dependency loaded from local module
Order ID: ...
Inspect go.mod.
You should see a replace directive similar to:
replace example.com/greeting-lib => ../greeting-libIt lets you test a local dependency without publishing a new remote version first.
A local filesystem replace path usually should not be left in a release-oriented go.mod accidentally. Review replace directives before merging or releasing.
First restore main.go to the Cobra + UUID version from Section 14.
Then run:
go mod edit -dropreplace=example.com/greeting-lib
go mod tidyConfirm:
go test ./...Use this command table as a reference.
| Command | Purpose |
|---|---|
go mod init <module-path> |
Initialize a module |
go mod tidy |
Synchronize go.mod and go.sum with source requirements |
go get module@version |
Add, upgrade, or downgrade a module requirement |
go list -m |
Show the main module |
go list -m all |
Show all selected modules in the build list |
go list -m -versions module |
Show known tagged versions |
go mod why -m module |
Explain why a module is needed |
go mod graph |
Print module requirement relationships |
go mod download |
Download modules into the module cache |
go mod verify |
Verify downloaded module content |
go mod vendor |
Create a vendor directory |
go mod edit |
Programmatically edit go.mod directives |
go env GOMOD |
Show the active go.mod path |
go env GOMODCACHE |
Show module cache location |
go env GOPROXY |
Show module proxy configuration |
go env GOPRIVATE |
Show private-module patterns |
go test ./... |
Test all packages in the module |
go build ./... |
Build all packages in the module |
Avoid these dependency-management mistakes.
Let Go manage checksum entries.
A command that changes many dependencies can introduce behavioral changes even if compilation succeeds.
Dependency changes are code changes and should be reviewed.
Run:
go mod tidybefore committing.
Use properly scoped private-module configuration and correct repository authentication.
Major-version import paths matter.
Review release notes, compatibility, tests, and security impact before upgrading production dependencies.
Use this workflow when adding a dependency to a real project.
Ask:
- Do we really need it?
- Can the standard library solve the problem?
- Is the dependency actively maintained?
- Is its license acceptable for the project?
- Is the dependency footprint reasonable?
Example:
go get github.com/google/uuid@v1.6.0go mod tidygo fmt ./...
go vet ./...
go test ./...go build ./...go list -m allFor unexpected modules:
go mod why -m MODULE_PATHgo mod verifyFor a stronger production workflow, install Go's vulnerability scanner as a command:
go install golang.org/x/vuln/cmd/govulncheck@latestThen run it from the module root:
govulncheck ./...Notice the command choice: use go install ...@version to install an executable. Use go get to change dependencies recorded for the current module. Installing a command with a version suffix does not add that command to this application's go.mod.
Treat vulnerability findings as dependency-review input: determine whether the vulnerable code is reachable, identify the fixed dependency version, upgrade deliberately, and rerun tests.
Review at minimum:
go.mod
go.sum
If Git is available:
git diff -- go.mod go.sumNormally commit both:
go.mod
go.sum
along with the source change that required the dependency.
From the order-service module root, run:
go mod tidy
go mod verify
go vet ./...
go test ./...
go build ./...
go run . --helpThen inspect:
go list -m allYour project should be clean, buildable, and have dependency metadata that matches its source code.
You are outside the module root and its parent hierarchy.
Move into the project:
cd order-serviceConfirm:
go env GOMODYour source imports a package that is not currently provided by the module build list.
If the import is intended:
go mod tidyOr explicitly request a version:
go get MODULE_PATH@VERSIONThe module may be private, but your Git/VCS credentials or private-module settings are incomplete.
go env GOPRIVATE GOPROXY GONOPROXY GONOSUMDBThen verify that your Git client can authenticate to the private repository.
Treat checksum mismatches as security-sensitive. Do not casually bypass verification.
Investigate whether:
- a dependency was republished incorrectly;
- a proxy/cache is serving unexpected content;
- the local module cache is corrupted;
- module metadata was modified incorrectly.
Do not solve a public-module checksum mismatch by globally disabling verification.
Use:
git diff -- go.mod go.sumThen investigate unexpected dependency changes with:
go list -m all
go mod why -m MODULE_PATHComplete these without copying commands from previous sections if possible.
Choose a small public Go package.
Tasks:
- Add an import.
- Run the appropriate module command.
- Confirm the module appears in
go.mod. - Run the application.
- Run
go mod tidy.
- List available versions of the module.
- Select a specific version.
- Update to that version.
- Confirm the selected version.
- Run tests.
Useful command pattern:
go list -m -versions MODULE_PATH
go get MODULE_PATH@VERSION
go list -m MODULE_PATH- Find a module marked
// indirect. - Run
go mod why -mon it. - Explain in one sentence why your application needs it.
- Add a module requirement.
- Do not import its package.
- Run
go mod tidy. - Confirm the unused requirement disappears if nothing else needs it.
- Ensure
go.modandgo.sumare present. - Download dependencies.
- Verify them.
- Test the module.
- Build the module.
Commands:
go mod download
go mod verify
go test ./...
go build ./...Which file defines the module path and dependency requirements?
A. go.yaml
B. go.mod
C. go.lock
D. modules.json
Answer: B
Which command synchronizes dependency requirements with imported packages?
A. go mod tidy
B. go clean
C. go fmt
D. go env
Answer: A
Which command intentionally selects a specific module version?
go get example.com/library@v1.2.3Answer: go get with an explicit version query.
Should go.sum normally be committed for an application/service?
Answer: Yes.
What does // indirect mean?
Answer: The module is required by the selected module/package graph even though the main module does not directly import a package from that module in the relevant direct-dependency sense.
Which command helps answer, "Why is this module in my dependency graph?"
Answer:
go mod why -m MODULE_PATHWhich command checks downloaded modules against expected hashes?
Answer:
go mod verifyWhat should be reviewed after an upgrade?
Answer: At minimum, source behavior, tests, go.mod, go.sum, and the resulting dependency graph; production teams may also review release notes, licenses, and security impact.
You have completed the lab when you can check every item below.
- I installed and verified Go with
go version. - I created a new module with
go mod init. - I can explain the module path in
go.mod. - I added an external dependency.
- I generated and inspected
go.sum. - I used
go mod tidy. - I used
go getwith an explicit version. - I listed available module versions.
- I downgraded and upgraded a dependency.
- I inspected all selected modules with
go list -m all. - I identified direct and indirect dependencies.
- I used
go mod why -m. - I inspected
go mod graph. - I downloaded dependencies with
go mod download. - I verified dependencies with
go mod verify. - I inspected
GOMODCACHE. - I inspected
GOPROXYandGOSUMDB. - I understand when
GOPRIVATEis needed. - I understand the
/v2,/v3, etc. major-version path rule. - I tested and built the module successfully.
- I know that
go.modandgo.sumshould normally be reviewed and committed.
Go Modules provide a complete dependency-management system built directly into the Go toolchain.
The core workflow to remember is:
Write/import code
↓
go get module@version ← when intentionally choosing/changing a version
↓
go mod tidy ← synchronize dependency metadata
↓
go test ./...
↓
go build ./...
↓
go mod verify
↓
Review go.mod + go.sum
↓
Commit
If you remember only five commands, remember these:
go mod init example.com/project
go get MODULE@VERSION
go mod tidy
go list -m all
go mod verifyThat workflow gives you clean dependency metadata, repeatable module resolution, explicit version control, and a strong foundation for production Go builds.