Module vs Package vs Package Path
Go has a confusing terminology around modules, packages, and package paths. This post aim to provide you simple but sufficient mental model to go modules.
Module
Russ Cox define go module as a collection of packages sharing a common import path prefix, known as the module path. A more intuitive mental model would be describe as a collection of Go packages that are versioned together as a single unit. It’s defined by a go.mod file at its root:
module github.com/user/myproject
go 1.21Think of a module as a “project” - it can contain multiple packages, but they all share the same versioning lifecycle. When you run go mod init, you’re creating a module.
Package
A package is a directory containing Go source files that all belong to the same namespace (we also call this package name). Every .go file in the same directory must declare the same package name:
// In file: myproject/auth/login.go
package auth
// In file: myproject/auth/token.go
package auth // Must be the same package nameThe package name (what comes after package) is what you use in your code to reference exported identifiers from that package.
Package Path
The package path is the import path used to import a package. It’s defined as:
module name + "/" + [the directory path relative to the module root]A concrete example is shown below:
Module: github.com/user/myproject
Directory: auth/
Package Path: github.com/user/myproject/authThe Confusing Part
Here’s where it gets tricky - the package name and the last element of the package path don’t have to match:
// Directory structure:
// project/
// go.mod (module github.com/user/project)
// authentication/
// login.go
// In login.go:
package auth // Package name is "auth"
// To import:
import "github.com/user/myproject/authentication" // Package path
// To use:
auth.Login() // Use package name, not "authentication"That’s because compiler use package path to find the package, but the developer use package name (define by package <name> in each go file) in the actual source code.
Important
While you can have different package names and directory names, it’s confusing and considered bad practice. Stick to matching them unless you have a very good reason.
Main Package Special Case
The main package is special - it’s the entry point for executable programs:
// In any directory, usually cmd/ or root
package main
func main() {
// This is where your program starts
}You can have multiple main packages in different directories within the same module, each creating a separate executable.
Module Versioning & Import Compatibility
Go’s module system follows Russ Cox’s import compatibility rule, which is the foundation of everything that makes Go modules work without the complexity hell of other package managers.

The Import Compatibility Rule
If an old package and a new package have the same import path, the new package must be backwards-compatible with the old package.
This is THE rule that saves you from dependency hell. Here’s why this matters:
// If your code works with v1.2.3
import "github.com/some/package"
// It MUST work with v1.9.5 (same import path)
import "github.com/some/package" // Still works!
// But v2.0.0 gets a different import path
import "github.com/some/package/v2" // Breaking changes allowedThis means updating from v1.2.3 → v1.9.5 should never break your build. If it does, that’s a bug in the package author’s v1.9.5, not expected behavior.
Minimal Version Selection
Unlike most package managers that use the “newest allowed version” (which changes when new releases come out), Go uses minimal version selection - it picks the oldest version that satisfies all requirements.
Your module requires: [email protected]
Some dependency requires: [email protected]
Result: Go picks v1.5.0 (the minimum that satisfies both)This gives you:
- Reproducible builds: Same result today and tomorrow
- No surprise breakage: Won’t suddenly use newer versions
- Simple conflict resolution: Just pick the highest minimum requirement
Major Version Handling
When package authors need to make breaking changes, they bump the major version and change the import path:
// v1.x.x - Original API
import "github.com/yaml/yaml"
// v2.x.x - Breaking changes, new import path
import "github.com/yaml/yaml/v2"
// You can use both in the same program!
func convert() {
oldData := yaml.Marshal(data) // v1 API
newData := yamlv2.Marshal(data) // v2 API
}This lets different parts of a large program migrate from v1 → v2 independently, avoiding the “diamond dependency problem” that plagues other ecosystems.
Why This Works
Traditional package managers try to solve incompatibility with complex constraint solving. Go eliminates incompatibility instead:
- No conflicting constraints: If two packages both import
foo, they get the samefoo(unless they explicitly import different major versions) - No “dependency hell”: There’s always exactly one valid solution
- No lock files needed: The minimal version selection algorithm is deterministic