JavaRush /Courses /Go SELF /Go Modules from scratch: go mod init, go.mod, and go.s...

Go Modules from scratch: go mod init, go.mod, and go.sum

Go SELF
Level 35, Lesson 0
Available

1. Basic concepts and starting a module

If you’ve just started out, the word “dependencies” can sound like something from adult life: mortgages, loans, and that library that suddenly requires another library, which in turn requires a third one. In reality, dependencies are simply other people’s code that you connect to your project. And Go’s module system was designed precisely to make this process manageable: which code, which version, where it came from—and to turn “it works on my machine” into “it works for everyone.”

Historically, Go had a mode where projects lived inside a special folder GOPATH, and dependencies were downloaded there as well. It worked… while the world was small and dependencies were modest. Then it turned out that without explicit version pinning, a project might build one way today and a different way a month from now (and it’s not mysticism—it’s just life).

That’s why Go introduced modules: a “delivery unit” of code with versioning and clear boundaries. Module support was added in Go 1.11 as an alternative to GOPATH, with the idea of built-in version management and package distribution.

One more thing that’s useful to keep in mind: the go command is the central piece of the ecosystem, and historically Go has repeatedly “made life easier” by bringing disparate tools under a single, clear interface. Module management is part of that approach: less ritual, more reproducibility.

What a module is and where its boundary is

Let’s agree on terminology so we don’t get confused later. A package is a folder with *.go files that contain package .... And a module is a set of packages described by a single go.mod file. And this is important: a module defines the boundary within which Go understands “what is your project” and how to resolve your imports.

The most practical analogy: a module is like a “project folder with a passport.” The passport is go.mod. Where the passport lives is the “front door”: everything below it in the folder tree belongs to the module (unless another go.mod is found further down).

Schematically, it looks like this:

todoapp/              <-- module root (go.mod is here)
  go.mod
  main.go
  todo/
    task.go           <-- todo package, also part of the same module

And now the key connection: the module path from go.mod becomes the prefix of the import paths for your own packages. In other words, the module answers the question: “What will your project be called in import?”

Creating a module with go mod init

When you write a small program in a single file, it may feel like you don’t need any modules. But as soon as you build a real project (at least with two packages), having go.mod makes the structure predictable.

The command that starts module life:

go mod init <module-path>

Here <module-path> is the string that will become the “name” of your module. In real projects this is usually the repository address, for example github.com/username/todoapp. In tutorials, example.com/... is often used to avoid tying the example to a specific hosting provider.

Let’s imagine our tutorial project is called todoapp, and we want the module path:

example.com/todoapp

Then after go mod init example.com/todoapp, you get a go.mod file. A minimal skeleton looks like this:


module example.com/todoapp

go 1.22

Note: this is not Go code, but a special format for the go tool. It isn’t “executed”; it’s “read” by go build, go test, and so on.

3. The go.mod file: how to read it and not be afraid

The go.mod file often scares beginners because it looks like a config file that is “dangerous to touch.” The good news: the basic part of go.mod is easy to understand if you don’t try to memorize everything at once. At the beginning, you just need to be able to read it and understand the meaning of the main lines.

The module directive: the project name for import

In go.mod, the line module ... sets the module path—the root path that is then used in imports of your packages.

If you have this structure:

todoapp/
  go.mod     (module example.com/todoapp)
  todo/
    task.go  (package todo)

then the import path of the todo package will be:

example.com/todoapp/todo

And here it’s important not to confuse two concepts:

Concept Example Where it’s used What it means
package name
todo
package todo
the name inside the code
import path
example.com/todoapp/todo
import "..."
the address where Go finds the package

The go directive: language version or toolchain behavior version

A line like go 1.22 is not “which Go you should install on your computer” (although it’s usually helpful to have a Go version that’s not lower). It’s more like “which version of tool and language behavior this module is built for.”

A practical beginner takeaway: this line helps Go tools behave predictably during builds, tests, and dependency resolution.

Why require might be missing from go.mod

At the very beginning, go.mod may consist only of module and go. And that’s not an error. The require directives appear when you start importing external packages (not from the standard library). For now, just lock in the idea: “an external dependency must be recorded in go.mod.”

We deliberately don’t dive into how to choose versions and which commands do that. Our goal is to start confidently with modules and understand their basic structure.

4. Example: todoapp with two packages

Now we’ll assemble a small piece of a “live” project so you can feel the usefulness of module path in practice. We’ll make two packages: main and todo. The todo package will contain a simple task title normalization function (remove extra spaces). Yes, it’s a micro-feature. But it’s exactly from these micro-features that an application grows, not a “2,000-line main.go sheet.”

Folder structure

Let’s imagine this structure:

todoapp/
  go.mod
  main.go
  todo/
    normalize.go

Code for the todo package

package todo

import (
	"strings"
)

// NormalizeTitle normalizes the task title into a neat form.
func NormalizeTitle(s string) string {
	return strings.TrimSpace(s)
}

Everything here is familiar: a package, a standard library import, a function.

main.go code importing your own package

package main

import (
	"fmt"

	"example.com/todoapp/todo"
)

func main() {
	title := todo.NormalizeTitle("   buy milk   ")
	fmt.Println(title) // buy milk
}

The main point: importing your own package goes through module path + subfolder.

If go.mod didn’t exist (or was “in the wrong place”), Go wouldn’t be able to interpret what example.com/todoapp/todo means in the context of your local disk. But with go.mod, everything becomes logical: “this is my module, here is its name, so this is how my packages are imported.”

To reinforce the logic, it’s helpful to imagine this diagram:

flowchart TD
    A[Go file: import '...'] --> B{Is it stdlib?}
    B -- yes --> C[Look in GOROOT: fmt, strings, ...]
    B -- no --> D{Does it start with the module path?}
    D -- yes --> E[Look for the package inside the current module]
    D -- no --> F[Look in external modules via dependencies]

That’s why module path is not a “formality,” but the key to helping Go understand where “yours” ends and “others’” begins.

5. go.sum and reproducibility

Why go.sum is needed

Now about the file that often triggers the reaction “who did this and why is it committed?” We’re talking about go.sum. If go.mod is a declaration of “which modules and which versions are needed,” then go.sum is the record of checks: “did we download the exact code we expect?”

It’s important to start with the right expectation: go.sum is usually not created by hand. It appears when Go tools first fetch external dependencies, and then it’s maintained automatically. And yes, most of the time it really is committed to the repository, because it’s part of a reproducible build: the same project on two machines should get the same verified set of sources.

In Go 1.16, for example, it was emphasized that commands in module mode should not “silently” change go.mod and go.sum on their own: if there’s a dependency problem, the tool reports an error and suggests what to do, instead of making unexpected auto-fixes. This is about predictability: fewer surprises, more control.

Also starting with Go 1.16, modules became the default mode in the toolchain, meaning the ecosystem finally settled around the module model.

What go.sum looks like

A fragment may look like this (schematically):

golang.org/x/net v0.20.0 h1:...
golang.org/x/text v0.14.0 h1:...

Don’t try to read this like a novel. These are module “checksums” (verification fingerprints) needed by the tools.

A practical rule: if you see changes in go.sum, it almost always means one of two things: either a dependency was added or updated, or the tools rebuilt/clarified the dependency graph. In any case, it’s not a file you edit in a text editor “for aesthetics.”

A short mental model for modules

When the module system clicks in your head, it stops being annoying. So let’s lock in a simple model without extra philosophy.

Your Go project is a folder that contains go.mod. This file defines the module name (module path) and rules for the tools. Inside the project, you create packages as subfolders and import them via module path + the subfolder path.

If you import something that is neither the standard library nor your own package, it’s an external module and it must be recorded as a dependency. For reproducibility and integrity checking, the tool maintains go.sum.

If you catch yourself thinking “I don’t understand where Go is getting this package from,” the answer almost always starts with: “Where is your go.mod, and what does it say in module?”

6. Common mistakes

Mistake #1: go.mod is not in the root, but in a random folder.
This often happens when someone created a project, then made another folder, then another one, and go.mod ended up on the second floor of that labyrinth. As a result, the module boundary becomes unexpected: some packages are “visible,” others are not, and imports start to look like magic. The fix is simple: consciously choose the project root and keep go.mod exactly there.

Mistake #2: mixing up package name and import path.
Beginners sometimes write something like "todo" in import and wonder why it doesn’t work. The package name (package todo) is the internal name in the code. But the import path is the address where Go finds it. For your own packages, it usually starts with the module path from go.mod.

Mistake #3: wanting to “rename the module” without understanding the consequences.
The line module ... is not just a signature. If you change the module path, you will most likely need to update imports of your own packages throughout the project. So changes to module are treated cautiously: it’s possible, but it’s not cosmetic.

Mistake #4: trying to edit go.sum manually.
go.sum looks like an incomprehensible set of lines, and your hands start itching to “clean up the junk.” But the point of the file is machine-level integrity verification. If you edit it manually, you usually don’t “improve the project”—you create a future mysterious build error. The right approach: treat go.sum as a tool artifact and trust the go command.

Mistake #5: expecting modules to be only about external libraries.
In fact, you need a module already to make your own imports stable and understandable. Even if you have zero external dependencies, go.mod is still useful as a “project passport” and a reference point for import paths.

1
Task
Go SELF, level 35, lesson 0
Locked
Hello, module
Hello, module
1
Task
Go SELF, level 35, lesson 0
Locked
Product Title
Product Title
1
Task
Go SELF, level 35, lesson 0
Locked
Two small consoles
Two small consoles
1
Task
Go SELF, level 35, lesson 0
Locked
UUID Generator
UUID Generator
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION