skip to content

Packages and Visibility

How a Go program is organized above the file: packages as the unit of encapsulation, capitalization as the access modifier, and the initialization order that runs before main. Interviewers use this area to see whether you can structure a service, not just write functions.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

22

Why does the Go compiler reject a file that imports a package it never uses?

level: juniorimportance: must knowfreq 60%

answer

  1. error, not a warning
  2. warnings get ignored
  3. every import costs build graph
  4. underscore marks it deliberate

basics

~20 s

Go makes an unused import a hard compile error rather than a warning: dead imports slow builds, grow the dependency graph and mislead readers. Delete the line, or make it deliberate with a blank import, an underscore before the path.

solid answer

~40 s

The build stops with `"os" imported and not used`. Go's designers chose an error over a warning on purpose: a warning nobody fixes is noise, and every import is a real edge in the build graph that costs compile time and drags transitive packages into the binary. The same discipline covers unused local variables (`declared and not used`), while unused package-level variables, function parameters and struct fields are all fine. In practice nobody maintains the import block by hand — editor tooling in the `gofmt` family, such as `goimports`, adds and removes lines on save. When you genuinely need a package linked in without ever naming it, you write `import _ "path"`; the underscore binds no identifier, so there is nothing to be unused, and the omission reads as intentional.

code

go · 8 lines
go
import (
	"fmt"
	"os" // compile error: "os" imported and not used
)

func greet() {
	fmt.Println("hi")
}

go deeper

for a junior

Be ready to say it is a compile error, not a warning, and to name the fix: delete the import, or write it with an underscore if you need the package only for its side effects.

for a middle

Explain the rationale — warnings get ignored and every import is a real build-graph edge — and know the boundary: unused locals are errors too, unused package-level variables, parameters and fields are not.

for a senior

Show how the rule shapes workflow: editor tooling maintains the import block, so reviews never argue about stale imports. Point out that the rule proves every named import is used, which is exactly why a blank import needs a comment.

for a principal

Frame it as the language trading small daily friction for a dependency graph you can trust. Be ready to say where else you would accept compiler-enforced hygiene over lint rules a team can quietly disable.

## The rule An import declaration introduces a name into the **file block** — the scope covering exactly one `.go` file. If your file imports `os` and no expression in that file mentions `os`, the compiler refuses to build it and prints something like: ./main.go:5:2: "os" imported and not used This is an error, not a diagnostic you can downgrade. There is no compiler flag, no pragma and no comment that suppresses it. Every other Go toolchain behaviour follows from that: the language has no "warnings" tier at all for this class of problem. ## Why an error rather than a warning Two reasons, both practical rather than theoretical. First, **warnings get ignored**. Once a build prints twenty of them, nobody reads the twenty-first, and the signal is gone. Go's position is that if something is worth telling the programmer, it is worth stopping for; if it is not worth stopping for, it does not belong in the compiler. Second, **imports are not free**. An import is an edge in the build graph. It pulls a package — and everything that package imports — into compilation and, for anything actually reachable, into the binary. A stale import that survived a refactor keeps a dependency alive that the code no longer needs, which shows up as slower builds, a larger binary, and a dependency list that misrepresents what the package really depends on. Keeping the graph honest at zero cost to the compiler is worth a small daily friction to the programmer. A third effect is social: because the rule is absolute, no code review ever argues about stale imports, and no lint configuration has to be agreed on. ## What the rule does and does not cover The same strictness applies to **unused local variables**: `x declared and not used`. It deliberately does not apply to: - package-level variables that nothing reads, - function parameters and named results, - struct fields, - unused constants and types. The distinction is that an unused import or an unused local is almost always either a leftover or a bug — you meant to use it and did not — whereas package-level declarations are frequently part of an API that other files or other packages consume, so the compiler cannot make the same judgment locally. ## The escape hatches There are exactly two, and only one of them is idiomatic. The idiomatic one is the **blank import**: `import _ "image/png"`. The underscore is the blank identifier, so the import binds no name; there is no name to be unused, and the compiler is satisfied. The package is still compiled, linked and initialised, which is the whole point of the form — it exists for packages whose value is a side effect of being present rather than an API you call. The scratch one, useful while debugging, is to reference the package once from a package-level declaration: `var _ = fmt.Println`. That keeps a named import alive while you comment out the code that used it. It is fine in a working tree and does not belong in a commit. Similarly, `_ = x` silences an unused local. Both are stopgaps, not style. ## Living with the rule The reason this is a footnote in daily Go rather than an irritant is tooling. `goimports` and the language server's organise-imports action rewrite the import block on every save: they delete what you stopped using and add what you just started using, grouping and sorting the block as `gofmt` would. Most Go programmers never type an import line by hand. ## The consequence worth understanding Because the compiler enforces this rule, you get a real guarantee: **every named import in a Go file is actually used by that file**. That guarantee is exactly why the blank import is a special case worth commenting. A blank import is the one import the compiler cannot check for you — nothing references it, so nothing breaks at build time if it is deleted. That is why the convention is to put a short comment on the line saying which side effect it is there for.

  • Does Go treat an unused local variable the same way?
    Yes — the compiler reports `declared and not used` for a local you never read. Unused package-level variables, function parameters and struct fields are all allowed, because the compiler cannot tell locally whether something else consumes them. If you need to keep a local around while debugging, assigning it to the blank identifier (`_ = x`) silences the error.
  • Is there any way to keep an import whose package you never reference by name?
    Write it as a blank import: `import _ "image/png"`. No identifier is bound, so nothing can be unused, and the package is still compiled, linked and initialised. Convention is a short comment on the line saying which side effect you are relying on, because no tool can infer that for you.

saying these in an interview costs you the question

  • Says go vet reports unused imports, not the compiler
  • Claims it is a warning you can suppress with a build flag
  • Thinks unused package-level variables are errors too
  • Believes the linker drops it anyway, so it is harmless
open as a page

What does a Go init() function do, and when does it run relative to main()?

level: juniorimportance: must knowfreq 72%

basics

~10 s

Go runs a package's init() functions automatically at startup: after that package's package-level variables are assigned and after every package it imports is fully initialized, but before main() begins. You cannot call init() yourself.

open as a page

What does putting a Go package under a directory named internal/ do, and who can still import it?

level: juniorimportance: must knowfreq 62%

basics

~20 s

A package whose import path contains an internal element can be imported only by code inside the tree rooted at that internal/ directory's parent. Any other import fails at build time with 'use of internal package ... not allowed'.

open as a page

In Go, what does `:=` inside an if block do when the enclosing function already declares that name?

level: juniorimportance: must knowfreq 62%

basics

~20 s

It declares a new variable that lives only until that block's closing brace. The outer variable of the same name is hidden, not assigned, so whatever the inner block writes is lost when the block ends.

open as a page

In Go, what makes an identifier visible to code in another package?

level: juniorimportance: must knowfreq 82%

basics

~20 s

The case of its first letter. A name beginning with an upper-case letter is exported and can be used by importing packages; a name beginning with a lower-case letter is usable only inside the package that declares it.

open as a page

In what order does Go initialize package-level variables when one depends on another?

level: middleimportance: must knowfreq 55%

basics

~20 s

In dependency order, not source order. Go assigns each package-level variable only after everything its initializer references has been assigned, regardless of file or line. Declaration order merely breaks ties, and a mutual dependency is a compile error.

open as a page

Can code in one file of a Go package read an unexported field of a struct declared in another file of that package?

level: middleimportance: must knowfreq 58%

basics

~20 s

Yes. In Go the package is the unit of encapsulation, not the file and not the type. Every file of a package shares one scope, so any file can read and assign the unexported fields of any type declared in that package.

open as a page

How do you break a Go import cycle that the compiler rejects between two packages?

level: seniorimportance: must knowfreq 66%

basics

~20 s

Go requires an acyclic package graph and fails the build with import cycle not allowed. Break it by declaring the interface you need in the consuming package, by lifting the shared types into a third package both import, or by merging two packages that were split in the wrong place.

open as a page

In a Go import, which identifier does the import path bind, and when do you alias it?

level: middleimportance: should knowfreq 52%

basics

~20 s

An import path only locates the package. The identifier bound in that file is the package's own declared name from its package clause, which usually but need not match the last path element. Alias when two imports would bind the same name.

open as a page

In a Go program with many imported packages, in what order are those packages initialized?

level: middleimportance: should knowfreq 45%

basics

~20 s

Bottom-up over the import graph: a package is initialized only after every package it imports is finished, and each package is initialized exactly once however many importers it has. Package main goes last, then main.main runs.

open as a page

What does a cmd/<name>/main.go layout give a Go repo that ships five binaries?

level: middleimportance: should knowfreq 48%

basics

~20 s

Each binary gets its own directory holding a package main, because a directory is one package and can declare func main only once. The directory name becomes the executable name, and shared code moves to an importable package.

open as a page

In Go, what is the scope of a variable declared in an if statement's init clause?

level: middleimportance: should knowfreq 54%

basics

~20 s

It covers the whole if statement: the condition, the if body, and every else-if and else branch. It does not exist before or after the statement, because the init clause declares into an implicit block wrapping the entire if.

open as a page

Why would a Go package keep a struct's fields unexported and hand callers an exported constructor instead?

level: middleimportance: should knowfreq 50%

basics

~20 s

To control how values are built and how they are represented. With the fields unexported, importers cannot set them, so the constructor becomes the only place invariants can be established, and the field layout can change later without breaking callers.

open as a page

Why does sql.Open fail with an unknown-driver error in a binary that compiled fine?

level: seniorimportance: should knowfreq 46%

basics

~20 s

The driver package was never linked into the binary. Nothing in your code names it, so the only edge pulling it in is a blank import, an underscore before the path, and a refactor can delete that line without producing any compile error.

open as a page

A Go daemon panics during startup inside an init() function and logs nothing. How do you diagnose it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Read the raw stack trace on standard error: initialization runs before main() configured logging, so nothing reached the log pipeline. The stack names the failing package. Then move failable work into a constructor that returns an error.

open as a page

In a Go repo, internal/util is imported by every package and keeps growing — how do you split it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Inventory what is in it and who calls each item, then move each cluster into its own internal package named for what it does. Helpers with one caller move into that caller, unexported; dead ones are deleted.

open as a page

A Go function returns a nil error even though an inner if block set err — how do you diagnose and prevent this class of bug?

level: seniorimportance: should knowfreq 45%

basics

~20 s

The inner block's := declared a second err that died at the closing brace, so the returned variable kept its nil zero value. Confirm by changing := to =; prevent it with the x/tools shadow analyzer and a test on the failure path.

open as a page

Importers already set a struct field your Go package exported by accident — how do you take it back?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Lower-casing the name breaks every importer at compile time, so do it in place only when you can rebuild them all in one change. Otherwise add the replacement API, mark the field deprecated, and remove it at the next major version.

open as a page

How do you decide which packages go under internal/ in a repo other teams import?

level: principalimportance: should knowfreq 32%

basics

~20 s

Default everything to internal/ and promote a package out only for a consumer that exists today. An importable path is a support commitment you cannot withdraw quietly, so promotion is reviewed as an API change.

open as a page

In Go, what happens when a local variable is named `len` or matches an imported package name?

level: middleimportance: nice to knowfreq 36%

basics

~20 s

Both compile. Go resolves names innermost-first through block, file, package and universe scopes, so a local declaration hides a builtin like len or an imported package name for the rest of that block, and later uses of the original meaning fail to compile.

open as a page

What is your policy on doing real work in init() in a Go library many teams import?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Allow only cheap, deterministic, un-failable work at import time, because every consumer pays for it unconditionally and cannot configure, order, skip or error-check it. Anything that reads ambient state or can fail belongs in an exported constructor the caller invokes.

open as a page

How do you decide a Go package's exported surface when many teams import it and unexporting later breaks them?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Export the smallest set of names that lets consumers do their job, and treat every capital letter as a promise you cannot withdraw cheaply. Start names unexported, export on a concrete request, and review the exported diff at every release.

open as a page