The Target model & the factory split¶
grpcclient answers one question well: what is the least a caller must state to dial a
gRPC service securely? Its answer is a plain Target — host, port, and TLS material —
and a Dial that turns that into a *grpc.ClientConn, drawing a deliberate line
between the dial factory (this module) and the interceptors (go/transit).
Why Target is not a server-settings type¶
The dial factory was lifted from go-tool-base's dialLocal, which took a server-side
ServerSettings. That coupled every client to a server configuration type. Target
breaks that:
type Target struct {
Host string // empty ⇒ loopback (localhost)
Port int
TLS tls.Pair // go/tls pair
}
A caller states only what dialing needs. go-tool-base keeps a thin DialLocal adapter
that maps its ServerSettings onto a Target, so a server consumer is unaffected —
but a client-only consumer never imports server settings, and the depfootprint guard
proves the server stack stays out of the graph.
How Dial selects credentials¶
Dial never asks the caller for grpc.WithTransportCredentials; it derives the
credentials from Target.TLS:
TLS.Enabled == false→ insecure loopback credentials (insecure.NewCredentials()). This is the local-server default.TLS.Enabled == true,TLS.Certset → a TLS credential over go/tls's hardened client config (TLS 1.2 floor, curated cipher suites), trusting the CA bundle inCert.TLS.Enabled == true,TLS.Certempty → the same hardened config over the system roots.
The derived credentials are placed first in the dial-option list, before the caller's options, so a caller's option can never silently downgrade the target's transport security.
Why the factory is separate from the interceptors¶
Circuit breaking and OpenTelemetry instrumentation are not in this module — they
live in go/transit and are passed to Dial as
grpc.DialOptions. That split is intentional:
- An interceptor is identical everywhere; a dial target is a policy. Fail-fast circuit breaking behaves the same for every caller, so it belongs in a shared, transport-neutral module. Which host, port and CA to trust is a choice a service makes, so it belongs in a dial factory the service owns.
- The interceptors are reused by the server too.
go/transitis consumed by both clients and servers; keeping it separate means one tested implementation of each concern. - Light graph. Because the factory only pulls the gRPC SDK,
go/tlsandgo/transit, a client-only consumer never links the server stack (controls, authn, gateway). Adepfootprint_test.goguard enforces this.
Laziness¶
Dial uses grpc.NewClient, so the returned connection is lazy: it performs name
resolution and connects on the first RPC, not at construction. Dial returning without
error therefore means the target and options were valid, not that the server is
reachable — surface connection failures where you make the call.
Relationship to go/transit¶
grpcclient.Dial → *grpc.ClientConn
│ credentials from Target.TLS (via go/tls), endpoint from Host/Port
└─ applies → go/transit client interceptors (circuit breaker, OTel) as dial options
The factory owns the connection and its credentials; transit owns everything that wraps a call. The transit middleware model covers the interceptor ordering.