gRPC-GoMiddleTechnical
Как работают client/server interceptors и где реализовать auth, logging и retry?
Interceptors — это middleware для gRPC в Go. Unary и stream interceptors перехватывают вызовы на клиенте и сервере; через них реализуются auth, logging, retry и трейсинг, объединяемые цепочкой через grpc.ChainUnaryInterceptor.
Что такое gRPC interceptors
Interceptors в gRPC — аналог middleware в HTTP-фреймворках. Они оборачивают обработку RPC-вызова на стороне сервера или клиента. Существует два вида: UnaryInterceptor (для обычных запросов) и StreamInterceptor (для стриминга).
Серверные interceptors
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// Auth interceptor — проверяет Bearer-токен в метаданных
func authInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 || !validateToken(tokens[0]) {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
return handler(ctx, req)
}
// Logging interceptor — логирует метод, длительность и код ответа
func loggingInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
start := time.Now()
resp, err := handler(ctx, req)
st, _ := status.FromError(err)
log.Printf("method=%s duration=%s code=%s", info.FullMethod, time.Since(start), st.Code())
return resp, err
}
// Запуск сервера с цепочкой interceptors
func main() {
s := grpc.NewServer(
grpc.ChainUnaryInterceptor(
loggingInterceptor, // выполняется первым
authInterceptor, // выполняется вторым
),
)
// регистрация сервисов...
_ = s
}
func validateToken(token string) bool {
return token == "Bearer secret" // упрощённо
}
Клиентские interceptors — retry
// Retry interceptor — повторяет запрос при временных ошибках
func retryInterceptor(maxAttempts int) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
var err error
for i := range maxAttempts {
err = invoker(ctx, method, req, reply, cc, opts...)
if err == nil {
return nil
}
st, _ := status.FromError(err)
if st.Code() != codes.Unavailable && st.Code() != codes.DeadlineExceeded {
return err // не ретраить non-retryable ошибки
}
backoff := time.Duration(i+1) * 100 * time.Millisecond
log.Printf("retry %d/%d after %s", i+1, maxAttempts, backoff)
time.Sleep(backoff)
}
return err
}
}
// Клиент с interceptors
func newClient() (*grpc.ClientConn, error) {
return grpc.NewClient(
"localhost:50051",
grpc.WithChainUnaryInterceptor(
retryInterceptor(3),
),
)
}
Stream interceptors
// Серверный stream interceptor для логирования
func streamLoggingInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
log.Printf("stream started: method=%s isClientStream=%v isServerStream=%v",
info.FullMethod, info.IsClientStream, info.IsServerStream)
err := handler(srv, ss)
log.Printf("stream ended: method=%s err=%v", info.FullMethod, err)
return err
}
// Регистрация stream interceptor
func newServerWithStream() *grpc.Server {
return grpc.NewServer(
grpc.ChainStreamInterceptor(streamLoggingInterceptor),
)
}
Передача данных через контекст
Interceptors часто добавляют данные в контекст для следующих обработчиков. Используйте typed keys, чтобы избежать коллизий:
type contextKey string
const userIDKey contextKey = "userID"
func authInterceptorWithCtx(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
userID := extractUserID(ctx)
ctx = context.WithValue(ctx, userIDKey, userID)
return handler(ctx, req)
}
func extractUserID(ctx context.Context) string {
return "user-123" // из JWT или metadata
}
Подводные камни
- Порядок в цепочке важен — logging должен быть внешним (первым), auth — внутренним. Иначе неаутентифицированные запросы не логируются или auth выполняется после бизнес-логики.
- Паника в interceptor — если interceptor не перехватывает panic, сервер падает полностью. Нужен recovery interceptor как самый внешний в цепочке.
- grpc.UnaryInterceptor vs grpc.ChainUnaryInterceptor — старый API
grpc.UnaryInterceptorпринимает только один interceptor. При передаче нескольких — только последний действует. Всегда используйтеChainUnaryInterceptor. - Context cancellation — retry-interceptor не проверяет
ctx.Err()перед следующей попыткой, что ведёт к лишним запросам после таймаута клиента. - Stream interceptors сложнее unary — для добавления логики в каждое сообщение стрима нужно оборачивать
grpc.ServerStreamкастомной структурой, переопределяяRecvMsg/SendMsg. - Middleware от сторонних библиотек (go-grpc-middleware) — не все совместимы с актуальным grpc-go API (v1.64+), проверяйте совместимость версий.
- Блокирующий retry без backoff — простой retry без экспоненциального отступа создаёт thundering herd при массовых отказах.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.