Skip to content

PATTERN Cited by 1 source

Build-tag dependency isolation

Intent

Keep an unwanted dependency out of some Go binaries by marking the file that imports it with a build tag the wanted binaries don't pass. The compiler skips the file, its imports never reach the linker, and the transitive dep tree rooted at it is gone.

When to use

  • A library optionally supports a feature whose implementation drags in heavy dependencies (e.g. a runtime-plugin facility, vendor-SDK integrations, rarely-used codec).
  • Most consumers don't use the feature and would rather not pay its binary-size cost.
  • Splitting the library into sub-packages would break API compatibility or is upstream-unfriendly.

Mechanism

  1. In the importing file, add a build tag:
    //go:build optional_feature
    
    package foo
    
    import "heavy/dep"
    // ... uses heavy.Dep ...
    
  2. Consumers that need the feature pass -tags optional_feature; everyone else builds without it.
  3. The compiler's file-selection pass excludes the file entirely, so heavy/dep — and its full transitive closure — never reach the linker.

Canonical wiki instance: containerd's plugin import

containerd/plugin/plugin_go18.go unconditionally imported the stdlib plugin package for user-loadable containerd plugins. Any program transitively importing containerd paid the 245-MiB-and-rising cost of plugin-mode linking.

Datadog's upstream fix — containerd#11203 — added a build tag gating the plugin import. Downstream consumers (like the Datadog Agent) build without the tag → plugin import doesn't exist in their binary → method-DCE re-engages → 245 MiB recovered, ~75 % of Agent users benefit. Instance of patterns/upstream-the-fix.

Relation to package-split

The complementary pattern is patterns/single-function-forced-package-split:

Pattern When
Build-tag isolation Dep-importing code is optional behaviour within a package most consumers want to import unchanged.
Package-split The dep-importing code is one function / symbol in a package consumers unavoidably import for other reasons.

Both prune transitive imports at their source. Both are standard Go techniques. Which one fits depends on whether the optionality is natural to expose at the file/tag boundary (build tags) or at the package boundary (split).

Forces

  • + Low code churn, upstream-friendly (single-file diff with a tag directive).
  • + Composable with the feature-per-binary build matrix that large Go programs already use.
  • + No API break.
  • Tag names become API. Adding a tag to remove an import is a behaviour change any downstream depending on the current default must opt into or out of.
  • Users who want the feature have to know to pass the tag.
  • Not a substitute for package discipline. If a single file keeps accumulating optional imports, split the package.

Seen in

Last updated · 200 distilled / 1,178 read