JavaRush /Courses /Go SELF /vendor/ in Go: when you need it and what it changes

vendor/ in Go: when you need it and what it changes

Go SELF
Level 35, Lesson 3
Available

1. Introduction

If you have already come to love the idea “dependencies are described in go.mod, and Go will download everything by itself”, then the appearance of the vendor/ folder can feel like a sudden comeback from the past: “Why would we copy someone else’s code into the repository—are we archivists?”. But practice quickly reminds you that the real world loves plots like “the internet got turned off”, “the proxy doesn’t work”, “production isn’t allowed to access the network”, “we have to pass an audit”, “the build must be reproducible two years from now, when the library has already disappeared”.

Vendoring is like an “emergency dependency kit” in the trunk. Usually you drive fine without it, but under certain conditions it turns from a weird option into the only way to make it. And it’s important to understand not only “how to enable it”, but also what exactly changes in build behavior and in team life.

2. What vendor/ is and what changes during builds

vendor/ is a directory in your repository where the source code of external dependencies is placed (usually those described in go.mod as require, plus their transitive dependencies). That is, you keep a copy of someone else’s code “next to yours” so the build can take dependencies not from the network and not from the module cache, but from the repository.

Historically, this is not a random tradition: the Go community actively used the “copy dependencies into the repository” approach even before modules, and tool-level support was added because doing it manually was too inconvenient and fragile. There were external tools (godep, glide, etc.) that copied dependencies into the repository, and later Go added explicit support for vendor directories to make this workflow simpler.

Important: vendor/ is not a replacement for go.mod/go.sum. It is specifically a “local copy of sources”, not a new version selection system.

How vendor/ affects import resolution

When you add vendor/, you do not rewrite imports in the code. Your code still writes:

import "github.com/google/uuid"

And this is essential: the import path is “what the package is called in the module world”, while vendor/ is only “where to get the sources for that import path” during the build.

The change happens inside the go tool: it can (depending on the mode) use vendor/ as the source of dependencies during builds and tests.

A simple diagram (roughly):

go build / go test
   |
   v
Import resolution (import path -> sources)
   |
   +--> option A: take from module cache (normal mode)
   |
   +--> option B: take from ./vendor/... (vendoring mode)

So vendor/ affects not Go syntax, but dependency resolution: where the package is actually taken from.

3. vendor/, module cache, and replace: similar, but different meaning

To avoid confusion, it helps to keep a “sanity table” in mind—where dependency code lives and who decides what to use.

Approach Where the dependency code lives What you pin Typical motivation
Regular modules In the global module cache (downloaded once and reused) go.mod + go.sum “Normal life”: clean, compact, predictable
replace
In an alternative source (often a local folder on disk) A replacement rule in go.mod Local development/debugging of a dependency
vendor/
Directly in the repository (./vendor/...) A snapshot of dependency sources Offline / audit / hermetic build

Notice that replace changes the dependency “delivery route” (and can make the project depend on the folder layout of a specific machine), while vendor/ makes the repository self-sufficient in terms of dependency sources. At the same time, go.mod/go.sum still remain the “truth about versions”.

4. How vendor/ is created and what’s inside

The vendor/ folder should not appear “manually out of thin air”. In a normal project, it is generated by Go tools (for example, with a command like go mod vendor). I intentionally say “like”, because it’s more important to understand the idea: vendoring is a reproducible snapshot, not a place for manual tinkering.

Typical contents look roughly like this:

todoapp/
  go.mod
  go.sum
  main.go
  internal/...
  vendor/
    github.com/google/uuid/...
    golang.org/x/text/...
    modules.txt

The modules.txt file (often present) is a “registry” that helps tools understand which modules and versions you vendored.

And one more important detail: having vendor/ does not cancel the fact that dependency management commands continue to work through go.mod/go.sum. Even if the build takes sources from vendor/, the go get and go mod tidy commands are still about managing dependencies and keep modifying go.mod and go.sum, because their job is version selection, import synchronization, and checksum control.

5. When vendor/ is needed, and when it becomes baggage

There is a temptation to enable vendor/ “just in case”, like people sometimes install antivirus on top of antivirus so the virus is definitely scared. But vendor/ is not a free feature: it has a cost, so it’s better to understand the motivations.

The most common real-life scenario is a limited or prohibited network. For example, builds in CI/CD or in an isolated environment where downloading dependencies during the build is not allowed. Then vendor/ turns the project into a “box that already contains everything inside”.

The second scenario is audit and control. Sometimes organizations want to store an exact snapshot of dependency sources next to the code: so you can guarantee that you built the binary from specific sources, and those sources are available without external services.

The third scenario is long-term reproducibility. Modules, proxies, repositories—any of these can disappear or become unavailable. But a repository with vendor/ (if maintained correctly) remains self-sufficient.

The cost of vendor/: size, noise, and discipline

If vendor/ were pure magic with no downsides, all projects would use vendor/ by default, and we wouldn’t even be discussing this lecture (instead, we’d have a lecture about how to grow potatoes on Mars). But the downsides of vendoring are very down-to-earth.

  • First, the repository gets bigger. Sometimes a lot bigger. This affects clone size, operation speed, and the “onboarding threshold” for newcomers: someone opens the project and sees ten thousand files, and they haven’t written a single line yet.
  • Second, you get noisy diffs and merge conflicts. Even if you changed one dependency, vendor/ may update a large set of files. For a team, this means: “dependency updates” become a separate event that you need to be able to read and review.
  • Third, vendor/ requires discipline: it easily goes stale. You can update go.mod but forget to update vendor/, and then you effectively have two sources of truth in the project arguing with each other. And when two “truths” argue, the one that crashes louder in CI usually wins.

6. Mini example: UUID in a training todoapp and vendoring

Imagine our training app is a console todoapp that we gradually evolve. Suppose we want to generate task identifiers not manually, but via a UUID library. For the example, let’s take the popular package github.com/google/uuid (you can replace it with any other external package—the logic will be the same).

Let’s add a small wrapper package so we don’t drag UUID details throughout the code:

package id

import "github.com/google/uuid"

func New() string {
	return uuid.NewString()
}

Now, in the task creation code, we can do something like:

package todo

import "example.com/todoapp/internal/id"

func NewTask(title string) Task {
	return Task{ID: id.New(), Title: title}
}

Note: the import stays normal and “module-style”. No vendor/... appears in the code—and that is correct.

After that, go.mod will get a require for the needed module/version (this is done by go get or by IDE tools). And if you decide to use vendoring, you generate the vendor/ folder, and the project begins to contain a copy of that dependency’s sources.

The key point here is that vendor/ is not an alternative way to connect a package in code, but an alternative way to keep the dependency sources nearby so the build can rely on them. And even with vendor/ present, dependency management remains via go.mod/go.sum: the go get and go mod tidy commands still change these files, because that is their job.

7. How to live with vendor/ in a team

When a project chooses to use vendor/, it’s good for the team to follow a predictable ritual. Otherwise, a “snowball” of unsynchronized changes appears very quickly: one developer updated go.mod, another pulled the branch and got weird discrepancies, a third accidentally committed an edit directly into vendor/ (yes, that happens too, and it’s its own genre of pain).

Usually the process looks like this: you change dependencies “at the module level” (that is, via go.mod), then you bring go.sum into order, and only then you update vendor/ as a snapshot of the current state. This matters specifically as a sequence: first choose versions and pin them in the module files, and then make an “offline copy”.

And it helps to keep a simple framing in mind: vendor/ is a layer “about sources nearby”, not a new dependency manager. Versions are selected and pinned in go.mod, verifiability is provided by go.sum, and vendor/ is a way to make the build less dependent on the network and external infrastructure.

8. Typical mistakes when working with vendor/

Mistake No. 1: editing code directly inside vendor/ “because it’s fast”.
At first it feels like you’re a genius: “I fixed a bug in a dependency in 30 seconds”. Then it turns out you are now maintaining a dependency fork by hand—but without a fork, without history, without documentation, and without any realistic way to update cleanly. If you really need to patch a dependency, that’s a separate deliberate story, not a sudden edit inside vendor/.

Mistake No. 2: thinking that vendor/ replaces go.mod and go.sum.
That mindset breaks your mental model: you start ignoring go.mod, and then you’re surprised why tools keep changing go.sum, why the IDE complains, and why a dependency update didn’t “get absorbed” by the build. vendor/ is a copy of sources, while go.mod/go.sum remain the declaration of dependencies and their verification.

Mistake No. 3: updating go.mod but forgetting to update vendor/.
This is one of the most unpleasant kinds of desynchronization: the repository contains vendor/ with old contents, while go.mod already requires something else. As a result, the build in “one mode” takes one thing, and in “another mode” takes another, and you get stories like “it works on my machine”. It’s especially painful to catch when CI is configured differently from local development.

Mistake No. 4: treating vendor/ as a “default development speed booster”.
Sometimes it seems: “If dependencies are in the repo, it must be faster”. In practice, most often only offline builds get faster, while the cost is repository growth and more complex review. Vendoring is usually a solution for constraints (network, audit, policy), not a universal optimization.

Mistake No. 5: committing vendor/ without understanding “why”, because “it’s trendy”.
In some teams, vendor/ is a critically important part of the process; in others, it’s dead weight. If you don’t have requirements for hermetic builds and you don’t have problems accessing modules, vendor/ may well turn into a suitcase without a handle: hard to carry, and a shame to throw away.

1
Task
Go SELF, level 35, lesson 3
Locked
First vendor
First vendor
1
Task
Go SELF, level 35, lesson 3
Locked
Vendor won't save you
Vendor won't save you
1
Task
Go SELF, level 35, lesson 3
Locked
Broken vendor registry
Broken vendor registry
1
Task
Go SELF, level 35, lesson 3
Locked
Running without replace
Running without replace
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION