AlgoMaster Logo

init() Function

Last Updated: May 22, 2026

Medium Priority
6 min read

Sometimes a package needs to do work before any of its code is actually called. Build a lookup table, register a payment processor, validate a configuration value, seed a random source. Go gives every package a special hook for exactly this: a function named init that runs automatically on startup, before main ever executes. This lesson covers how init works, when it fires, and the patterns Go developers use it for.

The init Function Signature

init is a regular function with two strict rules: it takes no parameters and returns no values. You never call it yourself. The Go runtime calls it for you during package startup.

Try giving init a parameter or a return value and the compiler stops you cold with func init must have no arguments and no return values. The signature isn't a convention you can bend. It's enforced.

The same goes for the name. Lowercase init, nothing else. There's no way to "export" it (and no reason to), so it's always unexported. And unlike most Go functions, init is allowed to be declared multiple times in the same file or package. We'll get to why that matters in a moment.

When init Runs

The Go runtime initializes a package in a specific order, and init has a fixed slot in that sequence:

  1. All imported packages are initialized first (recursively).
  2. Package-level variables are initialized in their declaration order, respecting dependencies between them.
  3. Every init function in the package runs.
  4. Only then does control reach main.

The following shows the order:

The package-level variable catalogSize is initialized first, which runs computeCatalogSize. Then init runs. Then main finally starts. If you flip the declaration order in the file, the variable still runs before init, because the spec guarantees that.

Multiple init Functions

A single file can declare more than one init function. So can a single package across multiple files. Both are legal, both are useful, and both have their own ordering rules.

Within a single file, init functions run top to bottom in the order they appear in the source. That's deterministic and you can rely on it.

Across multiple files in the same package, the order is the order the compiler presents the files to the linker, which is alphabetical by filename in practice but not guaranteed by the spec. The takeaway: don't write `init` functions whose correctness depends on the order across files. If one piece of setup must happen before another, put them in the same file (in the right order), or chain them through a shared variable, or merge them into one function.

Why Multiple init Functions

Splitting setup into several init functions isn't just allowed, it's often clearer. Each one can own a single concern.

Each init does one thing. The category map and the tax table are independent, so giving each its own initializer keeps the intent visible. You could fold both into a single init, but keeping them separate makes it easier to add or remove either one later.

Imported Packages Initialize First

Before any code in your package runs, every package you import is fully initialized. That includes their variables and their init functions. And this is recursive: if package A imports B, and B imports C, then C initializes first, then B, then A.

The diagram shows the full startup flow for a single package. Imports finish first, package-level variables run next, then every init in declared order, and only after all that does main get control. Each package the importer pulls in goes through the same sequence first.

Go guarantees that a package is initialized exactly once, even if it's imported many times through different paths. So if both your code and a library you use both import the standard library's time package, time's init still only runs once.

Common Uses for init

A few patterns come up over and over.

Building lookup tables. When a map can be computed from constants, doing it once at startup beats recomputing it on every call.

Validating configuration at startup. If a required setting is missing or wrong, fail fast at startup rather than at the first request. init can call log.Fatal to refuse to start when a precondition fails. Be careful here: every binary that imports the package pays this cost, even tests that don't need that config.

Registering handlers or drivers. This is the classic use. A package registers itself with some central registry the moment it's loaded. Database drivers (database/sql), image format decoders (image/png), and HTTP handlers all use this pattern.

By the time main runs, both processors are already in the map. Real systems do this across packages: paymentcard and paymentwallet are each their own package, each with an init that calls into a shared registry, and the application just imports them.

Side-Effect Imports

Some packages exist only for what their init function does. You don't call any of their exported names; you just want them loaded so their registrations happen. The problem: Go normally refuses to compile an import you don't use.

The fix is the blank identifier:

The _ says "I'm importing this for its side effects only, don't complain that I never reference it". The package's init runs, its registrations happen, and your code gets the behavior without ever naming the package. This is the most common reason to use the blank identifier at the package level.

A typical pattern in a real application:

The application code in main doesn't know or care which processors exist. Adding support for a new payment method is a one-line import. Removing one is a one-line delete. The registry approach plus side-effect imports is how many Go libraries handle pluggable backends.

Anti-Patterns

init is genuinely useful, but it's easy to overuse. A few things to avoid.

Heavy work in init. Network calls, large file reads, big computations. Every program that imports the package pays this cost on startup, including tests and short-lived tools that don't need the feature. Push expensive setup behind a function or a sync.Once that runs when something is first used, not when the package is loaded.

Panicking unnecessarily. A panic (or log.Fatal) inside init kills the whole program before main ever runs. That's fine when the precondition really is required for the program to function at all. It's a bad fit for "this feature might not be needed". Reserve init-time crashes for genuine startup failures, like a missing required environment variable.

Making init order load-bearing. Across files, the order isn't guaranteed by the language spec, so writing init functions that depend on "the other file's init has already run" is a bug waiting to happen. If two pieces of setup must happen in a specific order, put them in the same file in the right order, or write one init that calls both helpers, or use a single init plus regular function calls.

Using init when a package-level variable initializer would do. If all you're doing is assigning a value, a var declaration is shorter and the intent is clearer.

The first form earns you nothing. The second is one line, runs at the same point in startup, and reads more directly. Save init for things a single declaration can't express: building a map, validating a value, registering with another package.