Free Ebook cover Go (Golang) Fundamentals: Simple, Fast Programs for Beginners

Go (Golang) Fundamentals: Simple, Fast Programs for Beginners

New course

12 pages

Packages and Imports in Go: Organizing Code with Clear Boundaries

Capítulo 3

Estimated reading time: 6 minutes

+ Exercise

1) Package declaration rules and folder layout

In Go, every source file belongs to exactly one package. A package is the unit of compilation and reuse: files in the same folder (directory) typically share the same package name and can access each other’s unexported identifiers.

Package declaration rules

  • The first non-comment line in a Go file must be a package declaration (for example, package main or package mathutil).
  • All .go files in the same folder should declare the same package name (test files may use package x_test to test from the outside).
  • The package name is usually the last element of the import path, but it does not have to match (it’s strongly recommended to keep them aligned for clarity).

Folder layout and import paths

Go code is organized by folders inside a module. The folder path (relative to the module root) becomes the import path suffix. Example layout:

myapp/                 (module root; contains go.mod) myapp.go              (package main) mathutil/              (folder)   mathutil.go          (package mathutil)   helpers.go           (package mathutil)

With a module path like example.com/myapp, the import path for the mathutil package is example.com/myapp/mathutil.

How main differs from library packages

  • package main builds into an executable program. It must define a func main() entry point.
  • Library packages (anything not named main) build into reusable code that other packages import.
  • Design goal: keep main small and push reusable logic into library packages with clear APIs.

2) Import syntax, grouping imports, and avoiding unused imports

To use another package, you import it. Imports are file-scoped: each file declares the packages it needs.

Basic import syntax

import "example.com/myapp/mathutil"

Then you refer to exported identifiers with mathutil.Name.

Continue in our app.

You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.

Or continue reading below...
Download App

Download the app

Grouped imports

When you import multiple packages, use a parenthesized import block. A common convention is to group standard library imports separately from third-party or internal imports (blank line between groups). Tools like gofmt will format the block consistently.

import ( "fmt" "example.com/myapp/mathutil" )

Unused imports

Go does not allow unused imports. If you import a package and don’t use it, compilation fails. This is intentional: it keeps dependencies explicit and files tidy.

  • If you added an import temporarily, remove it when you no longer need it.
  • If you need a package only for its side effects (rare), you can use a blank identifier import: import _ "pkg". Use this sparingly and document why.

3) Exported identifiers and API surface design

In Go, visibility is controlled by capitalization:

  • Exported: names starting with an uppercase letter (for example, Add, RoundTo) are accessible from other packages.
  • Unexported: names starting with a lowercase letter (for example, clamp, validate) are only accessible within the same package.

Designing a simple package API

A good beginner-friendly rule: export the smallest set of functions/types that a caller needs, and keep helpers unexported. This gives you freedom to refactor internals without breaking users.

  • Expose stable concepts: functions/types that represent what the package “does”.
  • Hide implementation details: parsing, validation, intermediate steps, constants that are not part of the contract.
  • Prefer clear names over clever names. Callers should understand usage from the identifier name and its doc comment.

Also consider error handling as part of your API surface. If a function can fail due to input, decide whether to return an error or define behavior for out-of-range values. Keep the contract explicit in documentation.

4) Initialization: init() usage guidelines and when to avoid it

Go supports package initialization in two ways:

  • Package-level variable initialization (for example, var x = compute()).
  • An optional func init() that runs automatically when the package is imported.

When init() can be appropriate

  • Setting up internal package state that must exist before exported functions run (for example, building a lookup table).
  • Registering implementations with a registry (common in plugin-like designs).

When to avoid init()

  • When it hides important work (network calls, reading files, heavy computation) that callers cannot control.
  • When it makes testing harder (implicit global state).
  • When it introduces surprising side effects just by importing a package.

Prefer explicit initialization APIs when callers should control timing and configuration (for example, mathutil.New(...) or mathutil.Configure(...)). If you do use init(), keep it fast, deterministic, and side-effect-light.

5) Documentation comments and using go doc

Go documentation is built from comments placed immediately before package declarations and exported identifiers.

Doc comment conventions

  • Package comment: a comment block right before package mathutil describing the package purpose.
  • Exported identifier comment: should start with the identifier name (for example, // Add returns ...).
  • Keep comments focused on what the API does, its inputs/outputs, and edge cases.

Inspecting docs with go doc

From your module root, you can inspect documentation in the terminal:

go doc example.com/myapp/mathutil go doc example.com/myapp/mathutil.Add

This encourages writing small, clear doc comments because they become the primary “help text” for your package.

Build a small example: mathutil package + main

This example creates a reusable mathutil package with a small exported API and internal helpers. You will then import it from main.

Step 1: Create the folder structure

myapp/   go.mod   main.go   mathutil/     mathutil.go     helpers.go

Initialize the module (choose your own module path):

cd myapp go mod init example.com/myapp

Step 2: Write the mathutil package

Create mathutil/mathutil.go:

// Package mathutil provides small math helpers with a minimal API surface. package mathutil // Add returns the sum of a and b. func Add(a, b int) int { return a + b } // Average returns the integer average of values. // It returns 0 for an empty slice. func Average(values []int) int { if len(values) == 0 { return 0 } sum := 0 for _, v := range values { sum += v } // internal helper keeps rounding logic in one place return divRound(sum, len(values)) }

Create mathutil/helpers.go (unexported helpers):

package mathutil // divRound divides a by b and rounds to the nearest integer. // It assumes b > 0. func divRound(a, b int) int { q := a / b r := a % b // Round half up for positive numbers. // (For a more complete implementation, you would define behavior for negatives.) if r*2 >= b { q++ } return q }

Notice the API design choices:

  • Add and Average are exported (capitalized) because they are intended for callers.
  • divRound is unexported because it’s an implementation detail; callers don’t need it and you may want to change it later.
  • Doc comments describe behavior and edge cases (like empty input).

Step 3: Import and use the package from main

Create main.go:

package main import ( "fmt" "example.com/myapp/mathutil" ) func main() { fmt.Println("Add:", mathutil.Add(2, 3)) nums := []int{10, 11, 12} fmt.Println("Average:", mathutil.Average(nums)) }

Step 4: Run and inspect docs

Run the program:

go run .

Inspect package documentation:

go doc example.com/myapp/mathutil go doc example.com/myapp/mathutil.Average

If you try to call mathutil.divRound from main, it will fail to compile because it is unexported. That is the boundary a package provides: a small, intentional public surface with private implementation details behind it.

Now answer the exercise about the content:

Which statement best explains why a call to mathutil.divRound from package main fails to compile?

You are right! Congratulations, now go to the next page

You missed! Try again.

In Go, identifiers starting with a lowercase letter are unexported, so they are only accessible inside the same package. Only exported (capitalized) names can be used from another package.

Next chapter

Go Modules: Dependencies, Versioning, and Reproducible Builds

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.