One file, copier.go, holds all five of my top structural-risk findings in jinzhu/copier. The top function, simply named copier, carries an activity-weighted risk score of 21.75 — the highest in the repo — driven by a cyclomatic complexity of 191, a nesting depth of 8, and a fan-out of 115, and it was modified just 1 day ago with 1 touch in the last 30 days. That combination is worth watching closely, since it pairs high structural complexity with recent activity. The repo has 20 total functions analyzed, 7 of which land in the critical band, and the concentration of risk in one file is the story here: anyone editing copier.go right now is working in the most structurally loaded part of the codebase.
The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).
Top 5 Hotspots
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
copier | copier.go | 21.7 | 191 | 8 | 115 |
set | copier.go | 17.7 | 63 | 5 | 35 |
fieldByName | copier.go | 12.6 | 17 | 2 | 9 |
getFlags | copier.go | 12.0 | 27 | 4 | 5 |
deepFields | copier.go | 11.0 | 13 | 4 | 11 |
20 functions analyzed
The quadrant split tells its own story: 2 functions are actively changing while structurally complex (the fire quadrant), 9 are complex but dormant (debt), and the remaining 9 are low-complexity and inactive. There is no ‘watch’ quadrant activity at all — nothing here is simple-but-busy. The risk in this repo is concentrated, not spread thin.
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Exit Heavy×3Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.Deeply Nested×2Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.God Function×2God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Long Function×2Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Stale Complex×2Stale Complex
High structural complexity but untouched for a long time — structural debt that will bite whoever opens it next.Neighbor Risk×1Neighbor Risk
Co-located with other high-risk functions in the same file, compounding the blast radius of any change to that file.
copier — copier.go
This is the core copy routine — the entry point that reflects on both source and destination values and decides how to move data between them. A cyclomatic complexity of 191 is extreme by any standard; combined with a nesting depth of 8 and a fan-out of 115, this single function handles type detection, map handling, slice handling, interface unwrapping, and error routing all in one body. The source excerpt shows why: nested kind checks on reflect.Kind, inline map-key conversion with its own error path, and a deferred closure for interface reassignment, all before the function is a third of the way through its logic. The ‘exit_heavy’ and ‘god_function’ patterns both apply — I count at least four distinct early returns in the excerpt alone, each representing a branch a test suite needs to cover independently.
What makes this urgent rather than just messy is the activity signal: modified 1 day ago, with 1 touch in the last 30 days, for an activity-weighted risk score of 21.75, the highest in the repo. The file’s history also shows a bug-fix rate of about 34% across 117 total commits on copier.go, plus meaningful reviewer attention on pull requests touching this file — which lines up with the structural load. My recommendation: before the next change lands, split the type-dispatch logic (slice, map, struct, interface) into separate named functions. Even extracting the map-copy block into its own function would cut the top-level cyclomatic complexity meaningfully without touching behavior.
set — copier.go
set handles the actual value assignment between reflected fields — pointer dereferencing, sql.Scanner handling, deep-copy semantics, and type conversion fallbacks. At cyclomatic complexity 63 with nesting depth 5 and fan-out 35, it’s the second-most complex function in the repo, and the excerpt shows why: nested pointer-nil checks, a driverValuer branch, a deep-copy kind switch, and a sql.Scanner conversion path all stacked before the function even reaches its convertible-type fallback.
Unlike copier, this one sits in the debt quadrant — it hasn’t been changed in 604 days and has had no touches in the last 30 days. That’s a long time for a function this structurally loaded to sit untouched, and it means the next person who has to modify it (say, to support a new conversion type) inherits the full weight of that complexity with no recent context to lean on. The ‘stale_complex’ pattern flag is accurate here: this is exactly the kind of function that turns a small feature request into a multi-day investigation. I’d treat this as a pre-emptive refactor candidate — decompose the pointer-handling branch and the Scanner-handling branch into separate helper functions before the next feature touches it, rather than during.
fieldByName — copier.go
This one handles case-insensitive struct field lookup, with logic to prioritize exported fields when multiple case-insensitive matches exist. Cyclomatic complexity of 17 is moderate, and the nesting depth of 2 is shallow — the complexity here comes from the number of exit points, not from nesting. The excerpt shows four separate return statements handling: exact match, no-candidates zero-value, exported-candidate preference, and first-candidate fallback. That’s the ‘exit_heavy’ pattern in practice — each of those four paths needs its own test case to be confident the exported-field-priority behavior actually holds.
It sits in the fire quadrant alongside copier — 1 touch in the last 30 days and modified 1 day ago, for an activity-weighted risk score of 12.59. Given that recent activity, I’d prioritize adding table-driven tests around the case-insensitive matching logic now, while the change is fresh in someone’s head, rather than after it settles.
getFlags — copier.go
getFlags builds the tag-mapping structure that drives field-level copy behavior — parsing copier struct tags on both source and destination types and applying default flags like must and no-panic. Cyclomatic complexity of 27 with nesting depth 4 reflects two parallel loops (destination fields, then source fields), each with its own tag-parsing branch and error return. The ‘complex_branching’ pattern fits: there are multiple independent conditions (tag presence, name parsing success, must/no-panic flags) that combine multiplicatively.
This is a debt-quadrant function — 0 touches in the last 30 days and hasn’t been changed in 218 days. It’s not urgent today, but it is high blast-radius: any change to how copier tags are parsed touches this function, and its complexity means a change here is unlikely to be a one-line fix. Worth pulling the two field-iteration loops into a shared helper that takes a direction flag, since the destination and source loops in the excerpt already share most of their structure.
deepFields — copier.go
deepFields recursively walks anonymous (embedded) struct fields to build a flattened field list, with a package-level cache (deepFieldsMap) guarded by a read-write lock. Cyclomatic complexity of 13 and nesting depth 4 are the lowest of the top five, but this function carries a different kind of risk: it’s recursive, cache-mutating, and lock-guarded, which in Go means correctness depends on goroutine interaction, not just branch counting. The structural metrics alone don’t fully capture the risk of a shared mutable cache accessed under RLock/Lock.
It’s also the most dormant function in the top five — 0 touches in the last 30 days and hasn’t been changed in 1387 days, well over three years. That kind of longevity with zero recent touches suggests the caching logic is stable, but if concurrent-access patterns ever change (for example, if callers start invoking copier from multiple goroutines against overlapping types), this is the function I’d revisit first, specifically to verify the lock discipline still holds under the current call patterns from copier and set.
Across all five, the pattern concentration is worth naming directly: 4 instances of complex branching, 3 exit-heavy functions, 2 god-function flags, 2 long-function flags, and 2 stale-complex flags, all inside one file. That’s not five isolated problems — it’s one file where structural risk compounds because these functions call each other (copier calls set, getFlags calls deepFields), so a refactor in one has to be checked against the others.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
complex_branching | 4 |
exit_heavy | 3 |
deeply_nested | 2 |
god_function | 2 |
long_function | 2 |
stale_complex | 2 |
neighbor_risk | 1 |
These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.
See more analyses with these patterns: complex_branching, deeply_nested, exit_heavy, god_function, long_function, neighbor_risk, stale_complex.
Reproduce This Analysis
git clone https://github.com/jinzhu/copier
cd copier
git checkout c6b47b092d9840406d0abc347e68a28a7b812643
hotspots analyze . --mode snapshot --explain-patterns --force
To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.
I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →