Go (Golang) Tutorial 0/45 lessons ~6 min read Lesson 19

    Packages

    Packages are Go's unit of compilation and namespacing.

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Packages are Go's unit of compilation and namespacing. Every Go file belongs to a package declared at the top. The internal/ directory enforces package privacy — only code within the parent tree can import internal packages, a powerful enterprise pattern.

    Exported identifiers (capitalized) form the public API; unexported ones are implementation details. Package design affects testability, circular import errors, and godoc documentation quality.

    This lesson covers standard layout (cmd/, internal/, pkg/), init functions, and avoiding common import cycle pitfalls that block junior developers on day one of a new job.

    The story

    The Docker Engine codebase organizes code into packages: client for API calls, daemon for runtime logic, opts for flag parsing. Each directory is one package; exported names (capitalized) form the public API while lowercase helpers stay private — enforcing encapsulation without access modifiers.

    Package boundaries map to team ownership in monorepos at Google: clear import paths, minimal exported surface, and internal/ directories prevent other modules from importing implementation details.

    Understanding the topic

    Key concepts

    • package foo declares all files in directory share namespace.
    • Import path is module path + /package/dir — not necessarily disk path alone.
    • internal/ packages importable only from ancestor directories.
    • init() runs automatically on import — order by dependency.
    • go doc and pkg.go.dev document exported API.
    • Circular imports are compile errors — refactor to shared package.

    Step-by-step explanation

    1. One directory = one package (except cmd subdirs each main).
    2. Capitalized names exported across package boundary.
    3. Import groups: stdlib, blank, third-party, local — gofmt orders.
    4. Rename import: import f "fmt" or import . "fmt" (dot import rare).
    5. Blank import _ "pkg" runs init for side effects (driver registration).
    6. Build tags //go:build exclude files from compilation.

    Practical code example

    Multi-file package pattern with exported and unexported symbols:

    go
    // file: mathutil/add.go
    package mathutil
    // Add sums two integers — exported API.
    func Add(a, b int) int {
    return a + b
    }
    // file: mathutil/internal_helper.go (same package)
    package mathutil
    func validate(n int) bool {
    return n >= 0
    }
    func SafeAdd(a, b int) int {
    if !validate(a) || !validate(b) {
    return 0
    }
    return Add(a, b)
    }
    // file: main.go
    package main
    import (
    "fmt"
    "github.com/techlearningpro/demo/mathutil"
    )
    func main() {
    fmt.Println(mathutil.SafeAdd(10, 20))
    }

    Line-by-line code explanation

    • package client declares the package name — usually matches the directory name.
    • import "github.com/org/project/client" imports using the module path from go.mod.
    • ExportedFunc() starts with uppercase — visible to other packages.
    • helper() lowercase — accessible only within the same package.
    • internal/ directory blocks imports from outside the parent tree — enforced by the compiler.
    • init() runs automatically on import — use sparingly for registration, not heavy setup.
    • go doc net/http renders exported documentation from comments above declarations.
    • import alias "long/path/pkg" shortens verbose import paths in large codebases.

    Key takeaway: validate is unexported — only mathutil can call it. Split files freely within one package. internal/ dir adds stronger encapsulation at module level.

    Real-world use

    Where you'll use this in production

    • cmd/api/main.go imports internal/service and internal/repo.
    • Database driver blank import: _ "github.com/jackc/pgx/v5/stdlib".
    • Shared pkg/httputil for cross-service HTTP helpers.
    • Generated protobuf packages in gen/ or api/.

    Best practices

    • Use internal/ for code that must not be imported externally.
    • Keep exported API minimal — hide helpers as unexported.
    • Avoid init() logic beyond registration — hard to test.
    • Structure: cmd/, internal/, optional pkg/ for public libs.
    • One package per responsibility — not utils with 50 functions.
    • Run go doc ./... to verify exported documentation.

    Common mistakes

    • Circular imports between service and handler — extract interfaces.
    • Giant util package — split by domain.
    • Exporting everything — leaky abstraction.
    • Heavy init() causing slow imports and hidden side effects.
    • Mismatch between package name and directory name — confusing imports.

    Advanced interview questions

    Q1BeginnerExported vs unexported?
    Capitalized exported (public); lowercase unexported (package-private).
    Q2BeginnerPurpose of internal/ directory?
    Compiler-enforced visibility — importable only from parent module tree.
    Q3IntermediateFix circular import?
    Extract shared interfaces/types to third package both import.
    Q4IntermediateBlank import _ purpose?
    Run package init without direct reference — driver registration pattern.
    Q5AdvancedDesign package layout for payment microservice.
    cmd/payment/main; internal/domain, internal/handler, internal/postgres; api/ for protobuf; no business logic in main.

    Summary

    Packages organize code; exported names are the public API. internal/ enforces encapsulation at module level. Avoid circular imports — extract shared types/interfaces. Standard layout: cmd/, internal/, pkg/ for clarity. Next lesson: Go modules — dependency management.

    Ready to mark this lesson complete?Track your journey across the entire course.