API reference¶
The whole public surface of gitlab.com/phpboyscout/go/grpcclient is one function and
one struct. This page states what each field means, what it defaults to and what happens
when it is wrong.
| Symbol | Kind | Summary |
|---|---|---|
Dial |
func(Target, ...grpc.DialOption) (*grpc.ClientConn, error) |
Builds a lazy *grpc.ClientConn for the target |
Target |
struct |
Host, port and TLS material for one gRPC server |
There is nothing else exported — no constructor, no options type, no interface, no package-level state. Full godoc is on pkg.go.dev.
Dial¶
Dial does four things, in this order:
- Derives transport credentials from
t.TLS(see Target fields). - Substitutes
localhostfor an emptyt.Host. - Formats the endpoint as
fmt.Sprintf("%s:%d", host, t.Port). - Calls
grpc.NewClient(endpoint, opts...)with the derived credentials placed first in the option list and the caller's options after them.
On success it returns a stock *grpc.ClientConn — nothing about it is specific to this
module, so every method, every grpc.CallOption and every teardown convention is the
gRPC SDK's. Close it with conn.Close() when you are done.
On failure it returns nil and a wrapped error. See
Errors returned by Dial.
Dial does not connect to anything¶
Dial calls grpc.NewClient, which is lazy. No DNS lookup, no TCP connect and no TLS
handshake happen inside Dial; they happen on the first RPC, or when you call
conn.Connect() explicitly.
A Dial that returns without error therefore means the target string parsed and the
options validated. It says nothing about whether the server exists, is listening, or
presents a certificate you trust. Connection failures arrive at the call site, as a
codes.Unavailable status. Errors returned by Dial lists which failures land
where.
Dial is not a connection pool¶
Each Dial returns one independent *grpc.ClientConn. The module keeps no cache and no
registry, so calling Dial twice with the same Target gives you two connections and two
sets of subchannels. A *grpc.ClientConn is already safe for concurrent use and already
multiplexes calls over HTTP/2 — dial once per service and share the connection.
Target fields¶
| Field | Type | Zero value | Meaning |
|---|---|---|---|
Host |
string |
"" → localhost |
Dial host, used verbatim as the left side of host:port |
Port |
int |
0 |
Dial port, formatted with %d, never validated |
TLS |
gtls.Pair |
disabled | Transport security; only Enabled and Cert are read |
Target.Host — what an empty host means¶
An empty Host becomes localhost. That is the module's only default, and it exists
because dialling a co-located server on loopback is the common case.
Host is a hostname or IP literal, not a gRPC target URI. It is pasted into
host:port with fmt.Sprintf, so a scheme or a resolver prefix is not stripped and not
rejected:
Host you set |
Endpoint produced | Result |
|---|---|---|
"" |
localhost:8080 |
works |
svc.internal |
svc.internal:8080 |
works |
10.0.0.5 |
10.0.0.5:8080 |
works |
[::1] |
[::1]:8080 |
works — IPv6 literals must be bracketed |
::1 |
::1:8080 |
Dial succeeds; the RPC times out — see below |
dns:///svc |
dns:///svc:8080 |
Dial succeeds; the RPC resolves the wrong name |
https://svc |
https://svc:8080 |
Dial succeeds; the RPC fails |
Every one of those bad values passes Dial silently. If you accept a host from
configuration, validate it before you build the Target.
An unbracketed IPv6 host fails in one of two ways, depending on the port¶
Host: "::1" with Port: 8080 produces the endpoint ::1:8080, and that string is
itself a valid IPv6 literal — 8080 is a legal four-hex-digit group. The resolver reads
the whole thing as an address, applies the default port 443, and your intended port
is silently discarded. The RPC then fails with a timeout rather than an address error:
rpc error: code = DeadlineExceeded desc = context deadline exceeded while waiting for connections to become ready
A port that cannot be a hextet — anything above ffff, or with a non-hex digit — cannot
be absorbed that way, so the address is rejected outright and you get the clearer error
quoted in Errors:
rpc error: code = Unavailable desc = invalid target address ::1:37225, error info: address ::1:37225:443: too many colons in address
The second form is the one you want, because it names the problem. Which one you get is an accident of the port number, so do not rely on either — bracket the literal.
Target.Port — no validation, no default¶
Port is formatted with %d and never checked. 0, -1 and 99999 all produce a
*grpc.ClientConn from Dial without error; the failure appears at the first RPC. There
is no default port — leaving Port unset gives you localhost:0, which is not what you
meant.
Target.TLS — which Pair fields are read¶
gtls.Pair is a shared type used for both server and client TLS across the phpboyscout
toolkit, so it carries more fields than a client needs. Dial reads exactly two.
gtls.Pair field |
Read by Dial? |
What it does here |
|---|---|---|
Enabled |
yes | false → insecure credentials; true → TLS via go/tls's ClientConfig |
Cert |
yes, when Enabled |
Path to a PEM file of CA certificates to trust. Empty → system roots |
Key |
no | Silently ignored — the client presents no certificate |
ClientCAs |
no | Server-side only; silently ignored |
ClientAuth |
no | Server-side only; silently ignored, including an invalid value |
Two of those deserve emphasis because they mislead.
Cert is the CA you trust, not the certificate you present. On a server, Pair.Cert
and Pair.Key are the identity that server offers. On a client, Dial passes Cert
straight to gtls.ClientConfig, which loads it into tls.Config.RootCAs. Handing Dial
a server's own certificate therefore "works" for a self-signed server — you end up
trusting exactly that leaf — but it is not what the field name suggests.
Reusing one Pair for a server and a client is a trap. A Pair populated for mutual
TLS, with Key, ClientCAs and ClientAuth all set, produces a one-way TLS client here
and no warning. See What grpcclient does not do.
How Target.TLS becomes transport credentials¶
TLS.Enabled |
TLS.Cert |
Credentials Dial uses |
|---|---|---|
false |
anything (ignored) | insecure.NewCredentials() — plaintext HTTP/2 |
true |
"" |
credentials.NewTLS(gtls.ClientConfig()) — hardened config, system roots |
true |
path to PEM | credentials.NewTLS(gtls.ClientConfig(cert)) — hardened config, that file as the only trust anchor |
When Cert is set it replaces the system roots rather than adding to them:
gtls.ClientConfig builds a fresh x509.CertPool from the named files and assigns it to
RootCAs. A client configured with a private CA will not trust a public one.
What the hardened TLS config actually sets¶
Enabled: true selects go/tls's DefaultConfig, which is fixed and not configurable
through Target:
MinVersion: TLS 1.2.CipherSuites:ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,ECDHE_RSA_WITH_AES_256_GCM_SHA384,ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,ECDHE_RSA_WITH_AES_128_GCM_SHA256,ECDHE_ECDSA_WITH_CHACHA20_POLY1305,ECDHE_RSA_WITH_CHACHA20_POLY1305.CurvePreferences: X25519, then P-256.
Note that Go applies CipherSuites to TLS 1.2 only; a TLS 1.3 handshake uses Go's own
TLS 1.3 suites regardless.
Which hostname the certificate is checked against¶
Dial does not set ServerName, so gRPC verifies the server certificate against the
authority it derives from the endpoint — that is, Target.Host, or localhost when
Host is empty. There is no way to dial one address and verify a different name through
Target; if you need that, pass grpc.WithAuthority yourself as a dial option.