Skip to content
ioob.dev
Go back

Go Part 11 — Packages and Modules

· 5 min read
Go Series (11/12)
  1. Go Basics Part 1 — Variables, Constants, and Types
  2. Go Basics Part 2 — Conditionals and Loops
  3. Go Basics Part 3 — Functions
  4. Go Basics Part 4 — Error Handling
  5. Go Basics Part 5 — Arrays, Slices, and Maps
  6. Go Basics Part 6 — Structs and Methods
  7. Go Part 7 — Interfaces
  8. Go Part 8 — Pointers
  9. Go Part 9 — Goroutines and Channels
  10. Go Part 10 — Concurrency Patterns
  11. Go Part 11 — Packages and Modules
  12. Go Part 12 — Generics and Practical Patterns
Table of contents

Table of contents

How to Organize Code

All the examples so far have been crammed into a single main() function in package main. That’s fine for practice, but in real projects, code grows to thousands of lines and structure becomes necessary. In Go, the unit of code organization is the package, and the unit of dependency management is the module.

Packages — The Basic Unit of Code

In Go, every .go file must belong to a package. Files in the same directory must use the same package name, and different directories become different packages.

myproject/
├── go.mod
├── main.go          // package main
├── calc/
│   ├── math.go      // package calc
│   └── math_test.go // package calc
└── greeting/
    └── hello.go     // package greeting

Let’s create calc/math.go.

// calc/math.go
package calc

// Add adds two integers.
func Add(a, b int) int {
    return a + b
}

// subtract is an internal-only function.
func subtract(a, b int) int {
    return a - b
}

Using this package from main.go:

// main.go
package main

import (
    "fmt"
    "myproject/calc"
)

func main() {
    fmt.Println(calc.Add(3, 5))      // 8
    // fmt.Println(calc.subtract(3, 5)) // Compile error — lowercase, not visible
}

calc.Add is accessible but calc.subtract is not. The reason is in the next section.

Uppercase and Lowercase — Go’s Visibility Rules

Go’s access control is remarkably simple. If a name starts with an uppercase letter, it’s accessible from outside the package (exported); if lowercase, it’s package-internal only (unexported). That’s it.

package user

// User — starts with uppercase, accessible from other packages
type User struct {
    Name  string // Uppercase — externally accessible
    Email string // Uppercase — externally accessible
    age   int    // Lowercase — only accessible within this package
}

// NewUser — starts with uppercase, callable from outside
func NewUser(name, email string, age int) *User {
    return &User{Name: name, Email: email, age: age}
}

// validate — starts with lowercase, internal use only
func validate(email string) bool {
    return len(email) > 0
}

There are no public, private, or protected keywords. One rule — uppercase/lowercase — handles everything, making visibility immediately apparent when reading code. It might feel unfamiliar at first, but once you adapt, it’s hard to imagine a more concise access control system.

go mod — Module Management

A Go module is a collection of packages. The go.mod file marks the module root and manages the module name, Go version, and external dependencies.

When starting a new project:

mkdir myproject && cd myproject
go mod init github.com/username/myproject

This command creates the go.mod file.

module github.com/username/myproject

go 1.22

When you use external packages, dependencies are automatically added.

go get golang.org/x/text

Dependencies are recorded in go.mod, and checksums are stored in go.sum. You won’t need to edit go.sum directly, but it must be included in version control. It ensures that others building the same code get identical dependencies.

Here’s a summary of commonly used module commands.

go mod init MODULE_NAME   # Initialize new module
go mod tidy               # Clean up unused deps, add missing ones
go get PACKAGE@VERSION    # Add a specific version of a package
go list -m all            # List all dependencies

Running go mod tidy regularly is a good habit. After adding or removing imports in your code, running it keeps go.mod and go.sum clean.

init() — Package Initialization

Each package can define an init() function. This function runs automatically when the program starts, before main().

package config

import "fmt"

var DefaultPort int

func init() {
    DefaultPort = 8080
    fmt.Println("config package initialized")
}
package main

import (
    "fmt"
    "myproject/config"
)

func main() {
    // "config package initialized" prints first
    fmt.Println("server port:", config.DefaultPort) // 8080
}

The execution order is:

  1. init() of imported packages (in dependency order)
  2. init() of the main package
  3. main()

Mapping this sequence to a diagram shows the flow of program startup.

sequenceDiagram
    participant Runtime as Go Runtime
    participant Dep as Dependency packages
    participant Main as main package
    participant User as main()

    Runtime->>Dep: Initialize vars in dependency order
    Runtime->>Dep: Call init() (if any)
    Dep-->>Runtime: Ready
    Runtime->>Main: Initialize main package vars
    Runtime->>Main: Call main.init() (if any)
    Runtime->>User: Execute main()
    User-->>Runtime: Program exits

init() is used for package-level variable initialization, loading config files, registering drivers, and similar purposes. The classic example is blank-importing a database driver package like import _ "github.com/lib/pq". You don’t use the package directly, but its init() side effect (driver registration) is needed.

However, overuse of init() should be avoided. The implicit initialization order makes debugging harder, and unwanted side effects can occur during testing. Unless truly necessary, creating an explicit initialization function is often the better choice.

Project Structure Conventions

Go doesn’t have an officially enforced project structure, but there are widely adopted community conventions.

myproject/
├── cmd/
│   └── server/
│       └── main.go       # Executable entry point
├── internal/
│   ├── handler/
│   │   └── user.go       # HTTP handlers
│   └── service/
│       └── user.go       # Business logic
├── pkg/
│   └── validator/
│       └── email.go      # Externally reusable utilities
├── go.mod
└── go.sum

Each directory’s role:

For small projects, there’s no need to force this structure. If a single package suffices, not splitting into directories is actually cleaner. Structure should emerge naturally as the code grows — creating empty directories upfront is over-engineering.

internal Packages — Compiler-Enforced Boundaries

The internal directory has special meaning in Go. Packages inside this directory can only be imported from within the parent directory tree of internal.

myproject/
├── cmd/
│   └── server/
│       └── main.go       # Can use internal/handler
├── internal/
│   └── handler/
│       └── user.go       # Only accessible within this project
└── go.mod
// cmd/server/main.go — OK
import "myproject/internal/handler"

// From another module — compile error
// import "myproject/internal/handler"
// use of internal package myproject/internal/handler not allowed

While the uppercase/lowercase rule controls visibility within packages, internal controls visibility at module boundaries. Placing implementation details you don’t want to expose in internal lets the compiler prevent accidental external usage.

This is especially useful when building libraries. Put public APIs at the root or in pkg/, and hide internal implementations in internal/ to keep the API surface clean.

Import Organization

Import blocks in Go files are typically organized into three groups. Separating them with blank lines lets the goimports tool auto-sort them.

import (
    // Standard library
    "fmt"
    "net/http"

    // External packages
    "github.com/gorilla/mux"
    "go.uber.org/zap"

    // Project internal packages
    "myproject/internal/handler"
    "myproject/internal/service"
)

In Go, importing something and not using it causes a compile error. This strict rule might feel inconvenient at first, but it prevents unused code from accumulating, which is increasingly appreciated as projects grow.

With Go plugins installed in your editor (VS Code, GoLand), imports are auto-organized on save, so you rarely need to manage them manually.


Packages and modules are the skeleton of a Go project. Visibility is controlled with uppercase/lowercase, dependencies are managed with go.mod, and boundaries are guarded with internal. These rules may seem simple, but they provide tremendous power in maintaining consistent code structure as projects scale. Rather than trying to design the perfect structure from the start, letting packages naturally separate as code grows is the Go way.

The next part covers generics introduced in Go 1.18, along with table-driven tests, build tags, and go generate. As the final part of the series, it will be a time to synthesize everything we’ve learned.

-> Part 12: Generics and Practical Patterns


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Go Part 10 — Concurrency Patterns
Next Post
Go Part 12 — Generics and Practical Patterns