Skip to content
callcount

methodology · v0.2.0

How the numbers are made

Callcount type-checks open-source Go and ties every function call back to the function it resolves to, standard library or third-party. Here is the pipeline a count runs through, how each call is labelled, and where the method stops. The limits are on the page, in the same plain terms as the counts.

How a count is made

One count is one call expression in source that resolves to a Go function or method. Four stages turn a module zip into a ranked number.

  1. 01 fetch

    Fetch

    Pull each module@version as a zip from the Go proxy's sanctioned bulk endpoint. vendor/ and testdata/ are dropped before anything is parsed.

  2. 02 type-check

    Type-check

    Run go/types on each package in dependency order. An export-data cache feeds already-checked packages, stdlib and in-corpus dependencies, forward, so receivers resolve concretely across module boundaries.

  3. 03 label

    Label

    Classify every call expression into one of eight resolution kinds by its resolved type object, never by name string. Loop depth and test/generated flags ride along.

  4. 04 aggregate

    Aggregate

    Roll callsites up to per-symbol totals in DuckDB and rank by production calls, everything outside tests and generated code.

  • Calls are attributed by resolved type object, the defining package and receiver, never by matching a name string, so a local Split is never mistaken for strings.Split.
  • Every target is tagged std (standard library) or ext (third-party); both are counted the same way.
  • Test and generated calls are counted but flagged. The default ranking is production calls, everything outside *_test.go and generated files, the number that answers where optimization pays.

The resolution taxonomy

Not every call resolves with the same certainty. Each is labelled with exactly one of eight kinds, and the mix is published per symbol. Only the resolved kinds, where the target function is known, count toward the ranking. Because packages are checked in dependency order and fed forward through an export-data cache, a call into any module that is itself in the corpus resolves fully typed; only calls into modules outside the crawl fall back to pkgfunc-syntax or unresolved.

pkgfunc

Package function

Counted

A plain call to an exported function whose target the type-checker resolves exactly, standard library or an in-corpus module alike. The cleanest attribution there is.

strings.TrimSpace(s)
pkgfunc-syntax

Package function (by syntax)

Counted

A package function whose module was not part of this crawl. It is named unambiguously by its import path but never type-checked here, so it is attributed by import syntax and labelled as such. Because in-corpus dependencies now resolve fully typed, this label marks a real edge of the crawl, not merely third-party code. It still counts toward the ranking.

logrus.WithField(k, v)
method-concrete

Concrete method

Counted

A method call on a receiver whose concrete type is known, like a *regexp.Regexp. Counts toward the ranking because the target is unambiguous.

re.MatchString(s)
method-embedded

Embedded method

Counted

A method promoted onto a struct by embedding another type, for example a struct that embeds sync.Mutex and is called as s.Lock(). Resolved to the embedded method.

s.Lock() // embeds sync.Mutex
method-xpkg

Cross-package method

Counted

A method whose concrete receiver type is defined in a different package, of the same module or of a dependency the crawl also covers. The export-data cache feeds those checked packages forward, so most cross-package receivers resolve concretely; a residue still lands here.

cmd.Flags() // Command from another pkg
method-interface

Interface method

Not counted

A call made through an interface, like Write on an io.Writer. Counted separately and on purpose: the receiver at runtime might be any implementation, so this is not necessarily a call into the named type's code.

w.Write(b) // w is an io.Writer
method-value

Method value

Counted

A method expression or method value, where the method is taken as a first-class value and invoked later. Attributed to the underlying method.

f := buf.WriteString; f(s)
unresolved

Unresolved

Not counted

The receiver's type comes from a module the crawl does not cover, so its target can't be pinned. With cross-module resolution draining in-corpus dependencies, this now marks a genuine boundary of the crawl rather than an in-reach type we declined to chase, reported honestly instead of guessed.

x.Do() // x typed outside the crawl

This snapshot's mix

The taxonomy isn't hypothetical. Here is how every one of 94,690 attributed calls in the 2026-07-sample snapshot actually lands. 94% resolve to a specific target function and count toward the ranking; the rest are reported, not guessed.

  • pkgfunc 65% 61,130
  • pkgfunc-syntax 18% 17,360
  • method-concrete 9.2% 8,693
  • method-embedded 0.9% 819
  • method-xpkg 0.5% 470
  • method-interface 5.0% 4,718
  • method-value <0.1% 75
  • unresolved 1.5% 1,425

Static counts are not runtime cost

The one caveat worth saying loudly. A count is how many times a function is written in source, not how many times it runs, and not how expensive it is. A function called once inside a hot loop can dominate runtime while barely registering here; a function called across thousands of one-shot setup paths can top the chart and cost nothing. The in-loop share is published precisely so you can temper the raw number, but no count is a performance verdict on its own.

Snapshots & versioning

Callcount is not a live service. Each release is a frozen, dated snapshot. This one is 2026-07-sample, covering 384,210 callsites across 42 modules. The counting logic carries its own methodology version (v0.2.0); when the resolver changes, the corpus is re-processed and the version is bumped so before/after numbers stay comparable. Cite a number by its snapshot.

Known limits

A few places where a number means less than it looks. Named up front, so you read the counts with the right caution.

loop share

Loop share is syntactic

In-loop counting looks for enclosing for/range statements in source. It doesn't see callback-driven iteration, a body passed to sort.Slice or a range-over-func runs hot but reads as loop depth zero.
one platform

One build target

Files are type-checked for linux/amd64. Code behind other build tags, GOOS-specific files, cgo, assembly stubs, isn't part of any count.
interfaces

Interface calls sit at the interface

A call through io.Writer is attributed to io.Writer.Write, not to whatever concrete type runs at runtime. It's labelled method-interface and kept out of the resolved ranking on purpose.
relative cost

The cost estimate is a coarse experiment

Where a symbol has a benchmark, its page shows an estimated relative cost, ns/op times production calls, expressed against fmt.Sprintf = 1.0, never dollars. Per-op time depends heavily on inputs and ran on one machine, and static counts are not runtime frequency, so treat it as a rough lens, not a measurement.