Understanding Go Packages and Modules: The Two-Layer System
Discover why Go separates packages from modules, and how this design enables scalable, reproducible builds
Part 3 of 3
This article
6 min read
Series total
18 min
Remaining
6 min
Progress
100%
Why Go separates code organization from dependency management.
I remember the moment this finally clicked.
I had created a simple project:
main.go
auth/auth.go
Then I tried to import my local package:
import "auth"
And Go replied:
package auth is not in std
My first thought was:
โThe folder is right there. Why canโt Go find it?โ
Then I ran:
go mod init swn
Changed the import to:
import "swn/auth"
and suddenly everything worked.
That experience taught me something important:
In Go, packages and modules solve two different problems.
- Packages organize code.
- Modules give that code a stable, reproducible namespace.
Once you see the separation, Goโs import system stops feeling strange and starts feeling surprisingly elegant.
The Simpler Layer: Packages
Letโs start with packages.
A package is simply a folder containing Go files that belong together.
Suppose you have authentication logic.
swn/
โโโ auth/
โโโ login.go
โโโ token.go
Both files begin with:
package auth
This tells Go:
โThese files are part of the same package.โ
You can think of a package as a logical unit of responsibility.
authโ authenticationdatabaseโ database accesshandlersโ HTTP handlersmodelsโ data structures
Packages are not about dependencies yet. They are about structure and boundaries.
The Special Package: main
There is one exception: main.
package main
func main() {
// program starts here
}
If your project has a main package with a main() function, Go builds an executable application.
Without it, you have a library, not a program.
This distinction is easy to miss when starting out.
The Capitalization Rule
Go has no public or private keywords.
Instead, it uses a simple convention.
func Auth() {} // exported
func auth() {} // unexported
If a name starts with an uppercase letter, other packages can use it.
If it starts with lowercase, it stays internal.
This is one of those Go features that feels unusual for a day and then becomes hard to live without.
You can often understand a packageโs public API just by scanning for capitalized names.
Packages Solve Organization
So far, packages answer one question:
โWhat code belongs together?โ
As a project grows, this becomes essential.
swn/
โโโ auth/
โโโ database/
โโโ handlers/
โโโ models/
Each package has a clear responsibility.
This is how Go encourages modular design without heavy frameworks.
The Missing Piece
Now comes the confusing part.
If packages are just folders, why didnโt this work?
import "auth"
Because Go still doesnโt know which auth package you mean.
- A local package?
- A package from GitHub?
- A package from another project on your machine?
This is the problem that modules solve.
The Second Layer: Modules
A module is defined by a go.mod file.
module swn
go 1.25
This file does two important things:
- Names your project (
swn) - Tracks its dependencies
Think of a module as the root identity of your codebase.
The Key Insight
Once you declare:
module swn
Go treats every package inside the project as part of that module.
So this folder:
swn/
โโโ auth/
becomes the package path:
swn/auth
Now the import is unambiguous:
import "swn/auth"
This is the moment that usually makes the whole system click.
Packages Need a Namespace
Hereโs the mental model I wish I had earlier.
- Module โ
swn - Package โ
auth - Import path โ
swn/auth
The module provides the namespace.
The package provides the organization.
Together they create the import path.
Local and External Imports Use the Same Rule
This is a subtle but beautiful design choice.
| What | Import |
|---|---|
| Local package | swn/auth |
| External package | github.com/lib/pq |
Notice that both are just module paths.
Go does not have separate syntax for local and remote dependencies.
That consistency becomes incredibly valuable in large systems.
A Real Project Walkthrough
Suppose your project looks like this:
swn/
โโโ go.mod
โโโ main.go
โโโ auth/
โโโ auth.go
go.mod
module swn
auth/auth.go
package auth
func Auth(name string) string {
return "Welcome, " + name
}
main.go
package main
import (
"fmt"
"swn/auth"
)
func main() {
fmt.Println(auth.Auth("Naveen"))
}
What happens?
- Go reads the module name:
swn - It resolves
swn/authinside the current module - It loads the
authpackage - It allows access to the exported
Authfunction
Why Not Just Use Relative Imports?
Some languages allow imports like:
import "./auth"
Go intentionally avoids this for normal application code.
Why?
Because relative imports become fragile when:
- files move,
- directories are renamed,
- projects grow,
- or code is reused.
Go prefers stable, module-based paths.
This is a classic Go trade-off:
Slightly more explicit today, much less painful tomorrow.
The Two-Layer System
Here is the entire model in one diagram.
Module layer
module swn (identity + dependency management)
Packages
swn/auth
swn/database
swn/handlers
Application layer
package main (entry point + composition)
This is why I call it a two-layer system.
- Layer 1: Packages structure the code.
- Layer 2: Modules make the structure reproducible.
Why This Matters in the Real World
Imagine a Kubernetes controller with hundreds of packages.
Without modules:
- dependency versions drift,
- imports become ambiguous,
- builds become inconsistent.
Without packages:
- everything ends up in a few giant files,
- boundaries disappear,
- refactoring becomes dangerous.
Goโs separation allows both modularity and reproducibility.
That combination is a big reason Go scales well for infrastructure and backend systems.
The Deeper Design Philosophy
What I appreciate most is that this system reflects a broader Go principle:
- Packages โ clear boundaries
- Modules โ explicit dependencies
- Imports โ visible relationships
Nothing is hidden.
The structure is visible in the filesystem, in go.mod, and in the import statements.
What Finally Clicked for Me
I used to think:
โWhy do I need both packages and modules?โ
Now I think:
- Packages answer: How should I organize my code?
- Modules answer: How should this code be identified and reproduced?
Those are different questions.
And once I separated them mentally, Goโs import system stopped feeling like an arbitrary rule and started feeling like a carefully designed scaling mechanism.
The next time you run:
go mod init swn
youโre not just enabling imports.
Youโre giving your codebase an identity, a namespace, and the foundation for reproducible builds as it grows from one file to hundreds of packages.
Thatโs the real reason Go needs both packages and modules.
Series Progress
3 of 3Why Go's Variable Syntax is Different (And What It Solves)
Last article
๐ง Related Knowledge Network
Interconnected content based on shared technologies, topics, and research
๐Related Articles
4 items
How Go Variables Work: Memory, Safety, and the Runtime
Understand what happens in memory when you declare a variable, how Go manages memory safely, and how design choices enable automatic optimization
Why Go Separated Packages from Modules: A Design Philosophy
Explore the design decisions behind Go's two-layer system, how it solves real problems, and what it teaches us about building scalable systems
Why Go's Variable Syntax is Different (And What It Solves)
Understand why Go's var x int syntax differs from C and Python, and how this choice enables reading code at scale
Why Google Created Go: The Engineering Problems That Changed Cloud Computing Forever
Discover the engineering challenges at Google that led to Go's creation and how it revolutionized cloud computing
โจ Found 4 related knowledge items across 1 categories