В чём разница между unary interceptors и stream interceptors в gRPC-Go?
Unary interceptors обрабатывают одиночные RPC-вызовы (аналог HTTP middleware). Stream interceptors оборачивают стриминговые RPC и получают объект ServerStream, позволяя перехватывать каждый Send/Recv через обёртку.
Что такое interceptors в gRPC-Go
Interceptors — механизм middleware для gRPC, аналогичный HTTP-middleware. Они позволяют добавлять сквозную логику (логирование, аутентификацию, трейсинг, восстановление после паники) без изменения бизнес-кода. Существует два вида на стороне сервера и два на стороне клиента.
Unary Server Interceptor
Вызывается один раз для каждого Unary RPC. Сигнатура фиксирована: принимает контекст, запрос, информацию о методе и обработчик handler.
func LoggingUnaryInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req) // вызов реального метода
log.Printf("%s duration=%s err=%v", info.FullMethod, time.Since(start), err)
return resp, err
}
// Регистрация
s := grpc.NewServer(
grpc.UnaryInterceptor(LoggingUnaryInterceptor),
)
Stream Server Interceptor
Вызывается один раз при установке стриминга, но не для каждого отдельного сообщения. Чтобы перехватывать каждый Send/Recv, нужно обернуть grpc.ServerStream.
// Обёртка для перехвата сообщений потока
type wrappedStream struct {
grpc.ServerStream
}
func (w *wrappedStream) RecvMsg(m interface{}) error {
log.Printf("[stream] Recv: %T", m)
return w.ServerStream.RecvMsg(m)
}
func (w *wrappedStream) SendMsg(m interface{}) error {
log.Printf("[stream] Send: %T", m)
return w.ServerStream.SendMsg(m)
}
func LoggingStreamInterceptor(
srv interface{},
ss grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler,
) error {
start := time.Now()
err := handler(srv, &wrappedStream{ss}) // передаём обёртку
log.Printf("%s duration=%s err=%v", info.FullMethod, time.Since(start), err)
return err
}
// Регистрация
s := grpc.NewServer(
grpc.StreamInterceptor(LoggingStreamInterceptor),
)
Цепочка interceptors (chaining)
Начиная с grpc-go v1.34 доступны grpc.ChainUnaryInterceptor и grpc.ChainStreamInterceptor для регистрации нескольких interceptors:
s := grpc.NewServer(
grpc.ChainUnaryInterceptor(
RecoveryUnaryInterceptor,
AuthUnaryInterceptor,
LoggingUnaryInterceptor,
),
grpc.ChainStreamInterceptor(
RecoveryStreamInterceptor,
AuthStreamInterceptor,
),
)
Клиентские interceptors
// Unary client interceptor — добавляет токен авторизации
func AuthUnaryClientInterceptor(token string) grpc.UnaryClientInterceptor {
return func(
ctx context.Context,
method string,
req, reply interface{},
cc *grpc.ClientConn,
invoker grpc.UnaryInvoker,
opts ...grpc.CallOption,
) error {
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
return invoker(ctx, method, req, reply, cc, opts...)
}
}
conn, _ := grpc.NewClient(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(AuthUnaryClientInterceptor("my-token")),
)
Подводные камни
- Нельзя зарегистрировать более одного interceptor через
grpc.UnaryInterceptor()— вторая регистрация вызовет panic; используйтеgrpc.ChainUnaryInterceptor(). - Stream interceptor вызывается только при открытии потока, а не для каждого сообщения; без обёртки
ServerStreamнельзя перехватить отдельные Send/Recv. - Panic-recovery interceptor должен быть первым в цепочке (самый внешний), иначе паника из другого interceptor не будет поймана.
- При добавлении значений в контекст внутри interceptor используйте типизированные ключи (
type ctxKey struct{}), а не строки — для избежания коллизий. - Interceptor, не вызвавший
handler(), должен явно вернуть ошибку; молчаливый возвратnilсоздаст пустой ответ без обработки. - Трассировка через OpenTelemetry требует отдельных interceptors для Unary и Stream; пакет
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpcпредоставляет оба. - В stream interceptor
info.IsClientStreamиinfo.IsServerStreamпозволяют определить тип потока и применить разную логику для server/client/bidi streaming.
Common mistakes
- Давать ответ про unary и stream interceptors только на уровне определения, не показывая поведение в реальном приложении.
- Игнорировать границы ответственности вокруг темы «unary и stream interceptors»: кто отменяет работу, кто владеет ресурсом и где формируется ответ клиенту.
- Не связывать unary и stream interceptors с observability, тестированием или безопасностью, когда это влияет на продакшен-поведение.
What the interviewer is testing
- Точно объясняет, что именно делает unary и stream interceptors и где это используется в Go-коде.
- Связывает unary и stream interceptors с корректным lifecycle запроса, отменой, конкурентностью или конфигурацией сервера там, где это уместно.
- Не изобретает API и опирается на реальные контракты официальной документации.