The Go application SDK is module go.varianz.io/sdk, imported as go.varianz.io/sdk/varianz (install). The test-side package varianztest is covered in Testing → Go.
vz.Init() and defer vz.Close() in main() are required — without Init(), VPoints don’t connect to the coordinator. When the SDK is disabled, varianz.VPoint returns your original function unchanged and every registration call succeeds as a no-op.
The endpoint
WithEndpoint(...) is a default, not an instruction: VARIANZ_COORDINATOR_ENDPOINT outranks it, so a deployment can repoint a build it cannot rebuild.
In Go the runtime and the client resolve the endpoint by different paths. The stage-subscription
runtime honours VARIANZ_COORDINATOR_ENDPOINT; the coordinator client dials the WithEndpoint(...)
value verbatim. With a stale variable — inherited from a base image or a sibling service — the
runtime fails to connect, degrades to coordinator-less, and vz.Init() still returns nil. Every
VPoint runs its original body, no stage applies, and nothing errors:Assert on the resolved value at startup:ResolvedEndpoint() reports what the SDK resolved from the environment and varianz.toml. ok is
false when no source supplies one — distinct from an empty endpoint, which is what selects
local-only mode.
Defining VPoints
varianz.VPoint is a typed higher-order wrapper: it registers the function and returns a callable with the same signature.
Rules:
- The signature is
func(context.Context, Req) (Resp, error) — exactly one input parameter besides the context. Group multiple arguments into a struct.
Req/Resp can be structs, primitives, or proto messages (pointer types included).
- Use struct type names directly in CEL (
PricingResponse, not pb.PricingResponse). Nested proto messages construct fine in CEL literals — no flattening needed.
A CEL stage cannot reconstruct a []*T field — which is every protobuf
repeated <message>. Resp{items: invoke().items} returns the right number of elements with
every one empty (nil pointers in-process, empty messages over gRPC), err == nil, and no
diagnostic. []T, a bare *T return and repeated string are all unaffected.Separately, a struct with exactly one exported field is flattened: invoke() yields that
field’s scalar rather than the struct, so Resp{f: invoke().f} fails the whole call with
Value is not a collection or map. Use invoke() on its own, or give the struct a second field.
Where to declare
Three equivalent patterns — there is no rule that VPoints must be package-level:
-
Package-level
var (above) — simplest for standalone functions.
-
Service struct +
vz.Register — most idiomatic for dependency-injected services:
Every exported method becomes a VPoint, named PascalCase → kebab-case with the varianz tag as prefix (CalculatePrice → pricing/calculate-price; acronyms group: HTTPSPort → https-port).
-
Wrap a bound method —
varianz.VPoint(vz, name, svc.CalculatePrice) when you want a typed package-level callable that still captures instance state.
With varianz-gen you get typed proxies instead of proxy.Call’s any returns.
Sessions
The session rides on context.Context:
Both varianz.VPoint callables and proxy.Call extract it automatically. Interceptor and middleware snippets: Propagate sessions. Never start a goroutine without passing the parent context.
Field names
Schema field names resolve in priority order:
varianz:"name" tag — explicit override
protobuf tag — proto canonical name
json:"name" tag — covers oapi-codegen, sqlc, gqlgen, and similar codegen output
toSnakeCase(GoName) — untagged fallback (BasePrice → base_price)
The opaque option does not do what its name says. An opaque field is not hidden from CEL
and does not pass through: an expression can both read and write it, and it is treated as
required — any struct literal that omits it blanks the field, reported only as an advisory
diagnostic. Prefer varianz:"-", which excludes the field as documented.
Registration rejects func and unsafe.Pointer fields. Two limits worth knowing: a struct with no exported fields is accepted, not rejected — it registers, appears in vz.ListVPoints(), exposes nothing to CEL, and is silently blanked by any struct-literal override; and a chan is caught only as a field, not as a parameter. Proto-internal fields (state, sizeCache, unknownFields) are skipped automatically.
Context functions
Call vz.Discover() after every VPoint/Function call and before vz.Init() — it is what attaches the registered functions to each VPoint’s descriptor:
Available in CEL as ctx.applyTax(...).
Without Discover() — or with it called after Init() — the context function silently does not
exist. The stage fails to compile with Context function 'applyTax' not found on call context,
is never attached, and the VPoint returns its real value. The sync barrier reports no error. The
SDK does log the failure, but to stderr, which go test discards for a passing package: run
go test -v.
Troubleshooting
go test -tags=integration fails with vet errors in upstream code — the integration tag pulls files into the build that surface pre-existing go vet warnings (commonly status.Errorf(codes.Internal, err.Error())). Fix upstream with an explicit "%s", or run with -vet=off.
- Vendored builds drop the native library —
go mod vendor does not copy varianz/libs/, because that directory holds no Go source. vendor/go.varianz.io/sdk/ and the vendor/modules.txt entry are both present and the build still fails to link. See Installation → Go for the workaround.
- Link errors or missing symbols in Docker — you built with
CGO_ENABLED=0 or a static/scratch runtime image. See the Docker pattern.