The missing package error
You write a Go program that imports github.com/gin-gonic/gin. You hit run. The compiler stops you immediately with could not import github.com/gin-gonic/gin (no required module provides package github.com/gin-gonic/gin). The code is fine. The logic is sound. The problem is that Go doesn't know where to find that code on your machine, and it refuses to guess. You need to tell the toolchain to fetch the dependency and record it so the build can succeed.
Go uses modules to manage dependencies. A module is a collection of packages stored in a single version control repository. Your project is a module. Every external library you use is also a module. The module system requires explicit declarations. If a package isn't listed in your module definition, the compiler treats it as if it doesn't exist. This strictness prevents accidental dependencies and ensures every build is reproducible.
go get adds, go mod tidy cleans
Think of go.mod as a precise manifest for your project. It lists every external module you need and the exact version you require. go get is the command that adds items to this manifest. It downloads the package, resolves its dependencies, and updates go.mod and go.sum.
go mod tidy is the cleanup command. It scans your source code, finds all the imports, and compares them against go.mod. It adds any missing dependencies and removes any that are no longer used. The community treats go mod tidy as the source of truth for the dependency list. Run it before every commit. It ensures your manifest matches your code.
go get modifies the manifest. go mod tidy reconciles the manifest with reality. Use both.
A minimal dependency workflow
Start with a simple program that needs an external library. Create a module first if you haven't already.
go mod init example.com/myapp
This creates go.mod with the module path and the Go version. Now write the code.
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
// main starts the Gin router and listens on port 8080.
func main() {
// gin.Default() creates a router with logging and recovery middleware enabled.
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
// c.JSON sends a JSON response with status 200.
c.JSON(200, gin.H{
"message": "pong",
})
})
// r.Run() blocks the main goroutine and starts the HTTP server.
r.Run()
}
Try to run this. The compiler rejects it because gin isn't in the module. Add the dependency.
go get github.com/gin-gonic/gin
The toolchain contacts the module proxy, downloads the latest version of gin, and updates go.mod. It also writes a cryptographic hash to go.sum. Now run the cleanup.
go mod tidy
This verifies that gin is actually used in your code. If you had added a package but never imported it, tidy would remove it. Now the build succeeds.
go run .
The server starts. The dependency is managed. The manifest is clean.
How the module system tracks versions
When go get runs, it does more than download files. It updates two critical files.
go.mod contains the require block. This lists the direct dependencies and their versions. The module solver uses this to compute the minimal version set. If gin requires github.com/go-playground/validator v10.0.0, the solver adds that to the require list automatically. You don't manage transitive dependencies manually. The toolchain handles the graph.
go.sum contains hashes for every downloaded module. Each line looks like a module path, version, and a long hash string. The hash proves the code hasn't changed. Every build verifies the downloaded archive against the hash in go.sum. If the hash mismatches, the build fails with verifying module: checksum mismatch. This protects against tampering and corruption. Commit go.sum to version control. It guarantees that anyone cloning your repo gets the exact same code.
The module proxy caches packages. By default, Go uses proxy.golang.org. This proxy serves packages from public repositories. It speeds up downloads and provides a consistent interface. If the proxy is down, Go falls back to the origin repository. You can configure a different proxy using the GOPROXY environment variable. Most teams use the default. It works reliably.
Realistic usage: pinning and updating
Sometimes you need a specific version. Maybe a bug fix exists in v1.9.1 or you're waiting for v2.0.0. You can pin the version directly.
go get github.com/gin-gonic/gin@v1.9.1
This updates go.mod to require that exact version. The @ syntax tells go get to use the specified version instead of the latest. If you later import a package that requires gin v2.0.0, the module solver might complain about incompatible versions. Go uses semantic versioning. Major version bumps go in the module path, like github.com/gin-gonic/gin/v2. The solver prevents mixing major versions in the same build.
To upgrade a dependency, specify the new version.
go get github.com/gin-gonic/gin@latest
This fetches the newest version and updates go.mod. Run go mod tidy afterward to ensure the rest of the graph is consistent. Check the changes with go list -m all. This command prints the full dependency graph. It helps you spot unexpected upgrades or conflicts.
Avoid go get -u. This flag attempts to upgrade all dependencies. It can break your build by pulling in incompatible minor versions. Use specific version updates instead. Control your upgrades one package at a time.
Pitfalls and common errors
Running go get outside a module changes its behavior. If you aren't inside a module directory, go get downloads the package to the cache but doesn't create a go.mod file. You usually want to be inside a module. The error go: go.mod file not found in current directory or any parent directory tells you to initialize a module first.
Private repositories require authentication. go get fails with go: github.com/private/repo: git ls-remote -q --exit-code origin in /home/user/go/pkg/mod/cache/vcs/...: exit status 128 if it can't access the repo. Configure authentication using environment variables or a credential helper. Set GOPRIVATE to tell Go which modules are private and skip the proxy for them.
Beginners often confuse go get with go install. go install builds and installs a binary to GOBIN. go get manages dependencies. In Go 1.16+, go install no longer modifies go.mod. Use go install to get tools like golang.org/x/tools/go/analysis. Use go get to add libraries to your project. Trying to install a non-main package with go install results in go install: no install location for directory /home/user/src/myproject outside GOPATH.
The compiler rejects unused imports. If you add a dependency but don't import it, go build fails with imported and not used. This is a feature. Go forces you to keep your imports clean. go mod tidy handles the manifest, but the compiler handles the code. Both must agree.
Decision matrix
Use go get when you are adding a new dependency or updating a specific package to a known version.
Use go mod tidy when you want to synchronize your module files with your actual imports and remove unused dependencies.
Use go mod init when you are starting a new project and need to create the initial module definition.
Use go list -m all when you need to inspect the full dependency graph and resolve version conflicts.
Use go install when you want to build and install a binary tool to your local bin directory.
Use go get -u with extreme caution, only when you explicitly want to attempt upgrading all dependencies and are prepared to handle potential breakage.
Where to go next
- Go Environment Variables Explained: GOPATH, GOROOT, GOBIN, GOPROXY
- How to Manage Go Versions with goenv
go get adds. go mod tidy cleans. Trust the manifest. Commit go.sum. Reproducibility is free.