Usage: The DAP server explicitly documents its 3-goroutine model in a block comment at the top of service/dap/server.go:56–96. (1) main goroutine waits for stop signal; (2) run goroutine accepts client connections and drives the session; (3) per-request goroutines handle async operations (continue, next, step) while holding a lock to prevent concurrent requests.
Example:service/dap/server.go:56 — full goroutine architecture documented; service/dap/server.go:84–94 — per-request goroutines with changeStateMu protecting shared session state.
Assessment: Excellent — the explicit documentation of goroutine roles and ownership of synchronization is a model of clarity. The 1-connection-at-a-time constraint is justified by the debug session model and is stated explicitly.
Usage:pkg/proc/bininfo.go launches multiple goroutines in parallel to parse ELF/Mach-O/PE binary sections — symbol tables, DWARF frame sections, debug info, and goroutine struct offsets — all concurrently via sync.WaitGroup.
Example:pkg/proc/bininfo.go:1751 — wg.Add(3) then 3 concurrent goroutines for symbol names, debug frame, and gStruct offset parsing.
Assessment: Idiomatic and effective. The fan-out is scoped to a single loadBinaryInfo* call; the WaitGroup is local and not leaked. Parallelizing binary section parsing is architecturally appropriate given that DWARF parsing is CPU-bound.
Usage: The eBPF tracer goroutine in pkg/proc/internal/ebpf/helpers.go uses context.WithCancel for its background tracing goroutine. This is the only significant use of context.Context in the entire codebase (only ~10 total uses).
Example:pkg/proc/internal/ebpf/helpers.go:155 — ctx.ctx, ctx.cancel = context.WithCancel(context.Background()); helpers.go:170 — case <-ctx.ctx.Done().
Assessment: Context cancellation is used narrowly where needed (eBPF background goroutine). The rest of Delve avoids context threading through function signatures — appropriate for a debugger where most operations are synchronous stop-inspect-resume cycles.
Usage: Two places in cmd/dlv/cmds/commands.go register for OS signals via signal.Notify + channel, then dispatch to server Stop or process resumption in a goroutine.
Example:commands.go:763–764 — SIGINT forwarded to target process; commands.go:986–987 — SIGINT/SIGTERM triggers headless server stop.
Assessment: Idiomatic. Uses a buffered chan os.Signal (capacity 1) as required by signal.Notify. Each path is a dedicated go func that listens on the signal channel and calls the appropriate stop/interrupt method.
Usage: 22 select statements in production code. Used for: DAP stop signal (two selects guard against concurrent stop and concurrent stop-while-halted), eBPF buffer drain on shutdown, gdbserial non-blocking reads.
Example:service/dap/server.go:552,627 — select between incoming request and stop trigger; pkg/proc/internal/ebpf/helpers.go:169 — select between next buffer item and context done.
Assessment: Used at appropriate integration points. No busy-wait selects (all cases are channels, never default in a tight loop for production code).
Total occurrences: 115 (excluding fixtures/vendor)
sync.Mutex: Most common — guards shared state in BinaryInfo (cancelDownloadsMu, loadErrMu), Terminal (longCommandMu, quittingMutex, downloadsMu), eBPF context (m), gdbserial/test helpers.
sync.Once: 4 uses for one-time initialization: CPU feature detection (xsave_x86.go:12,38), GDB signal mask check (gdbserver.go:139), initial Go image load event (target.go:89).
sync.WaitGroup: Used in bininfo.go fan-out and eBPF goroutine lifecycle.
gosym.UnknownFileError, gosym.UnknownLineError, gosym.DecodingError — symbol table errors
api.ErrNotExecutable — sentinel for non-ELF/non-PE target binary
Wrapping approach:fmt.Errorf("%w") used in 20 places for boundary wrapping; direct custom type returns everywhere in pkg/proc. pkg/errors is not used.
errors.Is/As: Used in 10 places, primarily in tests and two production sites: cmd/dlv/cmds/commands.go:1163 checks api.ErrNotExecutable, pkg/proc/native/proc_linux.go:563 unwraps ErrBadBinaryInfo.
Notable: The type assertion style if _, isTypeConvErr := typerr.(*typeConvErr); isTypeConvErr (eval.go:625) is used for unexported error types where errors.As would require exporting. A mild inconsistency — new code uses errors.As.
Approach: Config struct passed top-down. No functional options, no builder pattern.
Example:service/debugger.Config (line 99) holds all debugger configuration fields (Backend, AttachPid, WorkingDir, Redirects, DisableASLR, etc.). This is passed to debugger.New(config), which validates and stores it. Similarly, service.Config wraps debugger.Config plus network/protocol fields.
Assessment: The config-struct approach is appropriate for a CLI tool where all configuration is resolved at startup. No dynamic reconfiguration is needed. Config structs are passed by value at construction time, which makes the call graph clear and prevents mutation surprises.
Approach: Manual wiring via Config structs — no DI framework (no wire, dig, or fx).
Evidence:cmd/dlv/cmds/commands.go:execute() builds a service.Config from flags, passes it to rpccommon.NewServer or dap.NewServer, which internally call debugger.New(config) during Run(). The entire composition happens in execute().
Assessment: Entirely appropriate for a single-binary tool with a fixed component graph. There is no need for DI frameworks when the wiring is done once at process start and never changes.
Approach: Filename-based platform isolation — zero if runtime.GOOS / if runtime.GOARCH switches in shared code. All OS/arch-specific logic lives in files named *_linux.go, *_darwin.go, *_windows.go, regs_amd64.go, regs_arm64.go, etc.
File count: 29 platform-specific files in pkg/proc/native/ and related packages.
Build tags for finer constraints: Some files combine OS and arch with build constraints, e.g.: //go:build (linux && 386) || (darwin && arm64) || (windows && arm64) || ... for hardware breakpoint stubs.
Assessment: This is one of the most disciplined examples of compile-time platform separation in any Go codebase. It makes the portability story explicit — a Linux binary contains zero Darwin code — and makes porting unambiguous: add a _newos.go file. There is no platform dead code in any given binary.
Usage: The expression evaluator in pkg/proc is implemented as a stack machine. pkg/proc/evalop/ defines Op as an interface, and all opcode types (PushConst, PushLocal, Select, TypeAssert, Jump, etc.) are concrete structs that implement it.
Example:pkg/proc/evalop/ops.go:11 — type Op interface { depthCheck() (npop, npush int) }. The interpreter loop in pkg/proc/eval.go:1105 dispatches with a massive type switch: switch op := ops[stack.opidx].(type) { case *evalop.PushCurg: ... case *evalop.PushConst: ... case *evalop.TypeAssert: ... }.
Assessment: This is a classic sum-type dispatch pattern in Go: define a sealed interface with an unexported method, then type-switch in the interpreter. The depthCheck() method serves as a compile-time stack-depth verifier (used in tests to validate opcode sequences). This pattern trades exhaustiveness checks (which Go doesn’t enforce on interfaces) for correctness-via-tests.
Prevalence: Heavy — 80 type switches and 42 direct type assertions in production source (excluding fixtures).
Dominant usage site: The eval.go interpreter loop (pkg/proc/eval.go:1105) contains ~30+ cases dispatching on Op subtypes — the core of the stack machine pattern.
Secondary sites: DWARF type hierarchy traversal in pkg/dwarf/godwarf/ (Type interface with ~15 concrete types: BasicType, PtrType, ArrayType, etc.) — type assertions used to downcast to concrete DWARF types.
Type assertions for error inspection:eval.go:625 — typerr.(*typeConvErr) for unexported error type detection.
Assessment: The high type-switch count is justified by the absence of generics in the original design and by the DWARF type system’s inherent sum-type nature. The pattern is idiomatic where exhaustive switching over a known set of types is required.
Assessment: Code generation is used precisely where manual maintenance would be error-prone (opcode tables, platform syscalls, eBPF skeletons) or where boilerplate is structurally derivable from types (Stringer, RPC registration). No gratuitous generation.
Usage: All terminal commands are registered in a single slice of anonymous structs in pkg/terminal/command.go:107+, each with aliases []string, cmdFn func(...), group, allowedPrefixes, and helpMsg string.
Example:command.go:112 — {aliases: []string{"break", "b"}, group: breakCmds, cmdFn: breakpoint, helpMsg: "Sets a breakpoint."} — the b alias for break is table-driven, not hard-coded in a switch.
Assessment: A clean data-driven dispatch approach. Adding a new command requires only one entry in the table; the findCommand function does a linear scan for alias matches (acceptable for REPL commands). The allowedPrefixes field encodes which command prefixes are valid (e.g., rev for reverse-step commands), enabling a declarative approach to command validation.
pkg/internal/lru/lru.go:20 — Cache[K comparable, V any] — a clean LRU cache parameterized over key and value types.
_scripts/rtype.go:752 — versionOkFilter[T interface{ versionOk(int) bool }] — a constrained generic filter used in the code-gen script only.
Assessment: Delve predates Go 1.18 and most of the codebase uses pre-generics idioms. The LRU cache is the only production generic, and it is a textbook example of when generics improve type safety. No over-engineering; no generic-for-generic’s-sake.
Usage: A unique internal convention: inline comments of the form // +rtype <typename> or /* +rtype <typename> */ annotate field accesses in pkg/proc/variables.go with the expected runtime type name.
Example:variables.go:905 — v = v.maybeDereference() // +rtype g documents that at this point v holds the g (goroutine) runtime struct; variables.go:910 — schedVar := v.loadFieldNamed("sched") // +rtype gobuf.
Assessment: A custom form of type documentation for code that must reflect on runtime internals rather than compile-time types. The rtype tool (_scripts/rtype.go) validates these annotations against the actual runtime type definitions. This is a maintainability trick for a fundamentally unsafe operation domain.
proc.MemoryReadWriter embeds MemoryReader and adds a write method — standard Go interface composition.
proc.ProcessInternal extends Process with mutation methods. This two-level split (public readonly + internal mutable) is the defining interface pattern of the codebase (see architecture result).
No deep embedding chains; interfaces are kept small (2–8 methods each).
DAP server pushes asynchronous events (stopped, continued, thread events) to the IDE client via Session.send(). The session holds a conn net.Conn and encodes DAP JSON messages. This is an implicit observer relationship driven by the DAP protocol structure rather than an explicit Go event bus.