gRPC-GoMiddleTechnical

В чём разница между 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 и опирается на реальные контракты официальной документации.

Sources

Related topics

В чём разница между unary interceptors и stream interceptors в gRPC-Go? | Talanto