This guide will walk you through creating and using your first Goa interceptor. We’ll create a simple logging interceptor that records the timing of method calls.
Interceptors are defined in your design using the Interceptor
function. Here’s
a simple logging interceptor:
var RequestLogger = Interceptor("RequestLogger", func() {
Description("Logs incoming requests and their timing")
// We want to read the method status from the result
ReadResult(func() {
Attribute("status", Int, "Returned status code") // Business logic status code - not HTTP
})
// We'll add timing information to the result
WriteResult(func() {
Attribute("processedAt", String, "When the request was processed")
Attribute("duration", Int, "Processing duration in milliseconds")
})
})
The Interceptor
DSL defines a new interceptor named RequestLogger
. Using
ReadResult
and WriteResult
, it specifies which fields it needs to access
from the result - in this case reading a result status code and writing timing
information. Goa also has equivalent DSL to read and write payloads.
You can apply interceptors at both the service and method level:
var _ = Service("calculator", func() {
// Apply to all methods in the service
ServerInterceptor(RequestLogger)
Method("add", func() {
// Method-specific interceptor
ServerInterceptor(ValidateNumbers)
Payload(func() {
Attribute("a", Int)
Attribute("b", Int)
})
Result(Int)
})
})
The example shows how to apply interceptors in the service design using
ServerInterceptor
. You can attach them at both the service level (affecting
all methods) or method level (affecting just that method).
This creates a simple logging system that can track timing across your service operations while maintaining type safety through Goa’s generated code.
The generated code will provide you with type-safe interfaces for implementing your interceptor. Here’s how to implement the logging interceptor:
func (i *ServerInterceptors) RequestLogger(ctx context.Context, info *RequestLoggerInfo, next goa.Endpoint) (any, error) {
start := time.Now()
// Call the next interceptor or the final endpoint
res, err := next(ctx, info.RawPayload())
if err != nil {
return nil, err
}
// Access the result through the type-safe interface
r := info.Result(res)
// Add our timing information
r.SetProcessedAt(time.Now().Format(time.RFC3339))
r.SetDuration(int(time.Since(start).Milliseconds()))
return res, nil
}
Let’s break down how this interceptor works:
The function signature follows Goa’s interceptor pattern:
Timing capture:
start := time.Now()
Records when the request started
Calling the next handler:
res, err := next(ctx, info.RawPayload())
Accessing the result:
r := info.Result(res)
Uses the generated type-safe interface to access the result
Adding timing information:
r.SetProcessedAt(time.Now().Format(time.RFC3339))
r.SetDuration(int(time.Since(start).Milliseconds()))
Returns the modified result:
return res, nil
Passes the enriched response back up the chain
Once you’ve defined your interceptor, Goa generates the necessary code to wire it into your service. Here’s how the generated code is structured:
ServerInterceptors
interface that defines all server-side interceptors:// ServerInterceptors defines the interface for all server-side interceptors
type ServerInterceptors interface {
RequestLogger(ctx context.Context, info *RequestLoggerInfo, next goa.Endpoint) (any, error)
// ... other interceptors ...
}
// Info structure provides metadata about the interception
type RequestLoggerInfo struct {
service string
method string
callType goa.InterceptorCallType
rawPayload any
}
// Type-safe interface for accessing the result
type RequestLoggerResult interface {
Status() int
SetProcessedAt(string)
SetDuration(int)
}
ServerInterceptors
interface in your service:type interceptors struct {
logger *log.Logger
}
func NewInterceptors(logger *log.Logger) *interceptors {
return &interceptors{logger: logger}
}
func (i *interceptors) RequestLogger(ctx context.Context, info *RequestLoggerInfo, next goa.Endpoint) (any, error) {
// Implementation from earlier example
start := time.Now()
res, err := next(ctx, info.RawPayload())
if err != nil {
return nil, err
}
r := info.Result(res)
r.SetProcessedAt(time.Now().Format(time.RFC3339))
r.SetDuration(int(time.Since(start).Milliseconds()))
return res, nil
}
func main() {
// Create your service implementation
svc := NewService()
// Create interceptors
interceptors := NewInterceptors(log.Default())
// Create endpoints with interceptors
endpoints := NewEndpoints(svc, interceptors)
// ... Proceed as usual ...
}
The generated code provides several key benefits:
The generated interfaces and wrappers ensure that your interceptors are properly integrated into the request processing pipeline while maintaining type safety throughout the entire chain.
When multiple interceptors are applied, they execute in the following order:
This means that interceptors wrap around both the request and response flow.
Now that you understand the basics: