12. Modules & Packages

📋 Jump to Takeaways

🎁 Every language has a story about how it organizes code. Some use header files, others rely on fragile relative imports. Go sidesteps all of that with a module system that's explicit, flat, and refreshingly predictable. If you can name a directory, you can structure a project.

Initializing a Module with go mod init

A module is the top-level unit of code distribution in Go. You create one with a single command.

go mod init github.com/yourname/myproject

This generates a go.mod file that declares your module path and the Go version you're targeting. Every Go project starts here — no exceptions.

module github.com/yourname/myproject

go 1.22

The module path is how other packages import your code. For personal projects, use your repo URL. For local experiments, even go mod init myapp works.

Package Declaration

Every .go file begins with a package declaration. This tells the compiler which package the file belongs to.

package main

import "fmt"

func main() {
    fmt.Println("Hello from package main")
}
// Output: Hello from package main

The main package is special — it's the entry point for executables. Library code uses any other name.

Package vs Directory

In Go, a package is a directory. All .go files in the same directory must declare the same package name. The package name typically matches the directory name.

myproject/
├── go.mod
├── main.go          // package main
└── math/
    └── calc.go      // package math
// math/calc.go
package math

func Add(a, b int) int {
    return a + b
}

You import by directory path, not by file name. One directory = one package. No mixing allowed.

Exported vs Unexported Names

Go uses a beautifully simple rule: if a name starts with a capital letter, it's exported (public). Lowercase means unexported (private to the package).

package user

// Exported — accessible from other packages
func Validate(name string) bool {
    return len(name) > 0
}

// unexported — only visible within this package
func sanitize(name string) string {
    return strings.TrimSpace(name)
}

No keywords like public or private. Just capitalization. You'll internalize this fast.

Importing Packages

You import packages by their full module path. Go's tooling handles the rest.

package main

import (
    "fmt"
    "github.com/yourname/myproject/math"
)

func main() {
    result := math.Add(3, 7)
    fmt.Println(result)
}
// Output: 10

Grouped imports use parentheses. The compiler enforces that you use every import — unused imports are a compile error, not a warning.

Internal Packages

Go has a built-in mechanism for code you want to share between your own packages but hide from external consumers. Place it in a directory named internal.

myproject/
├── cmd/
│   └── server/
│       └── main.go
├── internal/
│   └── auth/
│       └── token.go    // only importable within myproject
└── go.mod

Anything under internal/ can only be imported by code rooted at the parent of internal. The compiler enforces this — no configuration needed.

go.mod and go.sum

Your go.mod file tracks your module's dependencies and their minimum versions. When you add a dependency, Go also generates go.sum, which contains cryptographic hashes to verify integrity.

// go.mod
module github.com/yourname/myproject

go 1.22

require github.com/gin-gonic/gin v1.9.1

Commit both files to version control. go.mod declares what you need; go.sum ensures reproducible builds.

Adding Dependencies with go get

To add an external package, use go get:

go get github.com/gin-gonic/gin@latest

This downloads the package, updates go.mod, and records checksums in go.sum. You can pin specific versions by replacing @latest with a tag like @v1.9.1.

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "hello"})
    })
    r.Run() // listens on :8080
}

The Standard Library Tour

Go ships with a rich standard library. You'll reach for these constantly:

import (
    "fmt"           // formatted I/O (Println, Sprintf, Errorf)
    "os"            // file system, environment variables, exit
    "io"            // Reader/Writer interfaces, Copy, ReadAll
    "net/http"      // HTTP client and server
    "encoding/json" // JSON marshal/unmarshal
    "strings"       // string manipulation (Contains, Split, Replace)
    "strconv"       // string ↔ number conversion (Atoi, Itoa)
)
package main

import (
    "fmt"
    "strconv"
    "strings"
)

func main() {
    n, _ := strconv.Atoi("42")
    upper := strings.ToUpper("hello")
    fmt.Println(n, upper)
}
// Output: 42 HELLO

The standard library covers most needs. Reaching for third-party packages is a conscious choice, not a necessity.

Multi-File Packages

A package can span multiple files. All files in the same directory share access to each other's unexported names.

utils/
├── strings.go    // package utils
└── numbers.go    // package utils
// utils/strings.go
package utils

func Capitalize(s string) string {
    return strings.ToUpper(s[:1]) + s[1:]
}

// utils/numbers.go
package utils

func Double(n int) int {
    return n * 2
}

Split packages by logical concern, not by arbitrary line counts. Both functions are importable as utils.Capitalize and utils.Double.

The init() Function

Each file can define an init() function that runs automatically before main(). It takes no arguments and returns nothing.

package main

import "fmt"

var config string

func init() {
    config = "loaded"
    fmt.Println("init ran first")
}

func main() {
    fmt.Println("main:", config)
}
// Output:
// init ran first
// main: loaded

Use init() sparingly — for registering drivers, validating configuration, or setting up package-level state. Multiple init() functions in the same package execute in source file order, but relying on that order is fragile.

Organizing a Project

The Go community has settled on conventions for structuring larger projects:

myproject/
├── cmd/
│   ├── api/
│   │   └── main.go        // builds the API binary
│   └── worker/
│       └── main.go        // builds the worker binary
├── internal/
│   ├── auth/              // private business logic
│   └── database/
├── pkg/
│   └── validator/         // reusable library code (public)
├── go.mod
└── go.sum
  • cmd/ — each subdirectory is an executable with its own main package
  • internal/ — private application code, not importable by external modules
  • pkg/ — library code you intentionally expose for other projects to import

For small projects, a flat structure is perfectly fine. Adopt these conventions when your codebase outgrows a single directory.

Key Takeaways

  • Every project starts with go mod init — this creates your go.mod
  • One directory = one package; the package name matches the directory
  • Capital letter = exported; lowercase = unexported — no keywords needed
  • go.sum ensures reproducible, verifiable builds — commit it
  • Use go get to add dependencies with explicit versioning
  • The standard library (fmt, os, io, net/http, encoding/json, strings, strconv) handles most tasks
  • Multiple files in a directory share the same package scope
  • init() runs before main() — use it for setup, not business logic
  • Structure larger projects with cmd/, internal/, and pkg/ conventions
  • internal/ enforces encapsulation at the compiler level

🎁 You now know how to organize code into packages and modules. But what happens when a function becomes a value — something you can pass around, return, and even let carry its own state? Next up: closures, where functions remember the world they were born in.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue

© 2026 ByteLearn.dev. Free courses for developers. · Privacy