The v2+ import path problem
You just tagged your library v2.0.0. The breaking changes are intentional. You update the go.mod file to require Go 1.21, push the tag, and wait for the proxy to pick it up. A colleague tries to use the new version in a project. They run go get example.com/mylib@v2.0.0. The build fails with a confusing error about import paths. Or worse, the code compiles, but the runtime behavior is wrong because the tooling resolved to v1.9.0 instead.
This is the v2+ import path problem. It happens when a module reaches major version 2 or higher but the import path doesn't reflect that version. Go modules enforce a strict rule: any module with a version v2.0.0 or greater must include the major version in its module path. The path becomes the unique identifier for that version line. If the path doesn't match the version, the tooling rejects the module or imports the wrong code.
Why Go changes the path for major versions
Go modules treat the import path as the identity of a package. The path is how the compiler finds the code. When you import example.com/mylib, the toolchain looks for a module with that exact path.
Semantic versioning says major versions can break the API. Go takes this seriously. If you could just update example.com/mylib from v1.5.0 to v2.0.0, every project depending on v1.5.0 would suddenly break when they updated. Go prevents this by forcing major versions to live on separate paths.
Think of the import path like a street address. 123 Main St is the v1 line. If you demolish the building and construct something completely different, you can't keep the same address. You need a new address. In Go, the new address is 123 Main St /v2. The /v2 suffix tells the toolchain that this is a different module from the v1 line, even though they share the same repository.
This design allows a single project to depend on both example.com/mylib (v1) and example.com/mylib/v2 (v2) at the same time. The compiler keeps them distinct because their import paths are different. You can use v1 for legacy code and v2 for new features without conflict.
The import path is the version. Change the version, change the path.
Minimal example: the mismatch and the fix
Here's the mistake that triggers the error. The module path lacks the version suffix, but the version number is v2.
// go.mod
module example.com/mylib
go 1.21
// main.go
package main
import (
"example.com/mylib" // Wrong: missing /v2 suffix for major version 2+
)
func main() {
_ = mylib.Version
}
If you try to publish this module as v2.0.0, the Go toolchain rejects it. The error is module path example.com/mylib does not match major version v2. The tooling expects the module path to end with /v2 when the version is v2 or higher.
If you import this module in another project, the compiler complains with example.com/mylib@v2.0.0: is a module, but the import path example.com/mylib does not match the module path example.com/mylib/v2. The import path must match the module path declared in go.mod.
Here's the corrected setup. The module path includes the version suffix, and imports follow suit.
// go.mod
module example.com/mylib/v2
go 1.21
// main.go
package main
import (
"example.com/mylib/v2" // Correct: matches module path exactly
)
func main() {
_ = mylib.Version
}
The module path in go.mod is example.com/mylib/v2. The import statement uses example.com/mylib/v2. The toolchain resolves the dependency correctly. The package name inside the code is still mylib, derived from the last directory segment before the version suffix. The /v2 is invisible to the Go code logic; it only affects the module resolver.
How the toolchain resolves v2 modules
When you run go get example.com/mylib/v2@v2.0.0, the toolchain fetches the module at that path. It reads the go.mod file and checks the module directive. The directive must be example.com/mylib/v2. If it's example.com/mylib, the fetch fails.
The import path determines the package name. Go imports use the last element of the path as the identifier. import "example.com/mylib/v2" gives you the identifier mylib. You write mylib.Func(), not v2.Func(). The suffix is part of the path, not the package name.
This rule applies to subpackages too. If your module has a package at example.com/mylib/pkg, the v2 path is example.com/mylib/v2/pkg. The /v2 sits between the module root and the package name. You don't append /v2 to every package. The suffix belongs only to the module path.
// go.mod
module example.com/mylib/v2
go 1.21
// pkg/helper.go
package helper
// Helper returns a value from the v2 package.
func Helper() string {
return "v2"
}
// main.go
package main
import (
"example.com/mylib/v2/pkg" // Subpackage inherits /v2 from module path
)
func main() {
_ = pkg.Helper()
}
The import example.com/mylib/v2/pkg resolves to the helper package inside the v2 module. The module path provides the /v2 context. The subpackage path extends the module path. You never write example.com/mylib/pkg/v2.
Go modules treat major versions as separate packages. You can import both without conflict.
Realistic example: mixing v1 and v2
A common scenario is a project that depends on a library for legacy reasons but needs a new feature only available in v2. Go modules allow both versions to coexist. The import paths keep them distinct.
Here's a project that uses both v1 and v2 of the same library. The import paths keep them distinct.
// go.mod
module example.com/app
go 1.21
require (
github.com/some/lib v1.5.0
github.com/some/lib/v2 v2.1.0
)
// main.go
package main
import (
"github.com/some/lib" // v1.5.0
"github.com/some/lib/v2" // v2.1.0
)
func main() {
// Use v1 for legacy code
_ = lib.LegacyFunc()
// Use v2 for new features
_ = lib.NewFeature()
}
The require block lists both versions. github.com/some/lib resolves to v1.5.0. github.com/some/lib/v2 resolves to v2.1.0. The compiler sees two different import paths, so it treats them as two different packages. The code can call functions from both.
This works because the module paths are different. The v1 module is github.com/some/lib. The v2 module is github.com/some/lib/v2. They are independent dependencies. You can upgrade v2 without affecting v1. You can drop v1 when the legacy code is gone.
The module path is the contract. Once published, changing the module path is a breaking change for everyone. Treat the module path like a URL that never changes.
Pitfalls and compiler errors
The v2+ rule catches mistakes early, but the errors can be confusing if you don't know what to look for.
Module path mismatch. If go.mod declares module example.com/mylib but you tag v2.0.0, the toolchain rejects the version. The error is module path example.com/mylib does not match major version v2. Fix the module path in go.mod to include /v2.
Import path mismatch. If the module path is example.com/mylib/v2 but you import example.com/mylib, the compiler fails with example.com/mylib: module example.com/mylib@latest found (v1.0.0), but does not contain package example.com/mylib. The import must match the module path. Update the import to example.com/mylib/v2.
Subpackage suffix error. If you write import "example.com/mylib/pkg/v2", the compiler complains with example.com/mylib/pkg/v2: module example.com/mylib/pkg/v2@latest found, but does not contain package example.com/mylib/pkg/v2. The suffix belongs in the module path, not the subpackage path. Use example.com/mylib/v2/pkg.
Unused imports. Go rejects unused imports. If you add example.com/mylib/v2 but don't use it, the compiler says example.com/mylib/v2 imported and not used. Remove the import or use the package.
Version suffix in v1. If you add /v2 to a v1 module, the toolchain rejects it. The error is module path example.com/mylib/v2 has version suffix /v2, but version is v1.0.0. Only use the suffix for major version 2 or higher.
The compiler catches path mismatches early. Fix the path, not the version number.
Decision: when to use version suffixes
Use the /v2 suffix when your module reaches major version 2.0.0 or higher. Use the plain path without suffix for major version 1.x.x. Use go mod edit -module=... to rename the module path when upgrading to v2. Use replace directives only for local development or vendoring, not to bypass version rules. Use separate modules for truly independent components, even if they share a repository.
Version suffixes are for major breaks. Minor updates stay on the same path.