or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

admin.mdadvanced.mdclient-server.mdcredentials-security.mderrors-status.mdhealth.mdindex.mdinterceptors.mdload-balancing.mdmetadata-context.mdname-resolution.mdobservability.mdreflection.mdstreaming.mdtesting.mdxds.md
tile.json

name-resolution.mddocs/

Name Resolution

This document covers name resolution in gRPC-Go, including built-in resolvers, custom resolver implementation, target syntax, and service config integration.

Overview

The resolver package defines APIs for name resolution in gRPC. Resolvers translate target names into addresses and service configurations.

import "google.golang.org/grpc/resolver"

All APIs in the resolver package are experimental.

Target Syntax

gRPC target names follow the syntax defined in gRPC naming spec:

scheme://authority/endpoint

Examples:

  • dns:///example.com:8080 - DNS resolver
  • dns://8.8.8.8/example.com:8080 - DNS with custom DNS server
  • unix:///tmp/grpc.sock - Unix domain socket
  • unix-abstract:my-service - Unix abstract namespace socket
  • xds:///myservice - xDS-based resolution
  • example.com:8080 - Uses default scheme (typically "dns")
// Target represents a parsed dial target
type Target struct {
    // URL contains the parsed dial target with optional default scheme
    URL url.URL
}

// Endpoint retrieves endpoint without leading "/" from URL.Path or URL.Opaque
func (t Target) Endpoint() string

// String returns canonical string representation
func (t Target) String() string

Default Scheme

// SetDefaultScheme sets the default scheme used by grpc.NewClient
// Must be called during initialization (in init()) - not thread-safe
// Initial default is "dns"
func SetDefaultScheme(scheme string)

// GetDefaultScheme gets the current default scheme
func GetDefaultScheme() string

Example:

import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/resolver"
)

func init() {
    // Change default scheme to passthrough
    resolver.SetDefaultScheme("passthrough")
}

// Now "localhost:8080" uses passthrough instead of dns
conn, err := grpc.NewClient("localhost:8080", opts...)

Built-in Resolvers

DNS Resolver

The DNS resolver is the default resolver. It performs DNS lookups to resolve hostnames.

import (
    "time"
    "google.golang.org/grpc"
    "google.golang.org/grpc/resolver/dns"
)

// Configure DNS resolver parameters
func SetMinResolutionInterval(d time.Duration)
func SetResolvingTimeout(timeout time.Duration)

// SetMinResolutionInterval prevents excessive re-resolution
// Must be called at application startup
// Example: prevent DNS lookups more frequent than every 5 seconds
dns.SetMinResolutionInterval(5 * time.Second)

// SetResolvingTimeout sets maximum duration for DNS resolution
// Default is 30 seconds
dns.SetResolvingTimeout(10 * time.Second)

DNS Resolver Examples:

import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

// Basic DNS resolution
creds, _ := credentials.NewClientTLSFromFile("ca.pem", "")
conn, err := grpc.NewClient("dns:///example.com:443",
    grpc.WithTransportCredentials(creds))

// DNS with service name (SRV records)
conn, err := grpc.NewClient("dns:///myservice.example.com",
    grpc.WithTransportCredentials(creds))

// DNS with custom nameserver
conn, err := grpc.NewClient("dns://8.8.8.8/example.com:443",
    grpc.WithTransportCredentials(creds))

// Multiple A/AAAA records -> used with load balancer
serviceConfig := `{"loadBalancingPolicy":"round_robin"}`
conn, err := grpc.NewClient("dns:///example.com:443",
    grpc.WithDefaultServiceConfig(serviceConfig),
    grpc.WithTransportCredentials(creds))

Passthrough Resolver

The passthrough resolver passes the target string directly to the dialer without modification.

// Passthrough - no resolution
conn, err := grpc.NewClient("passthrough:///localhost:8080",
    grpc.WithTransportCredentials(insecure.NewCredentials()))

// Useful for direct IP connections
conn, err := grpc.NewClient("passthrough:///10.0.0.1:8080",
    grpc.WithTransportCredentials(creds))

Manual Resolver

The manual resolver allows programmatic control of resolved addresses, useful for testing:

import (
    "google.golang.org/grpc/resolver"
    "google.golang.org/grpc/resolver/manual"
)

// Create manual resolver
r := manual.NewBuilderWithScheme("mytest")
resolver.Register(r)

// Use in dial
conn, err := grpc.NewClient("mytest:///unused",
    grpc.WithResolvers(r),
    grpc.WithTransportCredentials(insecure.NewCredentials()))

// Manually update addresses
r.UpdateState(resolver.State{
    Addresses: []resolver.Address{
        {Addr: "localhost:8080"},
        {Addr: "localhost:8081"},
    },
})

// Update again later
r.UpdateState(resolver.State{
    Addresses: []resolver.Address{
        {Addr: "localhost:8080"},
        {Addr: "localhost:8082"},
    },
})

Manual Resolver Type

type Resolver struct {
    // BuildCallback is called when Build method is called
    BuildCallback func(resolver.Target, resolver.ClientConn, resolver.BuildOptions)

    // UpdateStateCallback is called when UpdateState is called
    UpdateStateCallback func(err error)

    // ResolveNowCallback is called when ResolveNow is called
    ResolveNowCallback func(resolver.ResolveNowOptions)

    // CloseCallback is called when Close is called
    CloseCallback func()
}

// NewBuilderWithScheme creates a new manual resolver builder
// Each instance should only be used with a single ClientConn
func NewBuilderWithScheme(scheme string) *Resolver

// Build returns itself (implements Builder interface)
func (r *Resolver) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error)

// Close is a no-op for Resolver and is provided to implement the resolver.Resolver interface
func (r *Resolver) Close()

// InitialState adds initial state to resolver and is used to update the state of ClientConn
func (r *Resolver) InitialState(s resolver.State)

// ResolveNow is a no-op for Resolver
func (r *Resolver) ResolveNow(resolver.ResolveNowOptions)

// Scheme returns the scheme name used by this resolver
func (r *Resolver) Scheme() string

// UpdateState updates the state of ClientConn with s
func (r *Resolver) UpdateState(s resolver.State) error

Resolver Interface

Core Interface

type Resolver interface {
    // ResolveNow is called by gRPC to attempt to resolve the target name again
    // This is a hint - resolver can ignore if not necessary
    // May be called multiple times concurrently
    ResolveNow(ResolveNowOptions)

    // Close closes the resolver
    Close()
}

type ResolveNowOptions struct{}

Builder Interface

type Builder interface {
    // Build creates a new resolver for the given target
    // gRPC dial calls Build synchronously and fails if error is not nil
    Build(target Target, cc ClientConn, opts BuildOptions) (Resolver, error)

    // Scheme returns the scheme supported by this resolver
    // Should not contain uppercase characters
    Scheme() string
}

// Register registers the resolver builder
// Builder.Scheme() is used as the registered scheme (case-sensitive)
// Must only be called during initialization (in init()) - not thread-safe
func Register(b Builder)

// Get returns the resolver builder registered with the given scheme
// Returns nil if no builder is registered
func Get(scheme string) Builder

Build Options

type BuildOptions struct {
    // DisableServiceConfig indicates whether resolver should fetch service config
    DisableServiceConfig bool

    // DialCreds is transport credentials from ClientConn
    // May be used if name resolution service requires same credentials
    DialCreds credentials.TransportCredentials

    // CredsBundle is credentials bundle from ClientConn
    CredsBundle credentials.Bundle

    // Dialer is custom dialer from ClientConn
    Dialer func(context.Context, string) (net.Conn, error)

    // Authority is the effective authority of the ClientConn
    Authority string

    // MetricsRecorder for recording metrics
    MetricsRecorder stats.MetricsRecorder
}

ClientConn Interface

The ClientConn interface provided to resolvers:

type ClientConn interface {
    // UpdateState updates the state of the ClientConn
    // If error returned, resolver should try to resolve again
    // Use backoff timer to prevent overloading
    // If resolved State is same as last reported, calling UpdateState can be omitted
    UpdateState(State) error

    // ReportError notifies ClientConn that Resolver encountered an error
    // ClientConn forwards this to the load balancing policy
    ReportError(error)

    // NewAddress notifies ClientConn of new resolved addresses
    // Deprecated: Use UpdateState instead
    NewAddress(addresses []Address)

    // ParseServiceConfig parses the provided service config
    // Returns parsed config object
    ParseServiceConfig(serviceConfigJSON string) *serviceconfig.ParseResult
}

Resolver State

type State struct {
    // Addresses is the latest set of resolved addresses for the target
    // Soon to be deprecated and replaced by Endpoints
    Addresses []Address

    // Endpoints is the latest set of resolved endpoints for the target
    // If resolver produces Endpoints but not Addresses, must ensure
    // LB policies selected support Endpoints
    Endpoints []Endpoint

    // ServiceConfig contains result from parsing latest service config
    // nil indicates no service config or resolver doesn't provide configs
    ServiceConfig *serviceconfig.ParseResult

    // Attributes contains arbitrary data about the resolver
    // Intended for consumption by load balancing policy
    Attributes *attributes.Attributes
}

Addresses and Endpoints

Address

type Address struct {
    // Addr is the server address for connection establishment
    Addr string

    // ServerName is the name of this address
    // If non-empty, used as TLS authority instead of hostname from dial target
    // WARNING: ServerName must only be populated with trusted values
    // Insecure to populate with untrusted data - bypasses TLS authority checks
    ServerName string

    // Attributes contains arbitrary data about this address
    // Intended for consumption by SubConn
    Attributes *attributes.Attributes

    // BalancerAttributes contains arbitrary data for LB policy
    // Does not affect SubConn creation, connection establishment, etc.
    // Deprecated: when Address is inside Endpoint, don't use this field
    BalancerAttributes *attributes.Attributes

    // Metadata associated with Addr, used for load balancing decisions
    // Deprecated: use Attributes instead
    Metadata any
}

// Equal returns whether addresses are identical
// Metadata compared directly, not with recursive introspection
func (a Address) Equal(o Address) bool

// String returns JSON formatted string representation
func (a Address) String() string

Endpoint

type Endpoint struct {
    // Addresses contains list of addresses to access this endpoint
    Addresses []Address

    // Attributes contains arbitrary data about this endpoint
    // Intended for consumption by LB policy
    Attributes *attributes.Attributes
}

// ValidateEndpoints validates endpoints from petiole policy's perspective
// Petiole policies should call this before calling into children
func ValidateEndpoints(endpoints []Endpoint) error

Address and Endpoint Maps

AddressMapV2

type AddressMapV2[T any] struct {
    // Has unexported fields
}

// NewAddressMapV2 creates a new AddressMapV2
// Map takes into account Attributes but ignores BalancerAttributes, Metadata, Type
// Not thread-safe - multiple accesses may not be performed concurrently
func NewAddressMapV2[T any]() *AddressMapV2[T]

// Set updates or adds value for address
func (a *AddressMapV2[T]) Set(addr Address, value T)

// Get returns value for address if present
func (a *AddressMapV2[T]) Get(addr Address) (value T, ok bool)

// Delete removes address from map
func (a *AddressMapV2[T]) Delete(addr Address)

// Len returns number of entries
func (a *AddressMapV2[T]) Len() int

// Keys returns slice of all current map keys
func (a *AddressMapV2[T]) Keys() []Address

// Values returns slice of all current map values
func (a *AddressMapV2[T]) Values() []T

EndpointMap

type EndpointMap[T any] struct {
    // Has unexported fields
}

// NewEndpointMap creates a new EndpointMap
// Keyed on unordered set of address strings within endpoint
// Not thread-safe
func NewEndpointMap[T any]() *EndpointMap[T]

// Set updates or adds value for endpoint
func (em *EndpointMap[T]) Set(e Endpoint, value T)

// Get returns value for endpoint if present
func (em *EndpointMap[T]) Get(e Endpoint) (value T, ok bool)

// Delete removes endpoint from map
func (em *EndpointMap[T]) Delete(e Endpoint)

// Len returns number of entries
func (em *EndpointMap[T]) Len() int

// Keys returns slice of all current map keys
func (em *EndpointMap[T]) Keys() []Endpoint

// Values returns slice of all current map values
func (em *EndpointMap[T]) Values() []T

Custom Resolver Implementation

Basic Custom Resolver

import (
    "context"
    "sync"
    "time"
    "google.golang.org/grpc/resolver"
)

// Custom resolver that periodically updates addresses
type myResolver struct {
    target   resolver.Target
    cc       resolver.ClientConn
    ctx      context.Context
    cancel   context.CancelFunc
    wg       sync.WaitGroup
}

func (r *myResolver) start() {
    r.wg.Add(1)
    go r.watcher()
}

func (r *myResolver) watcher() {
    defer r.wg.Done()
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()

    // Initial resolution
    r.resolve()

    for {
        select {
        case <-r.ctx.Done():
            return
        case <-ticker.C:
            r.resolve()
        }
    }
}

func (r *myResolver) resolve() {
    // Fetch addresses from custom source
    addrs := r.fetchAddresses()

    // Update ClientConn with new addresses
    err := r.cc.UpdateState(resolver.State{
        Addresses: addrs,
    })
    if err != nil {
        r.cc.ReportError(err)
    }
}

func (r *myResolver) fetchAddresses() []resolver.Address {
    // Custom logic to fetch addresses
    // Could query a database, config service, etc.
    return []resolver.Address{
        {Addr: "10.0.0.1:8080"},
        {Addr: "10.0.0.2:8080"},
        {Addr: "10.0.0.3:8080"},
    }
}

func (r *myResolver) ResolveNow(opts resolver.ResolveNowOptions) {
    // Trigger immediate resolution
    go r.resolve()
}

func (r *myResolver) Close() {
    r.cancel()
    r.wg.Wait()
}

// Builder for custom resolver
type myResolverBuilder struct{}

func (b *myResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
    ctx, cancel := context.WithCancel(context.Background())
    r := &myResolver{
        target: target,
        cc:     cc,
        ctx:    ctx,
        cancel: cancel,
    }
    r.start()
    return r, nil
}

func (b *myResolverBuilder) Scheme() string {
    return "myresolver"
}

// Register resolver
func init() {
    resolver.Register(&myResolverBuilder{})
}

// Usage
conn, err := grpc.NewClient("myresolver:///myservice",
    grpc.WithTransportCredentials(creds))

Resolver with Service Config

func (r *myResolver) resolve() {
    addrs := r.fetchAddresses()

    // Provide service config
    serviceConfigJSON := `{
        "loadBalancingPolicy": "round_robin",
        "methodConfig": [{
            "name": [{"service": "myservice"}],
            "waitForReady": true,
            "timeout": "10s"
        }]
    }`

    serviceConfig := r.cc.ParseServiceConfig(serviceConfigJSON)

    err := r.cc.UpdateState(resolver.State{
        Addresses:     addrs,
        ServiceConfig: serviceConfig,
    })
    if err != nil {
        r.cc.ReportError(err)
    }
}

Resolver with Attributes

import "google.golang.org/grpc/attributes"

func (r *myResolver) resolve() {
    addrs := []resolver.Address{
        {
            Addr: "10.0.0.1:8080",
            Attributes: attributes.New("weight", 100),
        },
        {
            Addr: "10.0.0.2:8080",
            Attributes: attributes.New("weight", 50),
        },
    }

    // Resolver-level attributes
    resolverAttrs := attributes.New("datacenter", "us-east-1")

    err := r.cc.UpdateState(resolver.State{
        Addresses:  addrs,
        Attributes: resolverAttrs,
    })
    if err != nil {
        r.cc.ReportError(err)
    }
}

Authority Override

Implement AuthorityOverrider to customize authority used for ClientConn:

type AuthorityOverrider interface {
    // OverrideAuthority returns authority to use for ClientConn
    // Must generate without blocking (typically inline)
    // Must keep returned string unchanged
    // Must return valid ":authority" header value (RFC3986 encoded)
    OverrideAuthority(Target) string
}

// Example implementation
type myResolverBuilder struct{}

func (b *myResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
    // Build resolver...
}

func (b *myResolverBuilder) Scheme() string {
    return "myscheme"
}

func (b *myResolverBuilder) OverrideAuthority(target resolver.Target) string {
    // Custom authority logic
    return "custom-authority.example.com"
}

Using Custom Resolvers

Client-Side Configuration

import "google.golang.org/grpc"

// Method 1: Register globally and use scheme
resolver.Register(&myResolverBuilder{})
conn, err := grpc.NewClient("myscheme:///myservice",
    grpc.WithTransportCredentials(creds))

// Method 2: Pass resolver directly
builder := &myResolverBuilder{}
conn, err := grpc.NewClient("myservice",
    grpc.WithResolvers(builder),
    grpc.WithTransportCredentials(creds))

Dial Options for Resolvers

// WithResolvers allows passing custom resolver builders
// Experimental
func WithResolvers(rs ...resolver.Builder) DialOption

// WithDisableServiceConfig disables service config from resolver
func WithDisableServiceConfig() DialOption

// WithDefaultServiceConfig provides fallback service config
func WithDefaultServiceConfig(s string) DialOption

Best Practices

Resolver Implementation

  1. Thread safety: Protect shared state accessed by multiple goroutines
  2. Backoff: Use exponential backoff for retries on errors
  3. Context usage: Respect context cancellation for graceful shutdown
  4. Initial resolution: Provide initial addresses in Build if possible
  5. ResolveNow: Implement for on-demand resolution
  6. Error reporting: Use ReportError for transient failures

Address Management

  1. Complete lists: Always provide complete address lists in UpdateState
  2. Stability: Avoid updating if addresses haven't changed
  3. Attributes: Use Attributes for custom per-address metadata
  4. ServerName: Only use with trusted values - security risk otherwise

Service Config

  1. Validation: Parse service config before calling UpdateState
  2. Backward compatibility: Ignore unknown fields in custom configs
  3. Default configs: Use WithDefaultServiceConfig for fallback
  4. Disable when needed: Use WithDisableServiceConfig for testing

Testing

import (
    "testing"
    "google.golang.org/grpc/resolver/manual"
)

func TestWithCustomResolver(t *testing.T) {
    // Use manual resolver for testing
    r := manual.NewBuilderWithScheme("test")

    conn, err := grpc.NewClient("test:///unused",
        grpc.WithResolvers(r),
        grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        t.Fatalf("failed to dial: %v", err)
    }
    defer conn.Close()

    // Update addresses programmatically
    r.UpdateState(resolver.State{
        Addresses: []resolver.Address{
            {Addr: "localhost:8080"},
        },
    })

    // Test RPCs...
}

Registration

  1. Register in init(): Call resolver.Register() in package init function
  2. Unique schemes: Use unique scheme names to avoid conflicts
  3. Lowercase: Scheme names should be lowercase
  4. Thread safety: Registration is not thread-safe, only do at initialization