What can a Go dependency's init functions and package-level variables do to every binary that imports it?
answer
- importing is executing
- before main, not on first call
- the linker will not strip it
- package-level var initializers count too
- a panic there kills the process at startup
basics
~20 sAnything. Package-level variable initializers and every init function in an imported package run before main, in every binary that pulls the package in, directly or transitively. You cannot import a package and skip them, so whatever they do is imposed on your program.
solid answer
~50 sImporting a Go package is not free: the package's package-level variable initializers run, then its `init` functions run, once, before your `main` is entered — and that happens whether you call one exported function from it or none. So a dependency can register an HTTP handler on `http.DefaultServeMux`, define command-line flags, read environment variables, start goroutines, open files, dial a network address, replace a process-wide logger, or panic and kill the process before any of your code has run. The linker's dead-code elimination removes unreferenced functions, but it does not remove `init`. That is why, when I vet a candidate module, I grep its source for `func init(` and for package-level `var x = someCall()` before I read anything else: those lines are the part of the dependency I cannot opt out of, and errors there surface as a startup crash rather than as a returned error.
code
go · 9 linespackage lib
// Runs at package initialization: network I/O before main.
var client = mustDial(os.Getenv("SERVICE_ADDR"))
func init() {
// Adds an endpoint to the process-wide default mux.
http.HandleFunc("/debug/dump", dumpHandler)
}go deeper
Remember the rule: any package you import runs its package-level variable initializers and its init functions before main, whether or not you call it. Be able to say what that means for a dependency you did not choose.
Explain the mechanics: initialization follows the import graph, variables come before init functions, the linker keeps init as a root, and a panic there is fatal before main. Name a concrete standard library package that registers something at init.
Show the review habit — grepping a candidate for func init and for package-level vars with calls on the right-hand side — and connect it to real production symptoms: startup crash loops, endpoints nobody wrote, flags nobody declared.
Set the policy for libraries your organisation publishes: work at import time is a cost imposed on every downstream binary, so decide whether your own packages are allowed to do it and what the constructor-based alternative looks like.
## What actually runs when you import When a Go program starts, the runtime initializes packages in dependency order: a package's imports are fully initialized before the package itself, and `main` is initialized last. Within a package, package-level variables are initialized first — in dependency order among themselves, not source order — and then every `init` function in the package runs. A package may declare any number of `init` functions, in any file, and none of them can be called or referenced from your code. The consequence that matters for review is simple: **importing is executing**. There is no lazy loading at package granularity. If your program's import graph reaches a package, that package's initialization runs in your process, at startup, in every binary that reaches it — including test binaries and any other program in your repository that happens to import a shared internal package. ## What that lets a dependency do Standard library packages use this deliberately, so it is easy to see the shape: - Importing `expvar` registers a handler on `http.DefaultServeMux` in its `init`. If your service serves on that default mux, an endpoint you never wrote is now exposed. - Image format packages register decoders through `image.RegisterFormat` at init time, which is why a blank import (`import _ "image/png"`) is the idiomatic way to "turn on" a format. A third-party package can do all of that and more: `flag.String` at package level adds flags to `flag.CommandLine` and changes your program's command-line surface; `log.SetOutput` or `log.SetFlags` in an `init` silently reshapes logging for everything else; a package-level `var conn = mustDial(os.Getenv("ADDR"))` performs network I/O at startup; a goroutine started in `init` runs for the life of the process with no way to stop it; and a `panic` in `init` terminates the program before `main`, so no `recover` of yours can see it and no health check ever passes. None of this requires malice to hurt you. A library that is perfectly reasonable as an application's top-level choice becomes a problem the moment it is buried three levels down in a dependency chain, because the binary that pays the cost is not the one that made the choice. ## Why the linker does not save you The Go linker performs dead-code elimination: exported functions you never reference are not carried into the binary. `init` functions are exempt — they are roots. So "we only use one helper from that package" does not shrink the initialization surface at all. The unit of initialization is the package, and the unit of dependency is the import graph, not the call graph. ## How to read a candidate for this With the module in the local cache you are reading plain text, so grep is the right tool: - `func init(` gives you the explicit hooks. - Package-level `var` declarations whose right-hand side is a call — `var x = compile(...)`, `var c = newClient()` — run the same way and are easy to miss because they do not look like code. - Follow the candidate's own imports one level: its dependencies' inits run too. Then ask three questions about each one: does it touch process-global state (`http.DefaultServeMux`, `flag.CommandLine`, the standard logger, `os.Stdout`)? Does it perform I/O or start a goroutine? Can it panic on a machine where an environment variable is missing? Anything that says yes belongs in the PR discussion, because it is the part of the dependency your program runs unconditionally. ## The idiomatic alternative, and what to ask the author for The Go-idiomatic replacement for work at init time is to do it lazily or explicitly: a constructor the caller invokes, or `sync.Once` guarding first use. A library that exposes `New(...)` and does nothing at import time can be imported by anyone at no cost. That is a reasonable thing to ask a maintainer for, and a reasonable reason to reject an import — especially in a package other teams will import, where you are choosing on their behalf. ## Common mistakes - Believing an unused import is stripped so its `init` never runs. It runs. - Believing a blank import is the only way to trigger init side effects. Any import triggers them; the blank form only exists because the compiler rejects an unused named import. - Assuming initialization failures are catchable. A panic in `init` is fatal to the process, before `main`.
- If your code imports a package but never calls anything in it, does its init still run?Yes. The compiler requires you to reference a named import, but the blank form `import _ "pkg"` exists precisely so you can trigger initialization without a reference. Either way the package's variable initializers and every init function run. Dead-code elimination removes unreferenced functions from the binary but keeps init functions, which are roots.
- What would you ask a library author to change if their init does the work you object to?Move it behind an explicit constructor the caller invokes, or guard it with sync.Once so it happens on first real use. A library that does nothing at import time can be imported by anyone at no cost, which matters most when the importer is itself a library other teams depend on.
- Why is an init function that panics worse than one that returns a bad result?It terminates the process before main is entered, so nothing of yours can recover it, no logging you configured is in place, and no readiness endpoint ever answers. The failure looks like a crash loop with a stack trace from a package the on-call engineer has never heard of.
saying these in an interview costs you the question
- Thinks unused imports are stripped so their init never runs
- Believes only blank imports trigger side effects
- Assumes initialization runs lazily on first call into the package
- Ignores package-level var initializers as if only init counts
- Expects to recover from a panic raised during initialization