This tutorial gives you one complete mental model for:
- Module
- Package
- Subpackage
- Submodule
go.modgo.work- Local development
- Production/release setup
The most important idea is:
Repository
│
└── Module
│
├── Package
├── Package
│ └── Nested Package
└── Package
And sometimes:
Repository
│
├── Root Module
│
├── Submodule
├── Submodule
└── Submodule
These two designs are very different.
1. What is a Go module?
A module is a collection of Go packages managed together.
A module is identified by:
go.mod
Example:
shop/
├── go.mod
├── main.go
├── payment/
├── order/
└── customer/
Here:
shop
is one Go module because it contains:
shop/go.mod
Example go.mod:
module example.com/shop
go 1.26
The line:
module example.com/shop
defines the module’s import path.
2. What is a Go package?
A package is a group of related .go files inside the same directory.
Example:
shop/
├── go.mod
├── main.go
│
├── payment/
│ └── payment.go
│
├── order/
│ └── order.go
│
└── customer/
└── customer.go
There is only one module:
example.com/shop
But there are multiple packages:
main
payment
order
customer
For example:
// payment/payment.go
package payment
import "fmt"
func Pay() {
fmt.Println("Payment completed")
}
You can call it from main.go:
package main
import "example.com/shop/payment"
func main() {
payment.Pay()
}
3. Module vs package
The easiest distinction is:
| Module | Package |
|---|---|
| Collection of packages | Collection of .go files |
Has go.mod | Normally does not have go.mod |
| Dependency/version boundary | Code organization boundary |
| Can contain many packages | Lives inside a module |
Example: example.com/shop | Example: payment |
Think:
Module
│
├── Package
├── Package
└── Package
Example:
shop ← Module
│
├── payment ← Package
├── order ← Package
└── customer ← Package
4. Is there a subpackage in Go?
Yes, you can organize packages inside other directories.
Example:
shop/
├── go.mod
│
└── customer/
├── customer.go
│
└── validation/
└── validation.go
Humans may call:
customer/validation
a subpackage.
But Go does not give it any special parent-child relationship.
Go sees:
example.com/shop/customer
and:
example.com/shop/customer/validation
as two separate packages.
Example:
// customer/validation/validation.go
package validation
func IsValid(name string) bool {
return name != ""
}
Use it:
package main
import (
"fmt"
"example.com/shop/customer/validation"
)
func main() {
result := validation.IsValid("Rajesh")
fmt.Println(result)
}
Output:
true
Important rule:
Directory = Package
Usually:
customer/
is one package.
And:
customer/validation/
is another package.
5. What is a submodule?
Go does not have a special language feature called submodule.
But you can create a nested module inside another repository.
If a directory contains its own:
go.mod
it becomes a separate module.
Example:
shop/
│
├── go.mod
├── main.go
│
├── payment/
│ ├── go.mod
│ └── payment.go
│
├── order/
│ ├── go.mod
│ └── order.go
│
└── customer/
├── go.mod
└── customer.go
Now you have four modules.
shop
payment
order
customer
You can think of:
payment
order
customer
as submodules or nested modules.
6. Root module + 3 submodules example
We will build:
shop/
│
├── go.mod
├── main.go
│
├── payment/
│ ├── go.mod
│ └── payment.go
│
├── order/
│ ├── go.mod
│ └── order.go
│
└── customer/
├── go.mod
└── customer.go
7. Create root module
Create project:
mkdir shop
cd shop
Create root module:
go mod init example.com/shop
This creates:
go.mod
with:
module example.com/shop
go 1.26
8. Create payment submodule
Run:
mkdir payment
cd payment
go mod init example.com/shop/payment
Create:
payment.go
package payment
import "fmt"
func Pay() {
fmt.Println("Payment completed")
}
Your payment module now looks like:
payment/
├── go.mod
└── payment.go
payment/go.mod:
module example.com/shop/payment
go 1.26
Go back:
cd ..
9. Create order submodule
Run:
mkdir order
cd order
go mod init example.com/shop/order
Create:
// order.go
package order
import "fmt"
func CreateOrder() {
fmt.Println("Order created")
}
order/go.mod:
module example.com/shop/order
go 1.26
Go back:
cd ..
10. Create customer submodule
Run:
mkdir customer
cd customer
go mod init example.com/shop/customer
Create:
// customer.go
package customer
import "fmt"
func CreateCustomer() {
fmt.Println("Customer created")
}
customer/go.mod:
module example.com/shop/customer
go 1.26
Go back:
cd ..
11. Root main.go
Create:
shop/main.go
Code:
package main
import (
"example.com/shop/customer"
"example.com/shop/order"
"example.com/shop/payment"
)
func main() {
customer.CreateCustomer()
order.CreateOrder()
payment.Pay()
}
Notice:
customer.CreateCustomer()
means:
package.function
Similarly:
order.CreateOrder()
payment.Pay()
12. Why this does not immediately work
You now have four independent modules:
example.com/shop
example.com/shop/payment
example.com/shop/order
example.com/shop/customer
The root module does not automatically know that:
./payment
should represent:
example.com/shop/payment
This is where go.work becomes useful.
13. What is go.work?
go.work defines a Go workspace.
A workspace tells Go:
Use these local modules together while I am developing.
From the root:
go work init .
Then:
go work use ./payment
go work use ./order
go work use ./customer
Now:
shop/
├── go.work
├── go.mod
├── main.go
├── payment/
├── order/
└── customer/
Your go.work will look approximately like:
go 1.26
use (
.
./customer
./order
./payment
)
Meaning:
Use root module
Use customer module
Use order module
Use payment module
14. Run the application locally
Run:
go run .
Output:
Customer created
Order created
Payment completed
The flow is:
main.go
│
├── customer.CreateCustomer()
│ │
│ ↓
│ customer/customer.go
│
├── order.CreateOrder()
│ │
│ ↓
│ order/order.go
│
└── payment.Pay()
│
↓
payment/payment.go
15. Why go.work is useful
Imagine you’re developing all modules together.
You modify:
payment/payment.go
Then immediately:
go run .
uses your local changed version.
You don’t have to publish:
payment v1.0.1
every time you make a small change.
So:
go.work
is mainly useful for:
local multi-module development
16. Development architecture
During development:
shop
│
main.go
│
┌────────────┼─────────────┐
│ │ │
↓ ↓ ↓
customer order payment
│ │ │
└────────────┼─────────────┘
│
go.work
│
Local directories
The root app imports:
import (
"example.com/shop/customer"
"example.com/shop/order"
"example.com/shop/payment"
)
But Go actually uses:
./customer
./order
./payment
because of go.work.
17. Production: what changes?
For production, normally dependencies should come from:
go.mod
instead of depending on your local workspace.
Production should generally use known module versions.
For example:
module github.com/company/shop
go 1.26
require (
github.com/company/shop/customer v1.0.0
github.com/company/shop/order v1.0.0
github.com/company/shop/payment v1.0.0
)
Now your dependencies are explicit:
customer v1.0.0
order v1.0.0
payment v1.0.0
18. Development vs production
Development
main.go
│
↓
go.work
│
├── ./customer
├── ./order
└── ./payment
Production
main.go
│
↓
go.mod
│
├── customer v1.0.0
├── order v1.0.0
└── payment v1.0.0
Simple rule:
Development → local code → go.work
Production → released/versioned code → go.mod
19. Real GitHub-style module example
Suppose your Git repository is:
github.com/company/shop
Root module:
module github.com/company/shop
go 1.26
Payment module:
module github.com/company/shop/payment
go 1.26
Order module:
module github.com/company/shop/order
go 1.26
Customer module:
module github.com/company/shop/customer
go 1.26
Your main.go becomes:
package main
import (
"github.com/company/shop/customer"
"github.com/company/shop/order"
"github.com/company/shop/payment"
)
func main() {
customer.CreateCustomer()
order.CreateOrder()
payment.Pay()
}
20. Publishing nested modules
Because each subdirectory is a separate module, each module can have its own version.
Example versions:
payment v1.0.0
order v1.0.0
customer v1.0.0
For nested modules in a single Git repository, tags are commonly:
git tag payment/v1.0.0
git tag order/v1.0.0
git tag customer/v1.0.0
Push tags:
git push --tags
Then the root module can depend on those versions.
Example:
module github.com/company/shop
go 1.26
require (
github.com/company/shop/customer v1.0.0
github.com/company/shop/order v1.0.0
github.com/company/shop/payment v1.0.0
)
Then build:
go mod tidy
go build .
21. What does go mod tidy do?
go mod tidy
makes sure:
go.mod
and:
go.sum
contain the dependencies your source code actually uses.
Think:
Source code imports
↓
go mod tidy
↓
Clean go.mod
Clean go.sum
22. What is go.sum?
go.sum records checksums for downloaded module dependencies.
Example structure:
shop/
├── go.mod
├── go.sum
└── main.go
You normally commit:
go.mod
go.sum
to Git.
go.sum helps Go verify that dependency contents have not unexpectedly changed.
23. Important: You usually do NOT need submodules
This is the most important practical advice.
For most applications, this:
shop/
├── go.mod
├── main.go
├── payment/
│ └── payment.go
├── order/
│ └── order.go
└── customer/
└── customer.go
is better.
You have:
1 Module
│
├── payment package
├── order package
└── customer package
There is only one:
go.mod
No go.work needed.
No separate versions needed.
No separate releases needed.
No dependency complexity.
24. Recommended normal project
Create:
mkdir shop
cd shop
go mod init example.com/shop
Structure:
shop/
├── go.mod
├── main.go
│
├── payment/
│ └── payment.go
│
├── order/
│ └── order.go
│
└── customer/
└── customer.go
25. payment package
package payment
import "fmt"
func Pay() {
fmt.Println("Payment completed")
}
26. order package
package order
import "fmt"
func CreateOrder() {
fmt.Println("Order created")
}
27. customer package
package customer
import "fmt"
func CreateCustomer() {
fmt.Println("Customer created")
}
28. Root main.go
package main
import (
"example.com/shop/customer"
"example.com/shop/order"
"example.com/shop/payment"
)
func main() {
customer.CreateCustomer()
order.CreateOrder()
payment.Pay()
}
Run:
go run .
Output:
Customer created
Order created
Payment completed
Notice something important:
The calling code looks almost identical to the multi-module example.
This:
payment.Pay()
doesn’t tell you whether payment is:
a package inside the module
or:
a completely separate module
The import path and module configuration determine that.
29. Single module vs multiple modules
Single-module design
shop/
│
├── go.mod
│
├── payment/
│
├── order/
│
└── customer/
Relationship:
shop module
│
├── payment package
├── order package
└── customer package
Best for:
- Normal applications
- APIs
- Web applications
- CLI tools
- Backend services
- Small and medium projects
- Packages released together
30. Multi-module design
shop/
│
├── go.mod
│
├── payment/
│ └── go.mod
│
├── order/
│ └── go.mod
│
└── customer/
└── go.mod
Relationship:
Repository
│
├── shop module
├── payment module
├── order module
└── customer module
Best when modules need:
- Independent versions
- Independent releases
- Independent dependencies
- Independent reuse
- Separate ownership
- Separate lifecycle
31. Comparison
| Feature | Package | Subpackage | Submodule |
|---|---|---|---|
| Separate directory | Usually | Yes | Yes |
Own go.mod | No | No | Yes |
| Independent version | No | No | Yes |
| Independent dependencies | No | No | Yes |
| Importable | Yes | Yes | Yes |
| Part of parent module | Yes | Yes | No |
| Common in normal apps | Very common | Common | Less common |
32. go.mod vs go.work
go.mod | go.work |
|---|---|
| Defines a module | Defines a workspace |
| Required for modules | Optional |
| Declares dependencies | Groups local modules |
| Used for builds/releases | Mostly useful for local multi-module development |
| Usually committed | Often developer/workspace-specific |
Contains require | Contains use |
Example go.mod:
module github.com/company/shop
go 1.26
require github.com/company/payment v1.0.0
Example go.work:
go 1.26
use (
.
./payment
)
Mental model:
go.mod
"What does this module depend on?"
versus:
go.work
"Which local modules am I developing together?"
33. What does import actually import?
Suppose:
import "example.com/shop/payment"
Go reads this as an import path.
Then:
payment.Pay()
means:
package identifier
.
exported function
So:
payment.Pay()
is:
payment package
↓
Pay function
34. Why is Pay capitalized?
In Go:
func Pay()
starts with capital P.
Therefore it is:
exported
Other packages can call it.
But:
func pay()
starts with lowercase.
Therefore it is:
unexported
Only code inside the same package can call it.
Example:
package payment
func Pay() {
}
Can be called:
payment.Pay()
But:
package payment
func pay() {
}
cannot be called from main.
35. Package name vs module path
Consider:
module example.com/shop
and:
payment/payment.go
with:
package payment
The full import path becomes:
example.com/shop/payment
Break it down:
example.com/shop / payment
│ │
Module Package
36. Complete hierarchy
The clean mental hierarchy is:
Git Repository
│
├── Module
│ │
│ ├── Package
│ │ │
│ │ ├── File
│ │ │ ├── Function
│ │ │ ├── Struct
│ │ │ ├── Method
│ │ │ └── Interface
│ │
│ └── Package
│
└── Optional separate Module
Example:
shop repository
│
├── go.mod
│
├── payment/
│ └── payment.go
│ │
│ └── Pay()
│
└── order/
└── order.go
│
└── CreateOrder()
37. Root module terminology
Suppose:
shop/
├── go.mod
└── payment/
└── payment.go
shop is the module root because:
go.mod
exists there.
Everything below it normally belongs to the same module until Go encounters another go.mod.
So:
shop/
├── go.mod ← Module A
│
├── customer/ ← Module A
├── payment/ ← Module A
│
└── tools/
├── go.mod ← Module B begins here
└── tool.go
The second go.mod creates a new module boundary.
This is a very important rule:
A new
go.modmeans a new module boundary.
38. The easiest way to recognize everything
Look for:
go.mod
If you see:
one go.mod
you probably have:
one module + many packages
If you see:
four go.mod files
you have:
four modules
Example:
shop/
├── go.mod ← Module 1
├── payment/
│ └── payment.go
├── order/
│ └── order.go
└── customer/
└── customer.go
= 1 module
Versus:
shop/
├── go.mod ← Module 1
├── payment/
│ └── go.mod ← Module 2
├── order/
│ └── go.mod ← Module 3
└── customer/
└── go.mod ← Module 4
= 4 modules
39. One very useful rule
Ask:
Do these components need independent versions?
If the answer is:
No
use packages.
1 module
+
many packages
If the answer is:
Yes
consider multiple modules.
multiple go.mod files
40. When should I use a package?
Use packages when you simply want to organize code.
Example:
shop/
├── payment/
├── order/
├── customer/
├── database/
├── config/
└── logger/
They are different areas of your application.
You do not need:
payment v1.0
order v2.0
customer v1.5
So packages are enough.
41. When should I use a submodule?
A separate module may make sense if payment is essentially an independent library.
For example:
payment
is used by:
shop
mobile-backend
billing-system
subscription-system
And it needs its own releases:
v1.0.0
v1.1.0
v2.0.0
Then making payment its own module can make sense.
42. Example real-world architecture
Normal application:
ecommerce/
├── go.mod
├── cmd/
│ └── server/
│ └── main.go
│
├── customer/
├── order/
├── payment/
├── inventory/
├── notification/
└── database/
This is:
1 module
+
many packages
Usually this is what you should start with.
43. Multi-module architecture
For a large library repository:
platform/
├── go.work
│
├── auth/
│ └── go.mod
│
├── database/
│ └── go.mod
│
├── messaging/
│ └── go.mod
│
└── telemetry/
└── go.mod
Now each library can evolve independently.
During development:
go.work
connects them.
For releases:
auth v1.2.0
database v3.0.0
messaging v2.1.0
telemetry v1.4.0
44. Golden rule
For beginners:
Start with ONE module.
Then create many packages:
module
│
├── package
├── package
├── package
└── package
Only introduce more modules when you actually need:
independent versioning
or:
independent release lifecycle
Do not create separate modules simply because you want to organize source code.
Packages already solve that problem.
45. Final mental model
Remember this:
GO PROJECT
│
↓
MODULE
│
│ defined by
↓
go.mod
│
├────────────┬─────────────┐
↓ ↓ ↓
PACKAGE PACKAGE PACKAGE
payment customer order
│
↓
.go files
│
↓
Functions / Structs / Methods / Interfaces
If you create another:
go.mod
inside:
payment/
then:
payment
stops being merely a package inside the parent module and becomes the root of another module.
Repository
│
├── Root Module
│ └── go.mod
│
└── Payment Module
└── go.mod
If several local modules need to be developed together:
go.work
can group them:
go.work
│
┌─────────┼─────────┐
↓ ↓ ↓
Module Module Module
And for release/production dependencies, think:
go.mod
│
↓
versioned dependencies
│
├── payment v1.0.0
├── order v1.0.0
└── customer v1.0.0
46. Final comparison cheat sheet
| Concept | Simple meaning | Main file/example |
|---|---|---|
| Repository | Git/project container | .git/ |
| Module | Versioned collection of Go packages | go.mod |
| Package | Related Go code | package payment |
| Subpackage | Package inside nested directory | customer/validation |
| Submodule | Separate nested module | Another go.mod |
| Workspace | Develop multiple modules together | go.work |
| Dependency | External/separate module required by a module | require |
| Import | Use another package | import "..." |
| Exported function | Function usable from another package | Pay() |
| Unexported function | Function private to package | pay() |
The one sentence to remember is:
Use packages to organize code, modules to version and distribute code, and
go.workto conveniently develop multiple local modules together.