GitHub CLI (gh) — Patterns#

Concurrency patterns#

Background goroutine with channel drain#

  • Usage: One instance — the startup update-check.
  • Example: pkg/cmd/root/extension.go:22updateMessageChan := make(chan *update.ReleaseInfo). A goroutine is started in ghcmd.Main() that fetches the latest release in the background; Main() drains the channel after Cobra returns to print the notification without blocking the command.
  • Assessment: Clean pattern for non-blocking background work. The channel-drain-after-main idiom is idiomatic and avoids leaking the goroutine.

Worker pool (bounded fan-out)#

  • Usage: pkg/cmd/release/download/download.go:240downloadAssets() creates numWorkers goroutines, each reading from a buffered jobs chan ReleaseAsset and writing errors to a buffered results chan error.
  • Example: for w := 1; w <= numWorkers; w++ { go func() { for a := range jobs { ... results <- downloadAsset(...) } }() } — classic bounded worker pool, job channel closed to signal completion, all results drained by a counting loop.
  • Assessment: Correct and idiomatic. Buffered channels sized to the input set prevent goroutine blocking.

errgroup fan-out for parallel API calls#

  • Usage: golang.org/x/sync/errgroup is used in 4+ packages (api/queries_repo.go, pkg/cmd/status/status.go, pkg/cmd/issue/shared/lookup.go, pkg/cmd/label/clone.go).
  • Example: status.go:278 — a *errgroup.Group with 10 workers (fetchWorkers) reads notifications from a toFetch channel; a separate collector goroutine accumulates results from a fetched channel; doneCh signals completion. Context cancellation (context.WithCancel) is used to abort all workers on first fatal error.
  • Assessment: The status command uses a sophisticated three-stage pipeline (dispatch → fetch workers → collector) with proper context cancellation. The errgroup usage provides structured error propagation — the first non-nil error returned from any worker is surfaced to the caller.

Context propagation (554 usages)#

  • Usage: Context is threaded through nearly all outbound operations (HTTP, git subprocess, API calls). Count: 554 references to context.Context.
  • Example: ghcmd.Main() creates a root context with signal.NotifyContext for SIGINT/SIGTERM handling; this context flows into Cobra’s ExecuteContextC() and propagates to all command handlers.
  • Assessment: Pervasive and idiomatic. Context is used both for cancellation (user interrupt) and value propagation (rare).

78 goroutines, 32 select statements#

  • The 78 go func launches and 32 select {} statements are consistent with a project that uses concurrency targeted at specific parallel I/O scenarios — not as a general computation model. Most goroutines are confined to command implementations rather than long-running services.

Error handling#

  • Style: Mixed — sentinel errors for well-known conditions, rich struct types for caller-inspectable errors, fmt.Errorf %w for contextual wrapping.
  • Error types defined:
    • cmdutil.FlagError — wraps a flag parsing error; triggers usage display
    • cmdutil.SilentError — sentinel; triggers exit(1) with no message
    • cmdutil.CancelError — sentinel; triggers exit(2)
    • cmdutil.PendingError — sentinel; triggers exit(8)
    • cmdutil.NoResultsError — communicates empty result sets without crash
    • api.HTTPError — carries StatusCode, Message, Errors, and ScopesSuggestion for rich error surfacing
    • api.GraphQLError — carries structured GraphQL error list
    • git.GitError — wraps git subprocess stderr + exit code
    • root.AuthError / root.ExternalCommandExitError — mapped to exit codes 4 and pass-through respectively
    • pkg/cmd/issue/shared.PartialLoadError — signals partial batch-fetch failure
    • pkg/cmd/auth/shared.MissingScopesError — structured OAuth scope guidance
    • Per-command: NotFoundError, FilteredAllError, InvalidValueError, AmbiguousBaseRepoError
  • Wrapping approach: fmt.Errorf("...context: %w", err) throughout. errors.Is and errors.As used for inspection at call boundaries. No pkg/errors dependency — stdlib errors package only.
  • Examples:
    • api/client.go:170errors.As(err, &restErr) to extract HTTPError from a wrapped chain before translating to a user message.
    • pkg/cmd/status/status.goerrors.As(err, &httpErr) then switch on httpErr.StatusCode to classify 403 vs 404 vs other.
    • cmdutil/errors.go:17FlagErrorWrap(err) wraps any error in *FlagError to trigger help display.

Configuration pattern#

  • Approach: Manual cmdutil.Factory struct — not functional options for the factory itself, but factory fields are lazy function closures (thunks).
  • Example: Every command receives func(f *cmdutil.Factory, runF func(*XxxOptions) error) *cobra.Command. The runF parameter is the business logic; in production it is nil and the constructor falls back to its own run(opts) closure. In tests, the caller injects a custom runF to bypass CLI parsing entirely.
  • Pattern: This runF injection is the primary DI mechanism for testability — it is used in virtually every NewCmd* function across all 35+ subcommands. It effectively decouples argument parsing (Cobra) from business logic (run function).

Dependency injection#

  • Approach: Manual wiring via cmdutil.Factory struct. No DI framework (no wire/dig/fx).
  • Evidence:
    • pkg/cmd/factory/default.goNew() constructs all shared services. Each service is stored as a function closure (lazy thunk) on cmdutil.Factory. Example: HttpClient func() (*http.Client, error) — the HTTP client is not built until a command actually calls f.HttpClient().
    • pkg/cmd/root/root.go — passes f *cmdutil.Factory (and a repoResolvingCmdFactory variant for commands needing GitHub API repo resolution) into each pkg/cmd/<feature> constructor.
    • Test pattern: pkg/cmd/pr/list/list_test.go — tests construct a cmdutil.Factory with mock HttpClient, IOStreams.Test(), and inject runF to call the run function directly.
  • Assessment: Explicit, traceable, and fast. No reflection or codegen. The lazy-closure design means unused dependencies (e.g., GitClient in gh auth login) have zero startup cost. The sole downside is boilerplate: every new service requires a new factory field.

Other notable patterns#

RunOptions / Options struct + runF injection (universal command pattern)#

Every subcommand uses the same three-layer structure:

  1. type XxxOptions struct { ... } — aggregates Factory references, flag values, and resolved dependencies.
  2. func NewCmdXxx(f *cmdutil.Factory, runF func(*XxxOptions) error) *cobra.Command — parses flags into Options, calls runF if non-nil (test injection), otherwise calls run(opts).
  3. func run(opts *XxxOptions) error — pure business logic; can be unit-tested without Cobra.

This pattern is so consistent that a generic test helper was extracted:

  • pkg/jsonfieldstest/jsonfieldstest.go:32type NewCmdFunc[T any] func(f *cmdutil.Factory, runF func(*T) error) *cobra.Command — a generic type alias for the constructor signature.
  • pkg/cmd/issue/argparsetest/argparsetest.go:19type newCmdFunc[T any] — same pattern for argument parsing tests.

Option[T] generic type (Rust-inspired optional values)#

  • pkg/option/option.go — a full Option[T any] type with Some[T], None[T], SomeIfNonZero[T comparable], Unwrap, UnwrapOr, UnwrapOrElse, UnwrapOrZero, IsSome, IsNone, Value, Expect, and Map[T, U any].
  • Used in internal/gh interface definitions for optional config values. Inlined from a third-party library with attribution; the team explicitly chose to own it as a domain type.
  • Assessment: Unusual for Go — the team deliberately adopted a Rust-style optional value type to eliminate nil-pointer ambiguity in config lookups. Requires discipline but improves API clarity for callers.

Generics (Go 1.18+)#

  • pkg/cmdutil/args.go:67func Partition[T any](slice []T, predicate func(T) bool) ([]T, []T) — generic slice partitioner.
  • pkg/option/option.goOption[T any], Map[T, U any] — see above.
  • pkg/jsonfieldstest/jsonfieldstest.go / pkg/cmd/issue/argparsetest/argparsetest.go — generic test helper types for the NewCmdFunc[T] pattern.
  • pkg/cmd/agent-task/shared/log.go:403func unmarshal[T any](raw string) *T — generic JSON unmarshaling helper.
  • Assessment: Targeted, disciplined generics usage — not over-applied. Each use has a clear benefit (eliminating type assertions or code duplication).

Sentinel error taxonomy with exit code mapping#

  • Four sentinel errors (SilentError, CancelError, PendingError, SilentError) plus two typed errors (AuthError, ExternalCommandExitError) are mapped in ghcmd.Main() to specific exit codes (1, 2, 4, 8).
  • This creates a machine-readable exit code protocol for shell scripts and CI systems without parsing stderr.
  • Assessment: Well-designed. The exit code taxonomy is documented and intentional. errors.Is / errors.As inspection in Main() cleanly separates error classification from error creation.

Interface-based mocking (no codegen)#

  • pkg/httpmock — a custom HTTP mock registry (httpmock.Registry) that intercepts http.RoundTripper calls for tests. Used across hundreds of command tests.
  • pkg/search/searcher_mock.go — hand-written mock for the Searcher interface with sync.RWMutex per method for safe concurrent test access.
  • pkg/cmd/agent-task/capi/client_mock.go — hand-written mock with per-method sync.RWMutex.
  • Assessment: The project avoids gomock/mockery code generation in favor of hand-written mocks and httpmock. This keeps tests readable and avoids regeneration overhead, at the cost of some manual maintenance.

ExportData method for JSON serialization#

  • api/export_repo.go, api/export_pr.go — domain structs implement ExportData(fields []string) map[string]interface{} for selective JSON field export driven by --json flags.
  • pkg/cmdutil/json_flags.go — registers --json and --jq flags; json_flags.go:22 defines JSONFlagError for invalid field names.
  • Assessment: A hand-rolled field-projection system. Rather than serializing the full API response, commands project only the requested fields. The approach is simple and introspectable but does not leverage struct tags — field names are hardcoded strings in ExportData.

Table-driven command tests with runF injection#

Tests call NewCmdXxx(f, func(opts *XxxOptions) error { ... }) to capture the Options struct and assert on it without running real I/O. This is the canonical test pattern and appears in every command package.

Type switches for API response parsing#

  • api/queries_pr.go:732, pkg/cmd/api/http.go:30,121, pkg/cmd/api/pagination.go:49,65 — type switches on interface{} values returned from JSON decoding of GraphQL responses.
  • Assessment: Necessary because the GitHub GraphQL API returns polymorphic union types that cannot be statically decoded. Type switches are scoped to the API layer and do not leak into command logic.

Graceful shutdown via signal context#

  • ghcmd.Main() uses signal.NotifyContext(context.Background(), os.Interrupt) to propagate SIGINT as context cancellation. All HTTP requests and git subprocess calls respect this context.
  • Assessment: Clean OS signal integration. Context propagation ensures that in-flight API calls are cancelled on Ctrl-C rather than left dangling.