Go-idiom & DX review of benzene-go

A snapshot assessment of how well this port fits Go idioms and Go-developer expectations, and where it still reads as C# transliterated into Go. It is a plan of record for the "tighten up the offering" work, the counterpart of PARITY.md (which tracks capability parity with the .NET reference) for idiom parity with the Go ecosystem.

Every recommendation is weighed against the same tension:

Each finding is classified:

Verdict

Go-idiomatic in its bones, historically C#-flavored on its surface. The hard architectural calls are right for Go: generics only where they buy type safety (Handler[TReq,TRes], Result[T], GetService[T]) and deliberately dropped for the type-erased Registry; context.Context for cancellation; accept-narrow-interfaces / return-structs applied consistently; a zero-dependency root with per-SDK satellite modules; standard-library-only tests with hand-written fakes (no testify/mock). The testing harness (benzenetest) is ahead of the .NET reference — setup is identical across every transport and only the one Send* call names the cloud. What read as "ported from C#" was almost entirely ergonomic surface, addressed by the items below.

Findings

# Finding Where Class Priority Status
1 No godoc Example tests anywhere whole module A High
2 Godoc leaks .NET identifiers / essay-length all packages A High
3 Split constructor convention (options vs struct+Validate()) self-hosted consumers/workers A/C High
4 Result[T]/Status not (T, error); DI-lite Container/Scope result.go, scope.go B High (doc)
5 *Decorator sender naming (GoF/C# term) client/*, mesh, diagnostics A Medium
6 Send* shape variation undocumented benzenetest C→doc Medium
7 responseevents not dogfooded through the harness responseevents C Medium
8 gcp-cloudrun-helloworld test uses raw httptest example test A Medium
9 App[TConfig] / ApplicationBuilder Use* redundancy app.go C Medium
10 Naming polish: CreatedResultCreated, predicate cluster result.go, status.go A Low
11 "table-driven where the shape allows" claim overstated AGENTS.md B Low
12 Module path ≠ package name forces import alias go.mod C Low

Detail & decisions

1 — Godoc Example tests (A, High)

Go treats Example functions as first-class, compile-checked documentation that renders on pkg.go.dev. For a port whose headline is testability, their absence was the loudest "not written by a Go dev" signal. Decision: adopt. Add Examples for the core flow (Register, App.Run, httpbinding.Handler, one Consumer) and benzenetest's flagship ingress→egress flow, plus an ExampleSend* per transport family.

2 — De-C#-ify godoc (A, High)

Exported-symbol comments carried .NET identifier name-drops ("mirrors the .NET BenzeneResultStatus"). Idiomatic godoc opens with a terse caller-facing sentence. Decision: adopt. Trim exported-symbol comments to what the caller needs; relocate cross-language rationale to a package-level "Cross-language notes" block. Zero parity cost.

3 — Constructor convention (A on consistency; C on direction; High)

Self-hosted consumers/workers doing the identical job disagreed on construction: functional options (awssqs.NewConsumer, azureservicebus.NewWorker) vs exported struct + Validate() (kafka, rabbitmq, azureeventhub). Both are individually idiomatic; the split is the problem. For a couple of required fields plus a few optionals with good zero values, struct-fields + Validate() is the more idiomatic Go (mirrors http.Server/http.Client). Decision: converge on struct + Validate() for the self-hosted consumers/workers (a deliberate pre-1.0 API change for awssqs/azureservicebus).

4 — Two deliberate wince-points, documented not changed (B, High-doc)

Handler returns Result[T] (a wire-contract Status vocabulary shared by every port), not (T, error) — this keeps a batch consumer from crashing on one bad message and is core cross-language identity. The Container/Scope DI-lite is a named spec concept. Decision: keep both, teach both — a README section on why handlers return Result[T], and the typed-key Container/Scope pattern (with "capture singletons in the handler closure" as the preferred plain-Go path) rather than a stringly-typed key.

5 — *DecoratorWith* (A, Medium)

The wrap-a-Sender-return-a-Sender pattern is idiomatic; the XDecorator name is a GoF/C# term. Decision: adopt. Rename RetryDecoratorWithRetry, CorrelationDecoratorWithCorrelationID, TraceContextDecoratorWithTraceContext (pre-1.0). Symbol names carry no parity cost.

6 — Send* shape families (C→doc, Medium)

The Send* signatures vary (headers present/absent, positional keys, return types) — this is correct wire fidelity (a DynamoDB record has no header channel; S3 delivers metadata not contents), not sloppiness. Decision: document, don't homogenize — a "shape families" note (queue/HTTP/stream/fan-in) in benzenetest/README.md.

7 — Dogfood responseevents through the harness (C, Medium)

responseevents is a literal ingress→handler→egress feature; its unit tests are fine but the most copy-worthy adopter scenario (assert the republished event via FakeMessageSender) wasn't shown. Decision: add one harness-based feature test.

8 — Align the gcp-cloudrun-helloworld test (A, Medium)

It tested the same greet handler via raw httptest instead of the harness, teaching a second idiom. Decision: align to benzenetest (or comment the deliberate contrast).

9 — App[TConfig] / ApplicationBuilder Use* (C, Medium)

App's three-closure lifecycle buys real test parity (same composition root in main and in benzenetest.NewHost), so it stays — but docs should show the plain-main wiring as the simple default. The ApplicationBuilder Use* setters duplicate its exported fields; prefer one way. Decision: keep App, reposition in docs; simplify the builder surface.

10 — Naming polish (A, Low)

CreatedResultCreated for symmetry with Ok/Accepted/Updated; document the Result.IsSuccessful vs Status.IsFailure split so the predicate cluster reads clearly. Decision: adopt the safe renames pre-1.0.

11 — Correct the convention wording (B, Low)

Reality is a sound mix of table-driven and named per-scenario tests. Decision: soften the AGENTS.md claim to match.

12 — Module path vs package name (C, Low)

…/benzene-go imports as package benzene, forcing an alias. Decision: leave (a rename is disruptive); note the alias in the README import snippet so it reads as intentional.

Strengths to preserve (do not "fix")