GitHub CLI (gh) — Patterns#
Concurrency patterns#
Background goroutine with channel drain#
- Usage: One instance — the startup update-check.
- Example:
pkg/cmd/root/extension.go:22—updateMessageChan := make(chan *update.ReleaseInfo). A goroutine is started inghcmd.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:240—downloadAssets()createsnumWorkersgoroutines, each reading from a bufferedjobs chan ReleaseAssetand writing errors to a bufferedresults 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/errgroupis 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.Groupwith 10 workers (fetchWorkers) reads notifications from atoFetchchannel; a separate collector goroutine accumulates results from afetchedchannel;doneChsignals 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
errgroupusage 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 withsignal.NotifyContextfor SIGINT/SIGTERM handling; this context flows into Cobra’sExecuteContextC()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 funclaunches and 32select {}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 %wfor contextual wrapping. - Error types defined:
cmdutil.FlagError— wraps a flag parsing error; triggers usage displaycmdutil.SilentError— sentinel; triggers exit(1) with no messagecmdutil.CancelError— sentinel; triggers exit(2)cmdutil.PendingError— sentinel; triggers exit(8)cmdutil.NoResultsError— communicates empty result sets without crashapi.HTTPError— carriesStatusCode,Message,Errors, andScopesSuggestionfor rich error surfacingapi.GraphQLError— carries structured GraphQL error listgit.GitError— wraps git subprocess stderr + exit coderoot.AuthError/root.ExternalCommandExitError— mapped to exit codes 4 and pass-through respectivelypkg/cmd/issue/shared.PartialLoadError— signals partial batch-fetch failurepkg/cmd/auth/shared.MissingScopesError— structured OAuth scope guidance- Per-command:
NotFoundError,FilteredAllError,InvalidValueError,AmbiguousBaseRepoError
- Wrapping approach:
fmt.Errorf("...context: %w", err)throughout.errors.Isanderrors.Asused for inspection at call boundaries. Nopkg/errorsdependency — stdliberrorspackage only. - Examples:
api/client.go:170—errors.As(err, &restErr)to extractHTTPErrorfrom a wrapped chain before translating to a user message.pkg/cmd/status/status.go—errors.As(err, &httpErr)then switch onhttpErr.StatusCodeto classify 403 vs 404 vs other.cmdutil/errors.go:17—FlagErrorWrap(err)wraps any error in*FlagErrorto trigger help display.
Configuration pattern#
- Approach: Manual
cmdutil.Factorystruct — 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. TherunFparameter is the business logic; in production it is nil and the constructor falls back to its ownrun(opts)closure. In tests, the caller injects a customrunFto bypass CLI parsing entirely. - Pattern: This
runFinjection is the primary DI mechanism for testability — it is used in virtually everyNewCmd*function across all 35+ subcommands. It effectively decouples argument parsing (Cobra) from business logic (run function).
Dependency injection#
- Approach: Manual wiring via
cmdutil.Factorystruct. No DI framework (no wire/dig/fx). - Evidence:
pkg/cmd/factory/default.go—New()constructs all shared services. Each service is stored as a function closure (lazy thunk) oncmdutil.Factory. Example:HttpClient func() (*http.Client, error)— the HTTP client is not built until a command actually callsf.HttpClient().pkg/cmd/root/root.go— passesf *cmdutil.Factory(and arepoResolvingCmdFactoryvariant for commands needing GitHub API repo resolution) into eachpkg/cmd/<feature>constructor.- Test pattern:
pkg/cmd/pr/list/list_test.go— tests construct acmdutil.Factorywith mockHttpClient,IOStreams.Test(), and injectrunFto call the run function directly.
- Assessment: Explicit, traceable, and fast. No reflection or codegen. The lazy-closure design means unused dependencies (e.g.,
GitClientingh 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:
type XxxOptions struct { ... }— aggregates Factory references, flag values, and resolved dependencies.func NewCmdXxx(f *cmdutil.Factory, runF func(*XxxOptions) error) *cobra.Command— parses flags into Options, callsrunFif non-nil (test injection), otherwise callsrun(opts).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:32—type 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:19—type newCmdFunc[T any]— same pattern for argument parsing tests.
Option[T] generic type (Rust-inspired optional values)#
pkg/option/option.go— a fullOption[T any]type withSome[T],None[T],SomeIfNonZero[T comparable],Unwrap,UnwrapOr,UnwrapOrElse,UnwrapOrZero,IsSome,IsNone,Value,Expect, andMap[T, U any].- Used in
internal/ghinterface 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:67—func Partition[T any](slice []T, predicate func(T) bool) ([]T, []T)— generic slice partitioner.pkg/option/option.go—Option[T any],Map[T, U any]— see above.pkg/jsonfieldstest/jsonfieldstest.go/pkg/cmd/issue/argparsetest/argparsetest.go— generic test helper types for theNewCmdFunc[T]pattern.pkg/cmd/agent-task/shared/log.go:403—func 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 inghcmd.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.Asinspection inMain()cleanly separates error classification from error creation.
Interface-based mocking (no codegen)#
pkg/httpmock— a custom HTTP mock registry (httpmock.Registry) that interceptshttp.RoundTrippercalls for tests. Used across hundreds of command tests.pkg/search/searcher_mock.go— hand-written mock for theSearcherinterface withsync.RWMutexper method for safe concurrent test access.pkg/cmd/agent-task/capi/client_mock.go— hand-written mock with per-methodsync.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 implementExportData(fields []string) map[string]interface{}for selective JSON field export driven by--jsonflags.pkg/cmdutil/json_flags.go— registers--jsonand--jqflags;json_flags.go:22definesJSONFlagErrorfor 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 oninterface{}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()usessignal.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.