Package Management

The Deployment That Broke Everything

It was Friday at 4:47 PM. I'd just merged a PR that upgraded our logging library. The build passed. Tests passed. I deployed to staging.

Everything broke.

The error was cryptic:

cannot find package "github.com/sirupsen/logrus"
  in any of:
    /usr/local/go/src/github.com/sirupsen/logrus (from $GOROOT)
    /home/deploy/go/src/github.com/sirupsen/logrus (from $GOPATH)

I spent the next 3 hours debugging. The problem? Our deployment script used go get without version pinning. The library had released v1.9.0 that day, breaking compatibility with v1.8.0. My local machine had v1.8.0 (working), but staging pulled v1.9.0 (broken).

In Python, I would've used requirements.txt with pinned versions:

logrus==1.8.0

In Go (pre-modules), I had to manually vendor dependencies or pray that go get grabbed compatible versions.

That Friday incident taught me: dependency management isn't optional. The next Monday, I learned about Go modules and never looked back.

This article covers everything I wish I'd known about Go's dependency management.


What Are Go Modules?

Go modules are Go's official dependency management system (introduced in Go 1.11, default since Go 1.16).

A module is a collection of related Go packages versioned together. It's defined by:

  • go.mod - Module definition and dependencies

  • go.sum - Cryptographic checksums for reproducible builds

Before Modules (The Dark Ages)

Problems:

  • No version pinning

  • No reproducible builds

  • Forced directory structure

  • Dependency hell

With Modules (The Light)


Creating Your First Module

Initialize a Module

The module path is typically your repository URL. For local-only projects:

Adding Your First Dependency

Create main.go:

Add the dependency:

Your go.mod now shows:

Note: The // indirect comment means it's a transitive dependency (dependency of a dependency).

The go.sum File

After running go mod download or building, you get go.sum:

This ensures reproducible builds - everyone gets the same versions.


Semantic Versioning in Go

Go modules use semantic versioning (semver): vMAJOR.MINOR.PATCH

  • MAJOR: Incompatible API changes (v1.x.x → v2.0.0)

  • MINOR: New features, backward compatible (v1.1.x → v1.2.0)

  • PATCH: Bug fixes, backward compatible (v1.1.1 → v1.1.2)

Version Selection

Major Version Suffixes

For v2+, Go requires the major version in the import path:

In go.mod:


Common go mod Commands

go mod init

Initialize a new module:

go get

Add or update dependencies:

go mod tidy

Remove unused dependencies and add missing ones:

Best practice: Run this before committing.

go mod download

Download dependencies to local cache:

Useful for CI/CD to cache dependencies.

go mod verify

Verify dependencies haven't been modified:

go mod graph

Show dependency graph:

go mod why

Explain why a dependency is needed:


Upgrading and Downgrading Dependencies

Check for Updates

Upgrade to Latest

Downgrade

Real Example: The Logging Library Upgrade

After my Friday disaster, here's how I handled upgrades:


The Replace Directive

The replace directive lets you override dependencies. Useful for:

  • Local development

  • Forking libraries

  • Testing patches

Replace with Local Path

Replace with Fork

Real Example: Patching a Bug

I once found a bug in a library we used. While waiting for the fix to be merged:

After the upstream fix was released:


Vendor Directory

The vendor directory stores dependencies in your repository.

Why Vendor?

  • Air-gapped environments (no internet access during build)

  • Guaranteed availability (dependencies can't disappear)

  • Faster CI builds (no download step)

Creating Vendor Directory

Using Vendor

Should You Vendor?

Yes, if:

  • Deploying to restricted environments

  • Repository policy requires it

  • Building Docker images (can cache vendor)

No, if:

  • Using modern CI/CD (can cache GOMODCACHE)

  • Repository size matters

  • Team prefers lighter repos

My approach: Don't vendor unless required. Go modules + caching work great.


Private Modules

Using Private Repositories

Tell Go to bypass proxy for private repos:

Authentication

For HTTPS:

For SSH:

Real Example: Internal Libraries

At my company, we had internal Go libraries:

In code:


Real Example: Complete Project Setup

Here's how I set up new projects after learning modules:

1. Initialize Project

2. Add Dependencies

3. Write Code

main.go:

4. Clean Up

5. Final go.mod

6. Commit


Best Practices

1. Always Commit go.sum

2. Run go mod tidy Before Committing

3. Pin Versions in Production

4. Review Dependency Updates

5. Use GOPRIVATE for Private Repos

6. Minimal Dependencies

Every dependency is a liability:

  • Security vulnerabilities

  • Compatibility issues

  • Build complexity

Ask: Do I really need this library? Can I write it myself?


Your Challenge

Create a CLI tool with proper dependency management:


Key Takeaways

  1. go.mod: Defines module and dependencies with versions

  2. go.sum: Ensures reproducible builds with checksums

  3. Semantic versioning: MAJOR.MINOR.PATCH version scheme

  4. go get: Add/update dependencies

  5. go mod tidy: Clean up unused dependencies

  6. Replace directive: Override dependencies for local dev or forks

  7. Vendor: Store dependencies in repository

  8. GOPRIVATE: Access private repositories


What I Learned

That Friday deployment disaster taught me that dependency management is critical:

  • go.mod gave me reproducible builds - same versions everywhere

  • go.sum prevented supply chain attacks - cryptographic verification

  • Semantic versioning made upgrades predictable - know what breaks

  • Zero deployment surprises for 2 years since adopting modules

Coming from Python's virtualenv/requirements.txt chaos and Node's node_modules hell, Go modules felt elegant. One command (go mod tidy), two files (go.mod, go.sum), zero surprises.

The time saved debugging dependency issues? Countless hours.


Next: Testing in Go

In the next article, we'll explore Go's testing framework. You'll learn table-driven tests, benchmarks, and the TDD approach that caught bugs before they hit production.

Last updated