endpoint

package
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 5, 2026 License: MIT Imports: 12 Imported by: 0

README

endpoint

The endpoint package is the core abstraction of the framework.

Core types

Type Description
Endpoint func(ctx, request) (response, error) — the single callable unit
Middleware func(Endpoint) Endpoint — wraps an endpoint to add behaviour
Factory func(addr) (Endpoint, io.Closer, error) — creates endpoints from addresses
Failer Optional interface on response types to carry business errors

TypedEndpoint (compile-time type safety)

TypedEndpoint[Req, Resp] eliminates runtime type assertions.

// Define a typed endpoint — no interface{} anywhere
var ep endpoint.TypedEndpoint[HelloReq, HelloResp] =
    func(ctx context.Context, req HelloReq) (HelloResp, error) {
        return HelloResp{Message: "Hello, " + req.Name}, nil
    }

// Apply middleware via NewTypedBuilder, then recover type safety with Unwrap
typed := endpoint.Unwrap[HelloReq, HelloResp](
    endpoint.NewTypedBuilder(ep).
        WithTimeout(5 * time.Second).
        Use(circuitbreaker.Gobreaker(cb)).
        Build(),
)

// Call site is fully type-safe — no .(HelloResp) assertion needed
resp, err := typed(ctx, HelloReq{Name: "world"})
fmt.Println(resp.Message)

Migration path: existing Endpoint code continues to work unchanged. Adopt TypedEndpoint incrementally at the boundaries where type safety matters most.

var metrics endpoint.Metrics

ep := endpoint.NewBuilder(base).
    WithMetrics(&metrics).
    WithErrorHandling("CreateUser").
    WithTimeout(5 * time.Second).
    Use(circuitbreaker.Gobreaker(cb)).
    Use(ratelimit.NewErroringLimiter(limiter)).
    Build()

Chain (lower-level)

ep = endpoint.Chain(
    loggingMiddleware,
    metricsMiddleware,
    authMiddleware,
)(base)
// call order: logging → metrics → auth → base

Built-in middleware

Middleware Import Description
MetricsMiddleware endpoint Counts requests, successes, errors, duration
ErrorHandlingMiddleware endpoint Wraps errors with operation name
TimeoutMiddleware endpoint Cancels context after deadline
LoggingMiddleware endpoint Logs each call with duration
Gobreaker endpoint/circuitbreaker sony/gobreaker circuit breaker
HandyBreaker endpoint/circuitbreaker streadway/handy circuit breaker
Hystrix endpoint/circuitbreaker afex/hystrix-go circuit breaker
NewErroringLimiter endpoint/ratelimit Reject immediately when over limit
NewDelayingLimiter endpoint/ratelimit Wait for token (respects ctx deadline)

Failer

Implement Failer on a response type to carry business errors without using the Go error return value. Useful when the transport protocol requires a successful wire-level response even on business failure (e.g. gRPC).

type MyResponse struct {
    Result string
    Err    error
}

func (r MyResponse) Failed() error { return r.Err }

Metrics

var m endpoint.Metrics
ep = endpoint.MetricsMiddleware(&m)(ep)

fmt.Printf("requests=%d success=%d errors=%d avg_ms=%.1f\n",
    m.RequestCount, m.SuccessCount, m.ErrorCount,
    float64(m.TotalDuration.Milliseconds())/float64(m.RequestCount))

See also

  • examples/middleware/ — runnable demo of every middleware
  • examples/quickstart/ — minimal HTTP service using Builder

Documentation

Overview

Package endpoint defines the core Endpoint type and related helpers.

An Endpoint is the fundamental building block of the framework: a function that accepts a context and an arbitrary request value, and returns an arbitrary response value or an error. Business logic, middleware, and transport layers all communicate through this single interface.

Index

Constants

This section is empty.

Variables

View Source
var ErrBackpressure = errors.New("too many concurrent requests")

ErrBackpressure is returned when the concurrency limit is exceeded.

Functions

func Nop

func Nop(context.Context, any) (any, error)

Nop is a no-op Endpoint that always succeeds and returns an empty struct. Useful as a placeholder in tests or when an endpoint is not yet implemented.

func RequestIDFromContext added in v1.2.0

func RequestIDFromContext(ctx context.Context) string

RequestIDFromContext extracts the request ID from the context.

func WithRequestID added in v1.2.0

func WithRequestID(ctx context.Context, id string) context.Context

WithRequestID injects a request ID into the context.

func WithTraceID added in v1.2.0

func WithTraceID(ctx context.Context, id TraceID) context.Context

WithTraceID injects a trace ID into the context.

Types

type Builder added in v1.2.0

type Builder struct {
	// contains filtered or unexported fields
}

Builder provides a fluent API for assembling an Endpoint with a middleware chain. Middlewares are applied in the order they are added (outermost first), matching the behaviour of Chain().

Example:

ep := endpoint.NewBuilder(myEndpoint).
    Use(loggingMiddleware).
    Use(ratelimit.NewErroringLimiter(limiter)).
    Use(circuitbreaker.Gobreaker(cb)).
    Build()

func NewBuilder added in v1.2.0

func NewBuilder(base Endpoint) *Builder

NewBuilder creates a Builder wrapping the given base Endpoint.

func NewTypedBuilder added in v1.2.0

func NewTypedBuilder[Req, Resp any](te TypedEndpoint[Req, Resp]) *Builder

NewTypedBuilder creates a Builder from a TypedEndpoint. This is a convenience shorthand for endpoint.NewBuilder(te.Wrap()).

Example:

ep := endpoint.NewTypedBuilder(myTypedEndpoint).
    WithTimeout(5 * time.Second).
    Use(circuitbreaker.Gobreaker(cb)).
    Build()

func (*Builder) Build added in v1.2.0

func (b *Builder) Build() Endpoint

Build applies all middlewares and returns the final Endpoint. The Builder can be reused after calling Build.

func (*Builder) Use added in v1.2.0

func (b *Builder) Use(m Middleware) *Builder

Use appends a Middleware to the chain. Returns the same Builder for method chaining.

func (*Builder) WithBackpressure added in v1.2.0

func (b *Builder) WithBackpressure(max int64) *Builder

WithBackpressure appends BackpressureMiddleware with the given concurrency limit.

func (*Builder) WithErrorHandling added in v1.2.0

func (b *Builder) WithErrorHandling(operation string) *Builder

WithErrorHandling appends an ErrorHandlingMiddleware for the named operation.

func (*Builder) WithLogging added in v1.2.0

func (b *Builder) WithLogging(logger *Logger, operation string) *Builder

WithLogging appends LoggingMiddleware for the named operation. This is a shorthand for Use(LoggingMiddleware(logger, operation)).

func (*Builder) WithMetrics added in v1.2.0

func (b *Builder) WithMetrics(m *Metrics) *Builder

WithMetrics appends a MetricsMiddleware and returns the Metrics pointer so the caller can inspect counters later.

var m endpoint.Metrics
ep := endpoint.NewBuilder(base).WithMetrics(&m).Build()

func (*Builder) WithTimeout added in v1.2.0

func (b *Builder) WithTimeout(d time.Duration) *Builder

WithTimeout appends a TimeoutMiddleware that cancels the context after d. This is a shorthand for Use(TimeoutMiddleware(d)).

func (*Builder) WithTracing added in v1.2.0

func (b *Builder) WithTracing() *Builder

WithTracing appends TracingMiddleware to the Builder.

type Endpoint

type Endpoint func(ctx context.Context, request any) (response any, err error)

Endpoint is a function that handles a single RPC-style request. It is the primary abstraction in the framework — every service method, middleware, and transport adapter is expressed in terms of Endpoint.

type EndpointCache

type EndpointCache struct {
	// contains filtered or unexported fields
}

EndpointCache maps service instance addresses to Endpoints. It is updated by Endpointer as service-discovery events arrive and optionally invalidates stale entries after a configurable grace period.

func NewEndpointCache

func NewEndpointCache(factory Factory, logger *log.Logger, options EndpointerOptions) *EndpointCache

func (*EndpointCache) Endpoints

func (c *EndpointCache) Endpoints() ([]Endpoint, error)

func (*EndpointCache) Update

func (c *EndpointCache) Update(event events.Event)

type EndpointCloser

type EndpointCloser struct {
	Endpoint
	io.Closer
}

type EndpointerOption

type EndpointerOption func(*EndpointerOptions)

EndpointerOption is a functional option for EndpointerOptions.

func InvalidateOnError

func InvalidateOnError(timeout time.Duration) EndpointerOption

InvalidateOnError returns an EndpointerOption that enables cache invalidation after a service-discovery error. The cache is cleared once timeout has elapsed, causing subsequent Endpoints() calls to return an error until healthy instances are re-discovered.

type EndpointerOptions

type EndpointerOptions struct {
	// InvalidateOnError, when true, causes the cache to be cleared after
	// InvalidateTimeout has elapsed following a discovery error.
	InvalidateOnError bool

	// InvalidateTimeout is the grace period before the cache is cleared.
	// Only meaningful when InvalidateOnError is true.
	InvalidateTimeout time.Duration
}

EndpointerOptions configures the behaviour of an EndpointCache when a service-discovery error is received.

type ErrorWrapper

type ErrorWrapper struct {
	Operation string
	Err       error
}

ErrorWrapper wraps an endpoint error with the name of the operation that produced it, enabling callers to distinguish errors from different endpoints using errors.As.

func (*ErrorWrapper) Error

func (e *ErrorWrapper) Error() string

func (*ErrorWrapper) Unwrap

func (e *ErrorWrapper) Unwrap() error

type Factory

type Factory func(instance string) (Endpoint, io.Closer, error)

Factory creates an Endpoint for a given service instance address. It is used by EndpointCache and Endpointer to build endpoints on demand as service-discovery events arrive.

The returned io.Closer (if non-nil) is called when the instance is removed from the cache, allowing the caller to release resources (e.g. close a gRPC connection).

type Failer

type Failer interface {
	Failed() error
}

Failer may be implemented by a response type to signal a business-logic error without using the Go error return value. If the response implements Failer and Failed() returns non-nil, the transport layer treats the call as failed.

When to use Failer

Most services should return errors normally:

func (s *svc) CreateUser(ctx context.Context, req CreateUserRequest) (CreateUserResponse, error) {
    if req.Name == "" {
        return CreateUserResponse{}, errors.New("name required") // normal Go error
    }
    ...
}

Use Failer only when the transport protocol requires a successful wire-level response even on business failure — for example, when a gRPC method must return a proto message (not a gRPC status error) to carry structured error details:

type CreateUserResponse struct {
    User  *User
    Error string `json:"error,omitempty"`
    err   error  // unexported, set by business logic
}

func (r CreateUserResponse) Failed() error { return r.err }

type Logger added in v1.2.0

type Logger = log.Logger

Logger is an alias kept for convenience within this package.

type Metrics

type Metrics struct {
	RequestCount    int64
	ErrorCount      int64
	SuccessCount    int64
	TotalDuration   time.Duration
	LastRequestTime time.Time
}

Metrics holds counters and timing data collected by MetricsMiddleware. All fields are updated in-place; use atomic reads if you need to observe them from a different goroutine.

type Middleware

type Middleware func(Endpoint) Endpoint

Middleware is a function that wraps an Endpoint to add cross-cutting concerns such as logging, metrics, rate limiting, or circuit breaking.

The recommended way to compose middlewares is via Builder:

ep := endpoint.NewBuilder(base).
    Use(m1).Use(m2).Use(m3).
    Build()

Chain is available for cases where a Middleware value itself is needed.

func BackpressureMiddleware added in v1.2.0

func BackpressureMiddleware(max int64) Middleware

BackpressureMiddleware returns a Middleware that limits the number of concurrent in-flight requests to max. When the limit is reached, new requests are rejected immediately with ErrBackpressure.

This is essential for large-scale systems to prevent cascading failures when a downstream service slows down.

Example:

// Allow at most 100 concurrent requests
ep = endpoint.BackpressureMiddleware(100)(ep)

func Chain

func Chain(outer Middleware, others ...Middleware) Middleware

Chain composes multiple Middlewares into a single Middleware. Prefer Builder.Use() for most use cases — it is more readable and supports named shortcuts (WithTimeout, WithMetrics, etc.).

The first argument is the outermost wrapper; subsequent arguments are applied inward:

outer → others[0] → others[1] → … → Endpoint

func ErrorHandlingMiddleware

func ErrorHandlingMiddleware(operation string) Middleware

ErrorHandlingMiddleware returns a Middleware that wraps any error returned by the next Endpoint in an ErrorWrapper tagged with operation. Use errors.As to unwrap it downstream:

var ew *endpoint.ErrorWrapper
if errors.As(err, &ew) { ... }

func InFlightMiddleware added in v1.2.0

func InFlightMiddleware(max int64, counter *int64) Middleware

InFlightMiddleware is like BackpressureMiddleware but also exposes the current in-flight count via the provided pointer. Useful for metrics.

Example:

var inflight int64
ep = endpoint.InFlightMiddleware(100, &inflight)(ep)
// inflight is updated atomically on every call

func LoggingMiddleware added in v1.2.0

func LoggingMiddleware(logger *log.Logger, operation string) Middleware

LoggingMiddleware returns a Middleware that logs each call to the wrapped Endpoint using the provided zap logger. It records:

  • the operation name
  • whether the call succeeded or failed
  • the elapsed duration

Example:

logger, _ := log.NewDevelopment()
ep = endpoint.LoggingMiddleware(logger, "CreateUser")(ep)

func MetricsMiddleware

func MetricsMiddleware(metrics *Metrics) Middleware

MetricsMiddleware returns a Middleware that records per-endpoint metrics into the provided Metrics struct. It increments RequestCount on every call, SuccessCount when the next Endpoint returns nil error, and ErrorCount otherwise.

func TimeoutMiddleware added in v1.2.0

func TimeoutMiddleware(d time.Duration) Middleware

TimeoutMiddleware returns a Middleware that cancels the context after d. The wrapped endpoint receives a context that will be cancelled when the deadline is exceeded.

Example:

ep = endpoint.TimeoutMiddleware(5 * time.Second)(ep)

func TracingMiddleware added in v1.2.0

func TracingMiddleware() Middleware

TracingMiddleware returns a Middleware that propagates or generates a trace ID and request ID in the context.

If the context already contains a trace ID (injected by the transport layer from an incoming header), it is preserved. Otherwise a new one is generated.

This enables end-to-end request correlation across service boundaries without requiring an external tracing system.

Example:

ep = endpoint.TracingMiddleware()(ep)

// In a handler, read the IDs:
traceID := endpoint.TraceIDFromContext(ctx)
reqID   := endpoint.RequestIDFromContext(ctx)

type SpanID added in v1.2.0

type SpanID string

SpanID is a unique identifier for a single operation within a trace.

type TraceID added in v1.2.0

type TraceID string

TraceID is a unique identifier for a distributed trace.

func TraceIDFromContext added in v1.2.0

func TraceIDFromContext(ctx context.Context) TraceID

TraceIDFromContext extracts the trace ID from the context. Returns an empty string if not set.

type TypeAssertError added in v1.2.0

type TypeAssertError struct {
	Got  any
	Want any
}

TypeAssertError is returned by Unwrap when the response value cannot be asserted to the expected type.

func (*TypeAssertError) Error added in v1.2.0

func (e *TypeAssertError) Error() string

type TypedEndpoint added in v1.2.0

type TypedEndpoint[Req, Resp any] func(ctx context.Context, req Req) (Resp, error)

TypedEndpoint is a type-safe variant of Endpoint that eliminates runtime type assertions. Req and Resp are the concrete request and response types.

Use Wrap to convert a TypedEndpoint into a plain Endpoint for use with middleware and transport layers. Use Unwrap to go the other direction.

Example:

type HelloReq  struct { Name string }
type HelloResp struct { Message string }

var ep endpoint.TypedEndpoint[HelloReq, HelloResp] =
    func(ctx context.Context, req HelloReq) (HelloResp, error) {
        return HelloResp{Message: "Hello, " + req.Name}, nil
    }

// Convert to plain Endpoint for middleware / transport
plain := ep.Wrap()

// Build with middleware, then call type-safely
typed := endpoint.Unwrap[HelloReq, HelloResp](
    endpoint.NewBuilder(plain).
        WithTimeout(5 * time.Second).
        Build(),
)
resp, err := typed(ctx, HelloReq{Name: "world"})

func Unwrap added in v1.2.0

func Unwrap[Req, Resp any](ep Endpoint) TypedEndpoint[Req, Resp]

Unwrap wraps a plain Endpoint in a TypedEndpoint[Req, Resp]. The returned function type-asserts the response; it returns an error if the response cannot be asserted to Resp.

Use this after applying middleware to recover type safety:

typed := endpoint.Unwrap[HelloReq, HelloResp](
    endpoint.NewBuilder(base).Use(myMiddleware).Build(),
)
resp, err := typed(ctx, HelloReq{Name: "world"})

func (TypedEndpoint[Req, Resp]) Wrap added in v1.2.0

func (te TypedEndpoint[Req, Resp]) Wrap() Endpoint

Wrap converts a TypedEndpoint into a plain Endpoint. The returned Endpoint performs a type assertion on the request value; it panics if the request is not of type Req.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL