Skip to content

Go Packages and Modules — go.mod, Import Paths and Package Visibility Explained

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Go Packages and Modules. We cover key concepts, practical examples, and best practices to help you master this topic.

Go packages organize code into namespaces with exported (capitalized identifiers) and unexported (lowercase) visibility, while modules defined in go.md manage dependencies, versioning, and import paths.

What You'll Learn

  • Creating and organizing packages
  • Exporting and unexporting identifiers
  • Module initialization with go.mod
  • Managing dependencies

Why It Matters

Packages and modules define your project structure. Docker has hundreds of packages. Kubernetes uses complex module hierarchies. DodaZIP organizes archivers, compressors, and utilities into separate packages.

Real-World Use

Every Go project uses packages. Standard library packages (fmt, http, io) define the patterns. Third-party dependencies via modules. Proper package design is essential for maintainable Go code.

flowchart LR
    A["Packages"] --> B["Exports"]
    B --> C["go.mod"]
    C --> D["Imports"]
    D --> E["Dependencies"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Package Declaration

Every Go file starts with a package declaration:

// math/operations.go
package math

func Add(a, b int) int { return a + b }
func Subtract(a, b int) int { return a - b }
// cmd/main.go
package main

import "fmt"
import "example.com/myapp/math"

func main() {
    fmt.Println(math.Add(5, 3))  // 8
}

Exported vs Unexported

Identifiers starting with uppercase are exported (public). Lowercase are unexported (private to the package):

package user

// Exported — accessible outside the package
type User struct {
    Name string  // Exported field
    age  int     // Unexported field
}

// Exported function
func New(name string, age int) *User {
    return &User{Name: name, age: age}
}

// Unexported function — internal helper
func validate(name string) error {
    if name == "" {
        return fmt.Errorf("name cannot be empty")
    }
    return nil
}

// Exported — but age is unexported, so external code can't set it
func (u *User) Age() int {
    return u.age
}

Package Initialization

Packages can have init functions that run automatically:

package db

import (
    "database/sql"
    "log"
)

var DB *sql.DB

func init() {
    var err error
    DB, err = sql.Open("postgres", "postgres://localhost/mydb")
    if err != nil {
        log.Fatal("failed to connect:", err)
    }
}

init order

// 1. Imported package init() runs first
// 2. Package-level variable initialization
// 3. Package's own init() runs

var Version = "1.0.0"

func init() {
    fmt.Println("Package initialized, version:", Version)
}

Module Initialization

go mod init example.com/myapp

Creates go.mod:

module example.com/myapp

go 1.22

go.mod structure

module github.com/user/myapp

go 1.22

require (
    github.com/gorilla/mux v1.8.1
    github.com/lib/pq v1.10.9
)

// Indirect dependencies
require github.com/google/uuid v1.6.0 // indirect

Importing Packages

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

    // Third-party
    "github.com/gorilla/mux"
    "github.com/lib/pq"

    // Local packages (within the module)
    "example.com/myapp/internal/auth"
    "example.com/myapp/pkg/handler"
)

Import Aliases

import (
    "crypto/rand"
    "math/rand"
    mrand "math/rand"  // Alias
)

func main() {
    // Use alias for disambiguation
    secure := rand.Read // crypto/rand
    _ = mrand.Intn(10)  // math/rand via alias
}

Blank Import

import _ "image/png"  // Runs init() without using the package

Internal Package

The internal package restricts imports to the parent module:

myapp/
├── internal/
   └── auth/
       └── auth.go      // Only importable by myapp/
├── pkg/
   └── handler/
       └── handler.go   // Can import internal/auth
└── cmd/
    └── server/
        └── main.go      // Can import internal/auth
// internal/auth/auth.go
package auth

// Only importable within myapp/
func ValidateToken(token string) bool {
    return token == "valid"
}

Dependency Management

# Add a dependency
go get github.com/gorilla/mux@latest

# Update all dependencies
go get -u ./...

# Remove unused dependencies
go mod tidy

# Vendor dependencies
go mod vendor

# Verify dependencies
go mod verify

Using Dependencies

package main

import (
    "fmt"
    "net/http"

    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/", homeHandler)
    http.ListenAndServe(":8080", r)
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, modules!")
}

Organizing Packages

Recommended project layout:

myproject/
├── cmd/
│   ├── server/
│   │   └── main.go        # Entry point
│   └── cli/
│       └── main.go        # Another entry point
├── internal/
│   ├── auth/
│   │   └── auth.go        # Internal package
│   └── db/
│       └── db.go           # Internal package
├── pkg/
│   ├── api/
│   │   └── api.go         # Public API package
│   └── config/
│       └── config.go       # Public config package
├── go.mod
├── go.sum
└── README.md

Common Mistakes

1. Circular Imports

// package a imports package b
// package b imports package a  // Error!

// Fix: extract shared types into a third package

2. Package Name Mismatch

// File: math/operations.go
package maths  // Must be math (matches directory name)

// Go convention: package name = directory base name

3. Not Committing go.sum

// go.sum is needed for reproducible builds
// Always commit it to version control

4. Using relative imports

// Bad — relative import
import "../auth"

// Good — module-based import
import "example.com/myapp/internal/auth"

5. Not running go mod tidy

// go.mod has outdated/unused dependencies
// Run: go mod tidy

Practice Questions

1. What determines if an identifier is exported?

The first letter. Uppercase = exported (public). Lowercase = unexported (package-private).

2. What is the init function?

A function without parameters that runs automatically when a package is imported. Used for initialization, connection setup, registration. Runs once per package.

3. What is an internal package?

A package in a directory named internal that can only be imported by code in the parent module. Go enforces this restriction at compile time.

4. How do you add a dependency?

Run go get <package>@<version>. This updates go.mod and go.sum. Then import the package in your code.

Challenge: Create a multi-package project with a public API package, an internal auth package, and a main cmd that uses both.

Solution

Project structure:

myapp/
├── cmd/server/main.go
├── internal/auth/auth.go
├── pkg/api/handler.go
├── go.mod
└── go.sum
// go.mod
module example.com/myapp
go 1.22

// cmd/server/main.go
package main

import (
    "fmt"
    "net/http"
    "example.com/myapp/internal/auth"
    "example.com/myapp/pkg/api"
)

func main() {
    handler := api.NewHandler(auth.NewAuthenticator())
    http.HandleFunc("/", handler.HandleHome)
    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
}

// internal/auth/auth.go
package auth

import "fmt"

type Authenticator struct {
    validTokens map[string]bool
}

func NewAuthenticator() *Authenticator {
    return &Authenticator{
        validTokens: map[string]bool{"secret123": true},
    }
}

func (a *Authenticator) Validate(token string) bool {
    return a.validTokens[token]
}

func (a *Authenticator) Login(username, password string) (string, error) {
    if username == "admin" && password == "pass" {
        token := "secret123"
        a.validTokens[token] = true
        return token, nil
    }
    return "", fmt.Errorf("invalid credentials")
}

// pkg/api/handler.go
package api

import (
    "fmt"
    "net/http"
    "example.com/myapp/internal/auth"
)

type Handler struct {
    auth *auth.Authenticator
}

func NewHandler(auth *auth.Authenticator) *Handler {
    return &Handler{auth: auth}
}

func (h *Handler) HandleHome(w http.ResponseWriter, r *http.Request) {
    token := r.Header.Get("Authorization")
    if !h.auth.Validate(token) {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    fmt.Fprintf(w, "Welcome, authenticated user!")
}

FAQ

{{< faq question="What's the difference between a package and a module?" >}} A package is a directory of Go files with the same package declaration. A module is a collection of packages with a go.mod file defining the module path and dependencies. {{< /faq >}}

{{< faq question="Should I use relative or absolute import paths?" >}} Always use absolute paths based on the module path: example.com/myapp/pkg/config. Never use relative imports like ../pkg/config. {{< /faq >}}

{{< faq question="What is go.sum?" >}} A file containing checksums of all dependency modules. It ensures reproducible builds by verifying dependency integrity. Commit it to version control. {{< /faq >}}

{{< faq question="Can I have multiple packages in one directory?" >}} No. All .go files in a directory must belong to the same package. Different packages go in different directories. {{< /faq >}}

{{< faq question="What is the blank identifier import for?" >}} import _ "image/png" runs the package's init() function without importing any identifiers. Used for driver registration (database drivers, image formats). {{< /faq >}}

Try It Yourself

mkdir -p myapp/{cmd/server,internal/auth,pkg/greeter}
cd myapp
go mod init example.com/myapp
// pkg/greeter/greeter.go
package greeter

import "fmt"

func Hello(name string) string {
    return fmt.Sprintf("Hello, %s!", name)
}

func goodbye(name string) string {
    return fmt.Sprintf("Goodbye, %s!", name)
}

// internal/auth/auth.go
package auth

import "fmt"

func CheckAccess(role string) bool {
    return role == "admin"
}

func logAttempt(role string) {
    fmt.Println("Access attempt with role:", role)
}

// cmd/server/main.go
package main

import (
    "fmt"
    "example.com/myapp/internal/auth"
    "example.com/myapp/pkg/greeter"
)

func main() {
    msg := greeter.Hello("Alice")
    fmt.Println(msg)

    if auth.CheckAccess("admin") {
        fmt.Println("Access granted")
    } else {
        fmt.Println("Access denied")
    }
}
go run ./cmd/server/
# Hello, Alice!
# Access granted

What's Next

Now that you understand packages and modules, learn about testing and benchmarking in Go.

Topic Description Link
Go Testing Table tests, benchmarks, coverage {{< ref "16-testing" >}}
Go Error Handling Error interface, wrapping, sentinel {{< ref "13-error-handling" >}}
Rust Cargo Compare Rust's package manager Rust

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go