AlgoMaster Logo

Packages Basics

Last Updated: May 22, 2026

High Priority
12 min read

A Go program is organized into packages. Every .go file declares which package it belongs to on its very first line, and the compiler uses that declaration to group files, control visibility, and decide what's reachable from where. This chapter covers what a package is, the one-directory-one-package rule, how multiple files in the same package see each other, the difference between main and library packages, exported vs unexported names, package-level variables and constants, init() functions, documentation conventions, and how to name a package well.

What a Package Is

A package in Go is a directory of .go files that all declare the same package name on their first non-comment line. The directory is the unit of compilation, the unit of import, and the unit of visibility. When you write import "github.com/example/shop/cart", you're pulling in everything inside the cart directory, treated as one cohesive thing.

Here's the smallest possible Go program, which is itself a package:

The first line, package main, is the package declaration. It tells the compiler "this file belongs to a package called main". Every .go file in the Go ecosystem starts with a line like this. There are no exceptions. A file without a package clause won't compile.

A package isn't a class, a namespace in the Java sense, or a module in the Python sense, even though it shares traits with all three. Think of it as a folder of code that the compiler treats as one library, with a single public surface. Everything inside the folder is one body of code; everything outside accesses it through the imported name.

Packages live inside modules, which are the larger unit Go uses for versioning and dependency management. A module can contain one package or many. For now, just know that the package is the smaller piece, and the module is the wrapper that says "here's a versioned collection of packages I publish".

One Directory, One Package

Go enforces a strict rule: every .go file in a directory must declare the same package. You can't mix package cart and package order in the same folder. If you try, the compiler refuses to build the directory.

Suppose you have a directory cart/ with two files:

File: cart/cart.go

File: cart/wrong.go

Running go build ./cart produces an error like:

The compiler scans the directory, sees two different package declarations, and stops. The rule is one directory, one package, and the directory name and package name don't even have to match (though by convention they usually do). The folder might be called cartservice/, but if every file inside says package cart, the package name is cart.

There's one exception to this rule: test files. Files ending in _test.go are allowed to declare either the same package as the rest of the directory (white-box tests) or that package plus _test (black-box tests). So a file cart/cart_test.go can say package cart or package cart_test, but no other variation is allowed.

Multiple Files, Same Package

A single package can span many files. Splitting a package into multiple files is purely organizational; the compiler treats all of them as one unit. Files in the same package share scope: a function defined in one file can call an unexported helper defined in another, and a struct defined in one file can have methods defined in a different file.

A small cart package split across two files:

File: cart/cart.go

File: cart/total.go

File: cart/item.go

Three files, one package. The Cart struct in cart.go has a method Total() defined in total.go. The unexported helper sum in total.go works on the unexported type item defined in item.go. None of this requires imports or forward declarations between files. Inside a package, every file can see every name the others declare, exported or not.

You'd use this package from another file like so:

File: main.go

The caller imports cart and uses the exported names New, Add, and Total. The unexported item and sum are hidden, even though they're part of the same package. The caller doesn't (and can't) know how the package organized itself internally.

This design has practical value. A package can grow as it needs to without forcing callers to update their imports. You can split a 500-line file into five 100-line files in the same package, and no consumer of the package notices. Conversely, you can merge files back together. The package, not the file, is the unit of API.

The main Package and Executables

Out of all package names, main is special. When the compiler sees package main, it tries to build an executable instead of a library. To do that, the package must also define a function func main() that takes no arguments and returns nothing. The runtime calls that function when the program starts.

Running go build on this produces a binary you can execute. Running go run main.go builds and runs it in one step. The compiler sees package main, finds func main(), links everything together, and emits a real executable.

Library packages (everything that isn't main) don't get built into executables. They get compiled into archive files (.a) and linked into binaries that import them. A library package has no main function and no entry point. It just exposes types and functions for other packages to use.

If a main package is missing the main function, the compiler fails:

And if a non-main package defines func main(), that function is just a regular function. It won't run automatically; it's only invoked if some other code calls it.

The convention in real projects is to put the main package in a directory like cmd/shop/ or just at the root of a small project. The supporting library packages live under directories like cart/, order/, customer/, and so on. The main package imports them and wires them together. That gives one clear entry point and many reusable libraries.

Exported and Unexported Identifiers

Go uses capitalization to control visibility. If a name starts with an uppercase letter, it's exported, meaning code in other packages can see and use it. If it starts with a lowercase letter, it's unexported, meaning only code inside the same package can use it. That's the entire rule. No public, private, or protected keywords. Just the first character of the name.

From another package, you can call catalog.New(...), set p.Name, read p.Price, and call p.Restock(5). You cannot reach p.stock (unexported field) or call p.inStock() (unexported method). The compiler stops you at build time:

The rule applies uniformly: types, functions, constants, variables, methods, and struct fields all follow the same capitalization-controls-visibility rule. Catalog, Add, MaxItems, DefaultTimeout, Name are all exported. catalog (the package name itself is a separate matter), add, maxItems, defaultTimeout, name are all unexported.

Here's the visibility rule in table form:

IdentifierFirst letterVisible from other packages?
ProductUppercaseYes
productLowercaseNo
PriceUppercaseYes
priceLowercaseNo
Add (method)UppercaseYes
validate (method)LowercaseNo

The point of unexported names is encapsulation. A package's exported surface is its contract with the rest of the world. Anything unexported is an implementation detail you can change without breaking callers. Most well-designed Go packages export a small handful of types and functions and keep the rest unexported.

Package-Level Variables and Constants

A Go file can declare variables and constants outside any function. These are called package-level (or top-level) declarations. They live for the entire lifetime of the program and are visible from anywhere inside the package. If their name starts with an uppercase letter, they're also visible from other packages.

Constants are fixed at compile time. Variables can be reassigned at runtime. Both are initialized before any function in the package runs, and they're initialized in dependency order: if var b = a + 1 depends on a, then a is initialized first, no matter where the two declarations appear in the file or in the directory.

Package-level variables come with a sharp edge. Because they live for the whole program and are visible to every file in the package, they're effectively global state. Two goroutines reading and writing the same package-level variable will race unless you protect it. The takeaway here is that shared mutable globals need care.

Constants don't have this problem because they can't be changed. Constants are also more efficient: the compiler can often inline them at the call site, avoiding a memory load entirely.

TaxRate is a constant, used in a multiplication. WelcomeMessage is a package-level variable. Both are defined outside main and are visible inside it. If this file were part of a larger package, both names would also be visible to the package's other files.

init() Functions

Every package can declare one or more functions named init() that run automatically when the package is loaded. An init function takes no arguments and returns nothing. You never call it directly; the Go runtime does. It's the place to put setup code that has to run before anything else in the package can be used.

The runtime calls init before main. By the time main runs, greeting is already set. You don't import anything special to make this work; the function's name alone is the signal.

A package can have more than one init function, and they can be spread across multiple files. There's a precise order Go follows:

  1. Inside a single file, init functions run in the order they're written (top to bottom).
  2. Across files in the same package, files are processed in the order the Go toolchain hands them to the compiler, which today is alphabetical by filename. So cart_a.go runs its init before cart_b.go.
  3. Across packages, an imported package's init runs before the importing package's init. The dependency graph is walked depth-first.

Here's a multi-file example showing the order. Imagine a setup package with three files:

File: setup/a.go

File: setup/b.go

File: main.go

The setup package's init functions run first because main imports it. Inside setup, file a.go (alphabetically first) runs both of its init functions in source order, then b.go runs its single init. Only after setup finishes initializing does main's own init run, and finally main itself.

The blank import _ "github.com/example/shop/setup" is the trick that pulls a package in purely for its init side effects. The relevant point here is that it's how you trigger initialization of a package you don't otherwise reference.

The order across packages can be visualized like this:

The diagram shows four packages with a simple dependency chain. The runtime walks the import graph bottom-up: leaf packages (db, log) initialize first, then packages that depend on them (cart), then the top of the graph (main), and only then does func main() run. If two leaf packages have no dependency between them, the order they initialize is determined by the order they appear in the import path resolution, which in practice means deterministic but not always obvious.

A few practical rules about init:

  • Don't put work in init that can fail in ways the program can recover from. If init panics, the program crashes before main even starts. Reserve init for setup that's guaranteed to succeed or where a failure should genuinely abort the program.
  • init functions can't be called directly. Writing init() in your code is a compile error.
  • init functions can't take arguments or return values. The signature is fixed.
  • Avoid hiding important logic in init. A reader looking at main shouldn't have to hunt through every imported package to understand what runs at startup.

Package Documentation

Go has a strong documentation convention built right into the toolchain. The go doc command and the pkg.go.dev website both render package documentation from comments in your source code. Following the convention means your package gets readable docs for free; ignoring it means your package looks unprofessional.

The rules are simple:

  1. A comment directly above a package declaration is the package's documentation.
  2. A comment directly above an exported identifier (type, function, variable, constant, method) is that identifier's documentation.
  3. The first sentence of the comment should start with the identifier's name and read as a full sentence.

Here's a documented package:

File: cart/doc.go

File: cart/cart.go

Notice the pattern: each comment starts with the name of the thing it documents. Cart represents..., New returns..., Add appends.... Tools like go doc and gofmt won't enforce this style automatically, but the linter golint and reviewers will. Code in the standard library follows this rule rigorously.

The doc.go file is a convention, not a rule. You can put the package-level comment above the package line in any file in the directory. Putting it in a dedicated doc.go is just convenient because it keeps the package overview separate from the code, and there's only ever one such comment per package.

Running go doc github.com/example/shop/cart prints whatever the package comment says, followed by a list of exported names with their own comments. Running go doc github.com/example/shop/cart.Cart zooms in on just that type. The toolchain has been doing this since Go 1.0; the comment style is a small price for the documentation pipeline you get for free.

Package Naming Conventions

A package's name is part of its API. Callers write cart.Add, order.Place, customer.New, so the name shows up at every call site. Picking a bad name shows up there too. Go's conventions for package names are tighter than for most identifiers in the language, and they exist because of how visible the name is.

The rules:

  • Lowercase. Package names are always lowercase. cart, not Cart or CART.
  • Single word when possible. cart beats shoppingcart. If you need two ideas, see if you can rename: httputil exists, but the standard library has http, bytes, strings, not httpUtilities, byteHelpers, stringFunctions.
  • No underscores. cart_helper is wrong; either rename or split into two packages.
  • No mixedCaps. shopCart is wrong; same fix as above.
  • No plurals. cart, not carts. The package holds the concept of a cart, not a collection of carts.
  • Short and meaningful. A few characters is fine if the meaning is clear: fmt, os, io, net. Avoid abbreviating to the point of mystery: cstmr for customer is not OK.
  • Avoid generic names. util, common, helpers, misc say nothing. If you can't name the package after what it does, the package probably shouldn't exist.

The reason these rules exist is the call site. Compare:

with:

The first reads cleanly. The second is noisy and redundant. The package name and the function name shouldn't repeat the same idea. cart.Add is good; cart.AddCartItem is not. The package gives the context, and the function name adds the action.

One more conventions worth knowing: when a package name would clash with a common variable name, pick a different package name or accept that callers will rename it on import. The standard library has a path package, but it's common to also have a local variable named path, so callers sometimes alias the import. Designing the package name to avoid the clash up front saves the friction.

Importing Packages, Briefly

A package on its own is useless. To use it from another package, you import it:

The string inside import is the import path. For standard library packages, it's just the name (fmt, os, strings). For third-party or local packages, it's the full module path plus the directory inside the module. Once imported, the package's exported names are accessed through the package name (cart.New, not cart.cart.New).

That's all this chapter says about imports. For now, the only thing to know is that packages don't import each other by directory path or filename; they import each other by the path declared in the surrounding module.