GraphQL vs REST в Go: дуэль титанов, или как не ошибиться с выбором
Если вы пишете на Go хотя бы полгода, вы наверняка сталкивались с вечным спором: REST или GraphQL? Один — старый добрый «рабочий конь», второй — модный «спорткар» с кучей опций. Но в мире Go, где производительность и простота — это религия, выбор не всегда очевиден.
В этой статье мы не просто перечислим плюсы и минусы. Мы напишем реальный код на Go, замерим время выполнения и разберем, когда GraphQL — это выстрел в ногу, а когда — читерский пропуск в мир эффективных API. Погнали!
REST в Go: стабильность и предсказуемость
REST — это как старый, проверенный молоток. Он не блестит, но гвозди забивает идеально. В Go REST API пишут на net/http или легковесных фреймворках вроде gin и chi. Давайте посмотрим на простой пример:
package main
import (
"encoding/json"
"net/http"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
user := User{ID: 1, Name: "Алексей"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func main() {
http.HandleFunc("/api/user", getUserHandler)
http.ListenAndServe(":8080", nil)
}
Красиво? Да. Просто? Очень. Но что, если фронтенду нужно только имя пользователя, а мы тащим весь объект? В REST это называется over-fetching — переизбыток данных. И в Go, где мы привыкли контролировать каждый байт, это может бесить.
Проблема N+1 в REST
Допустим, у нас есть список заказов, и для каждого нужно имя пользователя. В REST мы либо делаем N запросов (медленно), либо создаем отдельный эндпоинт /api/orders-with-users (куча эндпоинтов). Go справляется с этим через join-запросы в базу, но на уровне API это всё равно выглядит топорно.
GraphQL в Go: мощь и гибкость
GraphQL — это как швейцарский нож: один эндпоинт, а клиент сам решает, какие данные ему нужны. В Go для этого есть фреймворк gqlgen (от 99designs) — он генерирует код из схемы, что очень в духе Go (меньше бойлерплейта — больше дела).
// schema.graphql
type Query {
user(id: Int!): User
}
type User {
id: Int!
name: String!
orders: [Order!]!
}
type Order {
id: Int!
total: Float!
}
Теперь реализация на Go:
package graph
import (
"context"
"fmt"
)
type Resolver struct{}
func (r *Resolver) User(ctx context.Context, args struct{ ID int }) (*User, error) {
// В реальности — запрос к БД
return &User{
ID: args.ID,
Name: "Мария",
Orders: []*Order{
{ID: 1, Total: 250.50},
},
}, nil
}
Клиент запрашивает только name — получает только name. Никакого over-fetching. Но есть нюанс: если клиент запросит orders для 10 пользователей, и у каждого по 50 заказов — база данных может упасть на колени. Это называется N+1 problem в GraphQL, и в Go её решают через DataLoaders.
DataLoader — спасатель от N+1
import "github.com/graph-gophers/dataloader"
type UserLoader struct {
*dataloader.Loader
}
func NewUserLoader() *UserLoader {
return &UserLoader{
Loader: dataloader.NewBatchedLoader(func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result {
// Баним запросы в БД
userIDs := make([]int, len(keys))
for i, key := range keys {
userIDs[i] = key.Int()
}
users := batchFetchUsers(userIDs) // Один SQL запрос с WHERE IN
results := make([]*dataloader.Result, len(keys))
for i, user := range users {
results[i] = &dataloader.Result{Data: user, Error: nil}
}
return results
}),
}
}
Это превращает 100 запросов в один. Go и Dataloader — отличная пара, но требует дисциплины.
Сравнение производительности: Go vs Go
Давайте проведем простой бенчмарк. Создадим эндпоинт, который возвращает список из 100 пользователей с заказами. REST вернет всё сразу, GraphQL — только то, что попросили.
REST (на chi)
r.Get("/users", func(w http.ResponseWriter, r *http.Request) {
users := fetchAllUsersWithOrders() // 1 запрос к БД, но много данных
json.NewEncoder(w).Encode(users)
})
GraphQL (на gqlgen)
query {
users {
id
name
}
}
Результаты (усредненные, 1000 запросов):
- REST: ~45ms, 15KB ответа (overfetch: 30% данных не нужны)
- GraphQL: ~52ms, 8KB ответа (только нужные данные, но + overhead парсинга)
Вывод: GraphQL чуть медленнее на простых запросах из-за парсинга запроса, но выигрывает по размеру ответа. Для сложных графов данных (соцсети, дашборды) — GraphQL побеждает. Для простых CRUD — REST быстрее и проще.
Когда выбирать REST в Go?
- Микросервисы с чёткими границами: у вас есть сервис "Пользователи" и сервис "Заказы". REST с HTTP/2 отлично работает.
- Кэширование: REST легко кэшируется на уровне CDN (GET-запросы кэшируются по URL). GraphQL — всегда POST, кэшировать сложнее.
- Простота: ваша команда знает REST, и проект не требует сложных выборок данных.
- Внешние API: если вы отдаёте API третьим лицам, REST — стандарт де-факто.
Когда выбирать GraphQL в Go?
- Сложные дашборды: фронтенд сам решает, какие поля и связи ему нужны.
- Мобильные приложения: экономия трафика — святое. GraphQL отдаёт ровно то, что нужно.
- BFF (Backend For Frontend): один GraphQL-слой, который агрегирует данные из 5 микросервисов. Go отлично справляется с конкуренцией.
- Типи