GraphQL vs REST в Go: что выбрать в 2025

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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 отлично справляется с конкуренцией.
  • Типи

Блоги

Рекомендаций по книгам