Go Tutorials: Modules, Packages, Subpackages, Submodules, and Workspaces

This tutorial gives you one complete mental model for:

  • Module
  • Package
  • Subpackage
  • Submodule
  • go.mod
  • go.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:

ModulePackage
Collection of packagesCollection of .go files
Has go.modNormally does not have go.mod
Dependency/version boundaryCode organization boundary
Can contain many packagesLives inside a module
Example: example.com/shopExample: 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

FeaturePackageSubpackageSubmodule
Separate directoryUsuallyYesYes
Own go.modNoNoYes
Independent versionNoNoYes
Independent dependenciesNoNoYes
ImportableYesYesYes
Part of parent moduleYesYesNo
Common in normal appsVery commonCommonLess common

32. go.mod vs go.work

go.modgo.work
Defines a moduleDefines a workspace
Required for modulesOptional
Declares dependenciesGroups local modules
Used for builds/releasesMostly useful for local multi-module development
Usually committedOften developer/workspace-specific
Contains requireContains 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.mod means 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

ConceptSimple meaningMain file/example
RepositoryGit/project container.git/
ModuleVersioned collection of Go packagesgo.mod
PackageRelated Go codepackage payment
SubpackagePackage inside nested directorycustomer/validation
SubmoduleSeparate nested moduleAnother go.mod
WorkspaceDevelop multiple modules togethergo.work
DependencyExternal/separate module required by a modulerequire
ImportUse another packageimport "..."
Exported functionFunction usable from another packagePay()
Unexported functionFunction private to packagepay()

The one sentence to remember is:

Use packages to organize code, modules to version and distribute code, and go.work to conveniently develop multiple local modules together.

Related Posts

Go Tutorials: Go Methods

Go methods become much easier once you understand one idea: A method is simply a function attached to a type. And the important rule is: A receiver…

Read More

Go Tutorials: Numeric Types Beginner Tutorial

Go has four main families of numeric types: The simplest way to remember them is: Type family Stores Example int Whole numbers, positive or negative -10, 0,…

Read More

Go Tutorials: Go Testing, Benchmarking, and Profiling

These three topics become much easier once you separate the questions they answer: Topic Main question Main Go tool Result Testing Does my code work correctly? go…

Read More

Go Tutorials: Generics

1. What are Generics? Generics let you write one piece of code that works with multiple types while keeping Go’s compile-time type safety. Suppose you want a…

Read More

Go Concurrency Management Made Simple

This tutorial covers: The goal is: One concept → one purpose → one complete runnable example. Every example is independent. Save any example as: and run: 0….

Read More

Go Tutorials: Concurrency Made Simple

This tutorial covers: The goal is simple: One concept → one mental model → one complete example. Every example is independent. You can copy it into main.go…

Read More