Skip to content
AgileLogics
6 min read

Modules & Packages

Prerequisites: Level 1 — Beginner

In Level 1 every program lived in a single main.go. Real projects are split into packages, and packages are grouped into a module.

  • A package is a directory of .go files that all start with the same package clause. Everything in that directory can see everything else in it.
  • A module is a tree of packages versioned together. Its root holds a go.mod file that names the module path and the Go version.
  • A name is exported (visible to other packages) when it starts with a capital letter: greet.Hello is exported, greet.helper is not. There are no public or private keywords.
  • A package inside a directory named internal/ can only be imported by code in the directory tree rooted at the parent of internal/. Use it for code you don't want other modules to depend on.

Syntax

go mod init example.com/hello              # create go.mod
go get github.com/google/uuid@latest       # add or upgrade a dependency
go mod tidy                                # add missing / remove unused requirements

An import path is the module path followed by the package's directory:

hello/
├── go.mod                  module example.com/hello
├── main.go                 package main
├── greet/
│   └── greet.go            import "example.com/hello/greet"
└── internal/
    └── format/
        └── format.go       import "example.com/hello/internal/format"

Example

go.mod:

module example.com/hello

go 1.23

internal/format/format.go:

package format

// Exclaim adds an exclamation mark to s.
func Exclaim(s string) string {
    return s + "!"
}

greet/greet.go:

package greet

import "example.com/hello/internal/format"

// defaultName is unexported: only code in package greet can use it.
const defaultName = "Gopher"

// Hello is exported because its name starts with a capital letter.
func Hello(name string) string {
    if name == "" {
        name = defaultName
    }
    return format.Exclaim("Hello, " + name)
}

main.go:

package main

import (
    "fmt"

    "example.com/hello/greet"
)

func main() {
    fmt.Println(greet.Hello("Ada"))
    fmt.Println(greet.Hello(""))
}

Run it from the module root with go run . (the dot means "the package in this directory").

Expected output

Hello, Ada!
Hello, Gopher!

When you go get a third-party package, Go records the requirement in go.mod and writes checksums to go.sum. Commit both files.

Common mistakes

  • Trying to use a lowercase name from another package, such as greet.defaultName. It is not exported, so the compiler reports undefined: greet.defaultName.
  • Using the directory name alone in an import (import "greet"). Imports of your own packages always start with the module path from go.mod.
  • Creating an import cycle (package a imports b and b imports a). Go refuses to compile cycles; move the shared code into a third package.
  • Putting more than one package name in the same directory. Every file in a directory must use the same package clause (test files are the one exception, covered later).

Practice

In the hello module, add a package mathx with an exported function Double(n int) int, then call it from main.go to print 42.

Show solution

mathx/mathx.go:

package mathx

// Double returns n multiplied by two.
func Double(n int) int {
    return n * 2
}

main.go:

package main

import (
    "fmt"

    "example.com/hello/mathx"
)

func main() {
    fmt.Println(mathx.Double(21))
}