skip to content

How do you list every package in a Go build that imports unsafe or os/exec, or uses cgo?

level: middleimportance: nice to knowfreq 25%

answer

  1. expand the build, then filter
  2. -deps closes over imports transitively
  3. -f templates read package fields
  4. one field just for cgo files
  5. diff base against branch, not raw lists

basics

~20 s

Use go list -deps ./... to expand the full transitive set of packages the build compiles, with a -f template printing each package's import list, then filter for unsafe and os/exec. Packages whose CgoFiles field is non-empty are the ones using cgo.

solid answer

~50 s

`go list -deps ./...` expands to every package the named packages transitively import, standard library included, in dependency order — that is the real inventory of what gets compiled. Adding `-f` lets you print fields of the package structure, so `{{.ImportPath}} {{join .Imports " "}}` gives one line per package with its direct imports, and grepping that for `unsafe` or `os/exec` finds the capability you care about. `{{if .CgoFiles}}` isolates the packages that use cgo, and `{{if not .Standard}}` drops the standard library so you are only looking at dependencies. `-json` gives the same data machine-readably. The trick that makes this useful in review rather than overwhelming is to run it on the base commit and on the branch and diff the two sets: `unsafe` appears all over the standard library and is noise, while a *newly added* non-standard package importing `unsafe` or `os/exec` is exactly the thing to ask about.

code

text · 2 lines
text
$ go list -deps -f '{{if not .Standard}}{{.ImportPath}} {{join .Imports " "}}{{end}}' ./... \
    | grep -E ' (unsafe|os/exec)( |$)'

go deeper

for a junior

Know that go list -deps expands a build to every package it transitively imports, and that -f prints chosen fields of each one. Try it on a small module and read the output.

for a middle

Be able to write the command from memory, name the fields you would template over — ImportPath, Imports, CgoFiles, Standard — and say why filtering out the standard library is what makes the result readable.

for a senior

Turn it into something a team uses: compute the package set on both sides of a change, report only the delta, and be clear with the reviewer about what a static import inventory does and does not prove.

for a principal

Decide whether this belongs in the pipeline at all, what it is allowed to block versus merely comment on, and who is accountable when it flags a package that has been in the build for two years.

## What `-deps` gives you that nothing else does `go list` without flags prints the packages you named. With `-deps`, it prints those packages *plus every package they transitively import*, in dependency order (dependencies before dependents), and it includes the standard library. That list is the compile-time truth: it is the set of packages the toolchain will build and link. Compared with `go.mod`, which records module version constraints, and `go mod graph`, which records requirement edges, `-deps` is the only one of the three that answers "what code ends up in the binary". ## Templating over package fields `go list -f` takes a text template evaluated against a structure describing each package. The fields worth knowing for review are: - `.ImportPath` — the package's full path. - `.Imports` — the package's direct imports, as a list. `join` is available as a template function. - `.Deps` — the transitive import closure of that one package. - `.CgoFiles` — the `.go` files in the package that import `"C"`. Non-empty means the package uses cgo. - `.Standard` — whether the package is part of the standard library. - `.Module` — the module the package came from, with path and version. So the capability sweep is one command: ``` go list -deps -f '{{if not .Standard}}{{.ImportPath}} {{join .Imports " "}}{{end}}' ./... ``` Pipe that through a filter for the import paths you care about. And cgo is its own field: ``` go list -deps -f '{{if .CgoFiles}}{{.ImportPath}}{{end}}' ./... ``` For anything that needs to be parsed rather than eyeballed, `go list -deps -json ./...` emits the same records as a JSON stream. ## Why the three signals are worth separating **`unsafe`** disables the type and memory safety the rest of the language guarantees: pointer arithmetic through `unsafe.Pointer`, reinterpreting one type's memory as another, building slices and strings over arbitrary addresses. It is not automatically wrong — the standard library uses it heavily, `reflect` cannot exist without it, and high-performance libraries use it correctly — but it means correctness now rests on invariants the compiler is not checking, and mistakes there produce memory corruption rather than a panic at the fault site. `go vet`'s `unsafeptr` check catches one specific misuse (converting `uintptr` back to `unsafe.Pointer`), not the general case. **cgo** changes your build, not just your risk. It requires a C toolchain, complicates or blocks cross-compilation, and the race detector does not see inside the C code. A crash on the C side is not a Go panic and does not produce a Go stack trace. **`os/exec`** means the package runs external programs, which is a runtime dependency that `go.mod` does not record and the compiler cannot check — the missing tool is discovered in production. ## Turning it into a review gate The useful form of this is not a one-off command but a diff. `unsafe` is transitively reachable from almost any real build because of the standard library, so a raw list is all noise. Compute the set on the base commit, compute it on the branch, and report the difference: which non-standard packages are new to the build, and which of those new ones import `unsafe`, import `os/exec`, or have cgo files. A pull-request bot that comments only on that delta gives the reviewer three lines to think about instead of three hundred, and it makes the question concrete: *this* change adds *this* package, which shells out. ## The limits of a static inventory Be honest about what this proves. It is an import inventory, not behaviour. A package that imports `os/exec` may only use it on a code path you never take; a package with no risky imports can still exhaust memory, leak goroutines or send your data somewhere over `net/http`. The inventory tells you where to *read*, and reading the candidate's source is still the work. It also only covers the packages in the build configuration you ran it in: build constraints, `GOOS`/`GOARCH` and build tags change which files — and therefore which imports — are included, so a sweep run on your laptop may miss a file that only compiles for the target platform. ## Common mistakes - Confusing `go list -deps` (packages, compiled) with `go list -m all` (modules, build list). They answer different questions. - Treating any appearance of `unsafe` as a finding. Filter the standard library out and look at what changed. - Assuming one sweep covers all platforms. Re-run it with the `GOOS` and `GOARCH` you actually ship.

  • Why is a raw list of every package importing unsafe close to useless on a real service?
    Because the standard library uses unsafe extensively — reflect could not exist without it — so the closure of almost any build contains dozens of hits. Filter with `{{if not .Standard}}` and compare the branch against the base commit; the signal is a newly added third-party package with that import, not the presence of the import anywhere.
  • What does this inventory fail to tell you about a dependency?
    Behaviour. An import is a capability, not a call: a package importing os/exec may never reach that path, and a package with a clean import list can still leak goroutines, allocate without bound or make network calls through net/http. The sweep tells you which files to read; it does not replace reading them.
  • Could the same sweep miss a risky import entirely?
    Yes. go list evaluates build constraints for the current GOOS, GOARCH and build tags, so files that only compile for another platform are excluded along with their imports. If you ship linux/arm64 from a macOS laptop, re-run the sweep with those values set before you trust it.

saying these in an interview costs you the question

  • Confuses go list -deps with go list -m all
  • Treats every use of unsafe as automatically a defect
  • Forgets build constraints change which imports are visible
  • Assumes an import inventory proves what the code does
  • Reads go.mod and calls it the list of compiled packages