All articles

Most Go developers know these files exist. Far fewer understand why they become critical the moment your code leaves your laptop. Introduction Imagine it’s Friday evening. You’ve spent the entire week building a new feature. Everything works perfectly on your machine. You run: go run . No errors. You push your changes to GitHub. A few minutes later, your CI pipeline turns red. Your teammate pulls the same code but gets a completely different build. The Docker image fails with an error you’ve never seen before. Nothing in your application code changed. So what happened? The answer often lies in two files that many Go developers rarely think about: go.mod go.sum Here is the same idea in a concrete example. You add Gin to an API: go get github.com/gin-gonic/gin Go updates two files: // go.mod require github.com/gin-gonic/gin v1.11.0 # go.sum github.com/gin-gonic/gin v1.11.0 h1:… The first line records a dependency requirement. The second records the expected content. Commit both files with the code that uses Gin, and another machine can select and verify the same dependency. This article explains how go.mod and go.sum work, where they matter in production, and how to use them confidently. Life Before Go Modules When Go was first released, dependency management looked very different. Every Go project lived inside a special directory called GOPATH. A typical workspace looked something like this: GOPATH/ │├── src/ │ ├── github.com/ │ │ ├── Name1/ │ │ │ └── projectA/ │ │ └── Name2/ │ | └── projectB/ │ | ├── pkg/ └── bin/ Every project had to exist inside this folder. If someone cloned your repository somewhere else, Go simply wouldn’t recognize it. For small personal projects, this wasn’t too painful. But as applications became larger, the cracks started to show. Why Go Needed Modules Before Go 1.11, projects were managed using GOPATH. Dependencies were shared globally, and there was no standard way to record which versions a project expected. This led to three major problems: Version conflicts: Two projects couldn’t reliably use different versions of the same library. Non-reproducible builds: A teammate or CI server could download newer dependencies and produce a different binary from the same source code. Unreliable deployments: Months later, rebuilding an old release became difficult because the original dependency versions weren’t recorded. The root issue wasn’t Go itself, it was the lack of a reliable way to define a project’s dependency graph. Go Modules introduced that missing piece. go.mod: More Than Just a Dependency File If you ask a new Go developer what go.mod does, you'll often hear: “It stores project dependencies.” That’s not wrong — but it’s only a small part of the story. A better way to think about go.mod is this: go.mod is the blueprint of your Go module. It tells the Go toolchain what your project is, which dependencies it needs, and the rules it should follow to build it consistently. In other words, go.mod defines the identity of your project and describes the dependency graph required to build it. Without it, Go would have no reliable way to answer questions like: What module am I building? Which version of each dependency should I use? Which version of Go is this project intended for? Should any dependencies be replaced or excluded? Has the author retracted a broken release? These aren’t questions you think about while writing a small program. They’re questions that matter when your project is built by another developer, a CI pipeline, or a production Docker image. Creating Your First Module Every Go module starts with a simple command: go mod init github.com/user-name/project-api Go generates a file named go.mod . Initially, it looks something like this: module github.com/user-name/project-api go 1.24 At first glance, it doesn’t look very exciting. Just two lines. But those two lines already tell Go two important things: This project is identified as github.com/user-name/project-api. It is intended to be built using Go 1.24 module semantics. As your project grows, this file grows with it. Let’s understand each part. The module Directive The first line is usually the easiest to overlook. module github.com/user-name/project-api Many developers assume this is simply the GitHub repository URL. It isn’t. It’s the module path. Think of it as the unique identity of your project. Whenever another project imports your package, it refers to this module path. import "github.com/user-name/project-api/internal/server" Go uses the module path to determine where your code lives and how it should be resolved. Although the module path often matches the Git repository URL, what really matters is that it uniquely identifies your module. You can even create a local module with: module myapp It works locally because the module path is simply an identifier. However, for public projects, it’s common practice to use the repository path so other developers can fetch it automatically. Why Does This Matter? Imagine you’re publishing a library. Without a unique module path, how would another project know where to find it? The module path acts much like a website’s domain name. It uniquely identifies your project in the Go ecosystem. The go Directive The next line usually looks like this: go 1.24 Many beginners assume this means: “Everyone must install Go 1.24.” That’s not quite correct. The go directive tells the Go toolchain which version of Go's module behaviour and language semantics this project targets. This affects things like: module behaviour, language features, standard library compatibility, dependency resolution rules. For example: go 1.20 means: “This module expects to be built using the rules introduced in Go 1.20.” It isn’t simply checking your installed Go version. It’s telling Go how this module should behave. Why Is This Useful? Imagine Go 1.30 changes how module resolution works. Older projects shouldn’t suddenly behave differently just because developers upgraded their compiler. The go directive allows projects to continue using the module semantics they were written for, improving long-term compatibility. The require Directive Now we reach the part most developers recognise. As soon as you add an external dependency, Go updates your go.mod. Suppose you run: go get github.com/gin-gonic/gin Your file might now contain: require github.com/gin-gonic/gin v1.11.0 This tells Go: “This module requires at least version v1.11.0 of Gin. Go’s Minimal Version Selection algorithm then determines the final build list." Notice something important. It doesn’t say: “Download the latest version.” It specifies an exact version. This is one of the biggest reasons Go builds are reproducible. Every developer working on the project starts with the same dependency graph. Direct vs Indirect Dependencies Sometimes you’ll notice something like this: require ( github.com/gin-gonic/gin v1.11.0 golang.org/x/net v0.42.0 // indirect ) Why is golang.org/x/net marked as indirect? Because your code doesn’t import it directly. Instead, one of your dependencies depends on it. Think of it like this: Your Project │ ▼ Gin │ ▼ golang.org/x/net Your project never imports golang.org/x/net. But Gin does. Go records this relationship to ensure the entire dependency graph remains reproducible. We’ll come back to why indirect dependencies matter when we discuss go mod tidy. What Actually Happens When You Run go build? Every day, thousands of Go developers type: go build The command finishes in a few seconds, prints nothing, and leaves behind an executable. Simple. Or at least, it looks simple. Behind the scenes, the toolchain reads the module metadata, selects a build list, obtains missing modules, verifies them, and uses the local cache where possible. Understanding this process is the key to understanding why both go.mod and go.sum exist. Let’s walk through it step by step. Step 1: Go Reads go.mod The first thing the Go toolchain does is look for a go.mod file in your project. Project │├── go.mod ├── go.sum └── main.go This file tells Go three important things: Which module is being built. Which version of Go the project targets. Which dependencies (and versions) are required. At this point, Go has a blueprint for the project but it still doesn’t have the actual code for every dependency. It now needs to resolve the dependency graph. Step 2: Go Resolves Every Dependency Suppose your project imports Gin. import "github.com/gin-gonic/gin" Your go.mod contains: require github.com/gin-gonic/gin v1.11.0 Go now asks: “Do I already have version v1.11.0 of Gin available?" If the answer is yes, it can reuse it. If the answer is no, it downloads that version from the module proxy (or directly from the source repository if necessary). But Go doesn’t stop there. Gin itself depends on other libraries. Those libraries depend on even more libraries. Go continues resolving dependencies until it has the complete dependency graph required to build your project. A simplified view looks like this: Your Project │ ▼ Gin ╱ ╲ JSON Validator │ │ ▼ ▼ Bytes x/net Even though your code imports only gin, Go automatically discovers and resolves every dependency beneath it. Step 3: Every Download Is Verified Downloading code from the internet raises an important question. How does Go know that the code it downloaded is the same code the package author published? What if: the download was corrupted? a proxy server was compromised? someone maliciously modified the package? This is exactly where go.sum comes in. Every downloaded module is verified using cryptographic hashes before Go trusts it. We’ll explore this mechanism in detail in the next section. Step 4: Modules Are Cached Once a dependency has been downloaded and verified, Go stores it in the local module cache. On most systems, you’ll find it under: $GOPATH/pkg/mod or, more commonly today, $(go env GOMODCACHE) The next time you build the project, Go doesn’t need to download those modules again. Instead, it reuses the cached copies, making subsequent builds much faster. This cache is also one of the reasons Dockerfiles often copy go.mod and go.sum before the application source. We'll revisit that later. Step 5: Go Compiles Your Project Only after: resolving every dependency, verifying each one, and ensuring the dependency graph is complete, Go beginsThe module path acts much like a compiling your code. Finally, it links everything together into a single executable. From the outside, all you saw was: go build Under the hood, the process looked more like this: go build │ ▼ Read go.mod │ ▼ Resolve dependency graph │ ▼ Download missing modules │ ▼ Verify each module (using go.sum) │ ▼ Use local module cache │ ▼ Compile source code │ ▼ Generate executable Notice something interesting. So far, go.sum has appeared only once in this entire process. It isn’t responsible for deciding which dependency version to download — that’s go.mod's job. Instead, it answers a different question: “How do we know the downloaded dependency hasn’t been modified?” That distinction is one of the most commonly misunderstood parts of Go Modules. Let’s understand why go.sum exists and why deleting it is often more harmful than people realise. Why go.sum Exists go.mod contributes to selecting the build list. go.sum records checksums for module content and module files that the project has needed. It answers a separate question: did Go download the expected content for this module version? The checksums are SHA-256 based hashes. If a downloaded module does not match an expected checksum, Go stops rather than compiling it. A typical go.sum looks like this: github.com/gin-gonic/gin v1.11.0 h1:5J8... github.com/gin-gonic/gin v1.11.0/go.mod h1:b3M.. At first glance it looks intimidating. But every line has meaning. github.com/gin-gonic/gin The module. v1.11.0 The version. h1:5J8... The cryptographic checksum. Whenever Go downloads this module again, it computes the hash of the downloaded contents. If the newly computed hash matches the one in go.sum, the module is trusted. If it doesn’t match… verifying module: checksum mismatch SECURITY ERROR The build immediately stops. Go refuses to compile code that doesn’t exactly match the expected checksum. This protects your project from accidental corruption and many classes of supply-chain attacks. Why Are There Two Entries? One thing that confuses many developers is seeing two entries for the same module. github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0/go.mod Why? Because Go verifies two different things. The first checksum verifies the entire module source code. github.com/gin-gonic/gin v1.11.0 The second verifies only the module’s go.mod file. github.com/gin-gonic/gin v1.11.0/go.mod Why verify the module file separately? Because Go often needs dependency information before downloading the complete source code. Verifying the module definition independently makes dependency resolution both faster and more secure. Does go.sum Decide Which Version Gets Installed? No. This is probably the biggest misconception about Go Modules. Suppose you have require github.com/gin-gonic/gin v1.11.0 inside go.mod. Go downloads v1.11.0 because go.mod told it to. After downloading it, Go checks Is the downloaded v1.11.0 exactly the same version that was originally verified? using go.sum. So the responsibilities are completely different. go.mod : which dependency version should be used go.sum : Is that downloaded dependency authentic? Should You Commit go.sum? Absolutely. Some developers think: “Go can regenerate it anyway.” Technically, yes. But that completely misses its purpose. Imagine this scenario. You delete go.sum. Your teammate clones the repository a month later. One dependency has been compromised upstream. Since the original checksums are gone, Go has nothing to compare against. The trust chain has been weakened. Keeping go.sum in version control ensures everyone verifies dependencies against the same known-good checksums. That’s why almost every Go repository contains both files. go.mod go.sum Both should almost always be committed. When Does go.sum Change? You’ll notice Git often shows changes in go.sum. This usually happens when you go get go mod tidy go test go build Why? Because Go discovered a dependency it hadn’t downloaded before. Once downloaded, Go records its checksum. Even indirect dependencies can add new lines. That is completely normal. Understanding Every Important Directive in go.mod Most developers only ever notice module go require But go.mod supports several additional directives that become extremely useful in real-world development. replace One of the most useful directives. Imagine you’re developing two projects together. project-api shared-library Instead of publishing the library every time you make a change, you can tell Go to use the local copy. replace github.com/arpit/shared => ../shared Now every import of github.com/arpit/shared actually points to ../shared When is it useful? developing multiple repositories together testing local bug fixes working on forks temporarily replacing broken dependencies Example: replace github.com/gin-gonic/gin => github.com/my-fork/gin v1.11.0 This tells Go to use your fork instead of the original project. Should replace be committed? Usually only if the whole team needs it. A local path like replace github.com/company/lib => /Users/arpit/Desktop/lib works only on your machine and will break builds for everyone else. exclude Suppose a dependency releases a bad version. v1.5.2 You know it’s broken. Tell Go never to use it. exclude github.com/example/lib v1.5.2 Go will ignore that version during dependency resolution. This is uncommon in application code but useful for library maintainers. retract Used by module authors. Suppose you accidentally publish v1.4.0 and later discover a critical bug. You can retract it. retract v1.4.0 Now users will receive warnings when upgrading. It doesn’t delete the release. It simply marks it as one that should no longer be used. toolchain Introduced in newer Go versions. toolchain go1.24.2 This tells Go which toolchain version should be used. If your machine doesn’t have it installed, newer Go versions can automatically download the appropriate toolchain. This makes builds even more reproducible across different development environments. Common Go Module Commands Initialise a module go mod init github.com/user-name/project Creates a new go.mod. Add or upgrade a dependency go get github.com/gin-gonic/gin Updates go.mod and go.sum. Remove unused dependencies go mod tidy One of the most commonly used commands. It just : removes unused dependencies adds missing dependencies cleans go.sum Run it before every commit. Download dependencies go mod download Downloads modules into the cache without building. Very common inside Dockerfiles. Verify downloaded modules go mod verify Checks every cached dependency against the hashes stored in go.sum. Useful in CI pipelines. Explain why a dependency exists go mod why github.com/gin-gonic/gin Shows which package in your project requires that dependency. Great for debugging unexpectedly large dependency graphs. Why Dockerfiles Copy go.mod and go.sum First You’ve probably seen Dockerfiles like this: COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o app . Why not just copy everything? Because Docker caches layers. If only your application code changes but go.mod and go.sum stay the same, Docker reuses the cached dependency layer and skips downloading modules again. That can reduce build times dramatically. Common Mistakes Developers Make 1. Deleting go.sum to "fix" dependency issues Many developers delete go.sum when they encounter dependency errors because Go can regenerate it. While this may temporarily resolve some issues, it also removes the recorded checksums used to verify dependency integrity. The better approach is to understand the root cause instead of deleting an important security file. 2. Manually editing go.sum Unlike go.mod, go.sum is generated and maintained automatically by the Go toolchain. Editing it manually can introduce incorrect checksums, causing verification failures during builds. In almost every case, you should let Go update this file for you. 3. Forgetting to run go mod tidy As dependencies are added or removed, go.mod and go.sum can accumulate unused entries or miss required ones. Running go mod tidy cleans up unused dependencies, adds missing ones, and keeps your module files consistent. It's a good habit to run it before committing your code. 4. Committing machine-specific replace directives The replace directive is extremely useful during local development, but local filesystem paths only exist on your machine. replace github.com/company/lib => /Users/arpit/projects/lib If this is committed, teammates and CI pipelines won’t have the same directory, causing builds to fail. Only commit replace directives when they point to locations everyone can access, such as another module version or a shared repository. 5. Assuming go.sum stores versions A common misconception is that go.sum decides which dependency versions are installed, it doesn't. Version selection is handled entirely by go.mod. go.sum simply stores cryptographic hashes used to verify that the downloaded modules haven't been modified. 6. Assuming indirect dependencies don’t matter Just because your code doesn’t import a package directly doesn’t mean it’s unimportant. Indirect dependencies are required by the libraries your project uses, and without them your application may not build or run correctly. They are part of the complete dependency graph and contribute to reproducible builds. 7. Using go get when go install is more appropriate for installing tools A common mistake is using go get to install command-line tools. go get github.com/golangci/golangci-lint/cmd/golangci-lint Modern Go recommends using go install with an explicit version instead: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest go install installs the executable without modifying your project's go.mod or go.sum, keeping your application dependencies clean. go get is primarily intended for adding or updating dependencies used by your project. Final Thoughts Every Go developer sees go.mod and go.sum almost every day. It's easy to think of them as files that simply appear in every repository and occasionally change after running a command. But behind those two files is an entire system designed to solve one of the hardest problems in software engineering: building the same application, with the same dependencies, on every machine, every time. go.mod tells Go what your project should be built with. go.sum ensures that what Go downloads is exactly what it expects. Together, they make your builds reproducible, your CI pipelines reliable, your Docker images deterministic, and your dependency supply chain more secure. The next time you create a new Go project or review a pull request that changes these files, you’ll know they’re not just generated metadata, they’re part of the foundation that keeps Go builds predictable and trustworthy.

Jul 25, 20261 likes