Вступление: почему вашему Go-проекту нужен дирижёр
Представьте себе симфонический оркестр. У вас есть скрипки (сервис пользователей), духовые (сервис заказов), ударные (сервис платежей). Каждый инструмент прекрасен сам по себе, но без дирижёра они будут играть кто в лес, кто по дрова. В мире микросервисов на Go API Gateway — это тот самый дирижёр.
Когда вы пишете на Go, вы привыкли к производительности, простоте и элегантности. Но как только количество сервисов переваливает за 3–4, начинается хаос: клиенты не знают, куда стучаться, аутентификация дублируется в каждом сервисе, а мониторинг превращается в головную боль. API Gateway для Go сервисов решает эти проблемы разом.
В этой статье мы не просто поговорим о теории. Мы возьмём Go, стандартную библиотеку и пару популярных пакетов, чтобы собрать свой собственный шлюз. Готовы дирижировать? Поехали!
Что такое API Gateway и зачем он нужен в Go?
API Gateway — это единая точка входа для всех клиентских запросов к вашим бэкенд-сервисам. Представьте, что у вас есть:
user-service:8081— регистрация и профилиorder-service:8082— корзина и заказыpayment-service:8083— транзакции
Без шлюза клиент должен знать адреса всех трёх сервисов. С API Gateway на Go он стучится только в api.example.com, а шлюз сам решает, куда направить запрос.
Основные функции шлюза
- Маршрутизация — перенаправление запросов к нужному сервису
- Аутентификация и авторизация — проверка JWT токенов в одном месте
- Rate Limiting — защита от DDoS и перегрузок
- Агрегация данных — сбор ответов от нескольких сервисов в один
- Балансировка нагрузки — распределение запросов между инстансами
Go идеально подходит для этой роли: он лёгкий, быстрый и позволяет обрабатывать тысячи запросов в секунду на одном ядре.
Пишем простой API Gateway на Go: от идеи до кода
Давайте построим минимальный, но рабочий Go API Gateway. Мы будем использовать только стандартную библиотеку net/http и пакет httputil.ReverseProxy для проксирования.
Шаг 1: Базовая маршрутизация
Начнём с простого: направляем запросы на разные сервисы в зависимости от пути.
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func main() {
// Сервис пользователей
userServiceURL, _ := url.Parse("http://localhost:8081")
userProxy := httputil.NewSingleHostReverseProxy(userServiceURL)
// Сервис заказов
orderServiceURL, _ := url.Parse("http://localhost:8082")
orderProxy := httputil.NewSingleHostReverseProxy(orderServiceURL)
http.HandleFunc("/api/users/", func(w http.ResponseWriter, r *http.Request) {
log.Printf("Перенаправляем запрос к user-service: %s", r.URL.Path)
userProxy.ServeHTTP(w, r)
})
http.HandleFunc("/api/orders/", func(w http.ResponseWriter, r *http.Request) {
log.Printf("Перенаправляем запрос к order-service: %s", r.URL.Path)
orderProxy.ServeHTTP(w, r)
})
log.Println("API Gateway запущен на :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Этот код уже делает 80% работы: любой запрос к /api/users/ уходит на user-service, а к /api/orders/ — на order-service. Но где же аутентификация?
Шаг 2: Добавляем middleware для JWT
Настоящая магия API Gateway на Go происходит в middleware. Давайте добавим проверку JWT-токена перед проксированием.
package main
import (
"context"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
// Структура для хранения данных пользователя в контексте
type contextKey string
const userIDKey contextKey = "userID"
// Middleware для проверки JWT
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" || !strings.HasPrefix(token, "Bearer ") {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Упрощённая проверка: в реальности используйте библиотеку jwt-go
tokenValue := strings.TrimPrefix(token, "Bearer ")
if tokenValue != "valid-token" {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// Передаём ID пользователя в контекст
ctx := context.WithValue(r.Context(), userIDKey, "user-123")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func main() {
userServiceURL, _ := url.Parse("http://localhost:8081")
userProxy := httputil.NewSingleHostReverseProxy(userServiceURL)
// Прокси с логированием
userHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value(userIDKey)
log.Printf("Пользователь %s запрашивает %s", userID, r.URL.Path)
userProxy.ServeHTTP(w, r)
})
http.Handle("/api/users/", authMiddleware(userHandler))
log.Println("Защищённый API Gateway на :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Теперь все запросы к пользовательскому сервису проходят через аутентификацию. Go делает это элегантно и без лишних зависимостей.
Продвинутые паттерны: агрегация и rate limiting
Простое проксирование — это хорошо, но шлюз API для Go сервисов может гораздо больше. Давайте рассмотрим два мощных паттерна.
Агрегация данных: собираем пазл из сервисов
Представьте, что клиент хочет получить профиль пользователя с его последними заказами. Вместо двух запросов делаем один — шлюз сам соберёт данные.
func aggregateHandler(w http.ResponseWriter, r *http.Request) {
// Параллельно запрашиваем данные из двух сервисов
userChan := make(chan []byte)
orderChan := make(chan []byte)
go func() {
// Запрос к user-service
resp, _ := http.Get("http://localhost:8081/profile")
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
userChan <- body
}()
go func() {
resp, _ := http.Get("http://localhost:8082/orders/last")
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
orderChan <- body
}()
userData := <-userChan
orderData := <-orderChan
// Объединяем в один JSON
combined := fmt.Sprintf(`{"user": %