Мы все ежедневно работаем с REST API. В REST при выполнении запроса мы отправляем ответ с полной полезной нагрузкой данных объекта. Звучит довольно просто, но у этого подхода есть два существенных недостатка. Допустим, API обслуживает разных клиентов, таких как App/Web, и им нужны разные типы данных (в зависимости от различий в дизайне App и Web). Таким образом, если бэкенд-разработчик в конечном итоге создаст общую конечную точку для обоих этих клиентов, мы можем либо недополучить фактические данные, которые нам нужны, либо получить слишком много данных, в то время как в идеале нам нужен только небольшой объем данных.

Теперь, чтобы решить эту проблему, вы уверены, что мы должны определить две разные конечные точки на серверной части? Один обслуживает приложение, а другой обслуживает Интернет.
Теперь проблема возникает, когда вам нужно включить новое поле в данные ответа. При описанном выше подходе вам нужно будет отдельно добавить обе конечные точки, что приведет к дублированию кода и удвоению пропускной способности для такой простой задачи.
Если вы хотите узнать больше о приведенной выше постановке проблемы, вы можете прочитать ее здесь: Backend For Frontend
Как насчет того, чтобы вместо нескольких API у нас был один GraphQL API. У него будет единственная точка входа; данные запрашиваются и извлекаются путем простой передачи набора обязательных полей в самой полезной нагрузке запроса. Таким образом, клиенты (приложение или Интернет) могут определить точный набор значений, которые они хотят, чтобы внутренний сервер извлекал/вычислял и отправлял их взамен. Facebook разработал его в 2012 году, а затем публично выпустил в 2015 году.

** Краткий отказ от ответственности. Этот учебник очень практичен и требует от вас изучения GraphQL 👩🏼💻
В этом блоге мы будем использовать библиотеки Golang и gqlgen для создания простого GraphQL API. Прежде чем углубиться, мы немного проверим Golang и Graphql.
Go (также называемый языком Golang или Go) — это скомпилированный и статически типизированный язык программирования с открытым исходным кодом, поддерживаемый Google. Он создан, чтобы быть простым, высокопроизводительным, удобочитаемым и эффективным.
GraphQL — это язык запросов для API и среды выполнения для выполнения этих запросов с вашими существующими данными. GraphQL предоставляет полное и понятное описание данных в вашем API, дает клиентам возможность запрашивать именно то, что им нужно, и ничего больше, упрощает развитие API с течением времени и предоставляет мощные инструменты разработчика.
Если вы новичок в Golang, вы можете начать с Golang tour, чтобы изучить основы языка.
Что касается GraphQL, давайте разберемся с некоторыми основами, прежде чем мы начнем:
- GraphQL создает интерфейс уровня данных вашего приложения и служит API для клиентов вашего сервиса.
- Схема GraphQL может быть определена тремя способами:
— с помощью объекта GraphQLSchema
— с помощью запроса самоанализа
— записать схему в файл .graphql с помощью SDL (язык определения схемы). - Вы можете сделать четыре основных запроса в GraphQL: запрос, мутация, фрагмент и подписка.
- Вместо того, чтобы делать запросы с разными HTTP-глаголами к другим URL-адресам, все запросы выполняются как
POSTзапросы к одному URL-адресу. В запросеPOSTв теле отправляется строка, указывающая, какой запрос или мутацию следует выполнить, и какие свойства ожидаются в ответ.
Пример запроса/ответа GraphQL будет выглядеть так:

Давайте сами создадим образец API, чтобы полностью понять, как он работает 🤝
1. Инициализировать проект
Создайте новый каталог и инициализируйте в нем модули go.
Я создал новый каталог как go_graphql
mkdir go_graphqlcdgo_graphqlgo mod initgo_graphql
2. Установите зависимости
Добавьте github.com/99designs/gqlgen к вашему project's tool.go. Он установит зависимости для этого руководства.
printf '// +build tools\npackage tools\nimport _ "github.com/99designs/gqlgen"' | gofmt > tools.go go mod tidy
3. Инициализировать gqlgen
Теперь используйте команду gqlgen init для настройки проекта gqlgen. Он инициализирует конфигурацию gqlgen, сгенерирует модели и создаст скелет проекта для GraphQL.
go run github.com/99designs/gqlgen init
Теперь структура вашего проекта будет выглядеть примерно так:
├── go.mod
├── go.sum
├── gqlgen.yml
├── graph
│ ├── generated
│ │ └── generated.go
│ ├── model
│ │ └── models_gen.go
│ ├── resolver.go
│ ├── schema.graphqls
│ └── schema.resolvers.go
└── server.go
Вот описание сгенерированных файлов от gqlgen:
gqlgen.yml— Конфиг-файл gqlgen, ручки для управления сгенерированным кодом.graph/generated/generated.go— среда выполнения GraphQL, основная часть сгенерированного кода.graph/model/models_gen.go— Сгенерированные модели, необходимые для построения графика. При необходимости мы заменим модели нашими моделями.graph/schema.graphqls— это файл, в который вы будете добавлять схемы GraphQL.resolver.go— Тип преобразователя корневого графа. Этот файл не будет восстановлен.graph/schema.resolvers.go— здесь живет код вашего приложения.generated.goвызовет это, чтобы получить данные, запрошенные пользователем.server.go— точка входа в ваше приложение. Он устанавливаетhttp.Handlerдля сгенерированного сервера GraphQL. Настройте его так, как считаете нужным.
Всякий раз, когда мы меняем схему, нам нужно повторно запускать команду generate.
go run github.com/99designs/gqlgen generate
Запустите сервер с go run server.go и откройте браузер, и вы должны увидеть игровую площадку GraphQL, чтобы убедиться, что настройка выполнена правильно.
Площадка GraphQL будет выглядеть примерно так:

4. Определите нашу схему
Чтобы GraphQL понимал структуру наших объектов, нам нужно определить наши объекты в файле схемы. По умолчанию поле models в gqlgen.yml указывает на models_gen.go, которое вы должны заменить своим объектом. Файл graph/schema.graphqls содержит схему по умолчанию. Либо вы можете добавить новые файлы схемы в папку графа и указать их в gqlgen.yml, либо обновить только этот файл.
Схема по умолчанию:
# GraphQL schema example
#
# https://gqlgen.com/getting-started/
type Todo {
id: ID!
text: String!
done: Boolean!
user: User!
}
type User {
id: ID!
name: String!
}
type Query {
todos: [Todo!]!
}
input NewTodo {
text: String!
userId: String!
}
type Mutation {
createTodo(input: NewTodo!): Todo!
}
Некоторые ключевые моменты, которые следует отметить здесь:
- Структурное имя после
typeназываетсяCharacter. КаждыйCharacterимеет свой собственныйfield(s)и каждыйfieldимеет свой тип. - ! в конце типа означает, что поле является необнуляемым.
inputтипов, которые используются в качестве аргументов для ваших преобразователей.- Все преобразователи делятся на две категории:
*query: используется для получения информации, эквивалентноGETмаршрутам REST API
*mutations: используется для создания, обновления и удаления данных, эквивалентноREST,POSTиPUT.
Все возможные запросы объявлены в типе Query, а мутации в типе Mutation в файле выше. Чтобы предоставить входные данные преобразователю createTodo в виде аргумента с именем input, он должен соответствовать типу ввода NewTodo.
Если мы хотим сравнить команды операций CRUD в SQL и GraphQL:

При этом GraphQL является самодокументируемым, зная, какие функции запускать при поступлении различных запросов. Однако нам нужно определить функции распознавателя, чтобы они соответствовали объявлениям распознавателя, которые будут найдены в schema.resolvers.go. Перед этим давайте сначала посмотрим пример Query в GraphQl.
Простой запрос
Давайте пока запустим простой запрос и вернем жестко закодированные данные.
Откройте файл schema.resolvers.go и проверьте функцию Todos:
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
Здесь он принимает Context в качестве входных данных и возвращает часть Todo и ошибку, если она присутствует. Аргумент ctx содержит данные от лица, отправившего запрос, например, какой пользователь работает с приложением и т. д.
Давайте пока отправим фиктивный ответ для этой функции в schema.resolvers.go:
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
var todos []*model.Todo
dummyTodo := model.Todo{
Text: "our dummy text",
Done: true,
User: &model.User{Name: "testing123"},
}
todos = append(todos, &dummyTodo)
return todos, nil
}
Теперь запустите сервер с go run server.go и введите следующий запрос на игровой площадке Graphql:
query {
todos{
text
done,
user{
name
}
}
}
Ответ будет выглядеть примерно так:
{
"data": {
"todos": [
{
"text": "our dummy text",
"done": true,
"user": {
"name": "testing123"
}
}
]
}
}
Мутация
Давайте реализуем мутацию CreateTodo. Поскольку в этом руководстве у нас нет базы данных, мы получим данные задачи, создадим объект задачи, добавим его в список и отправим обратно для ответа!
Чтобы реализовать это, нам нужно внести изменения в файл resolver.go и schema.resolvers.go.

В файле resolver.go мы можем добавить свойства к структуре Resolver, которая становится доступной через экземпляр преобразователя, представленный r. Мы добавим свойство, представляющее собой массив элементов todo, который мы можем использовать для отслеживания созданных элементов Todo.
import "gqlgen_tutorial/graph/model"
// This file will not be regenerated automatically.
//
// It serves as dependency injection for your app, add any dependencies you require here.
type Resolver struct{
TodosList []*model.Todo
}
Теперь давайте реализуем эти функции преобразователя в schema.resolvers.go:
package graph
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
import (
"context"
"fmt"
"gqlgen_tutorial/graph/generated"
"gqlgen_tutorial/graph/model"
)
// CreateTodo is the resolver for the createTodo field.
func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) {
// CREATE A NEW TODO
todo := &model.Todo{
ID: fmt.Sprint(len(r.TodosList) + 1),
Text: input.Text,
Done: false,
User: &model.User{
ID: input.UserID,
Name: fmt.Sprintf("Elmo%s", fmt.Sprint(len(r.TodosList)+1)),
},
}
// ADD THE TODO TO THE TODOS ARRAY
r.TodosList = append(r.TodosList, todo)
// RETURN ALL THE TODOS
return todo, nil
}
// Todos is the resolver for the todos field.
func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) {
return r.TodosList, nil
}
// Mutation returns generated.MutationResolver implementation.
func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} }
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
Теперь запустите сервер с go run server.go и введите следующий запрос на игровой площадке GraphQL:
mutation {
createTodo(input: {
text: "Test Graphql 1"
userId: "456"
})
{
id
text
done
}
}
И вы получите такой ответ:
{
"data": {
"createTodo": {
"id": "1",
"text": "Test Graphql 1",
"done": false
}
}
}
Давайте реализуем методы RemoveTodo и FindTodo. Как упоминалось выше, нам нужно будет добавить для них Mutation, Query и input в файле schema.graphqls.
Обновлено schema.graphqls:
type Mutation {
createTodo(input: NewTodo!): Todo!
removeTodo(input: DeleteTodo!): Todo!
}
input DeleteTodo {
todoId: String!
}
type Query {
todos: [Todo!]!
findTodo(input: FindTodo!): Todo!
}
input FindTodo {
todoId: String!
}
Давайте повторно запустим команду генерации и увидим волшебство 🪄
go run github.com/99designs/gqlgen generate
Вы заметите:
1. В schema.resolvers.go добавлена новая функция как
// RemoveTodo is the resolver for the removeTodo field.
func (r *mutationResolver) RemoveTodo(ctx context.Context, input model.DeleteTodo) (*model.Todo, error) {
panic(fmt.Errorf("not implemented: RemoveTodo - removeTodo"))
}
2. В файле model/models_gen.go будет присутствовать newtype:
type DeleteTodo struct {
TodoID string `json:"todoId"`
}
type FindTodo struct {
TodoID string `json:"todoId"`
}
Теперь реализуйте функции, упомянутые выше:
// RemoveTodo is the resolver for the removeTodo field.
func (r *mutationResolver) RemoveTodo(ctx context.Context, input model.DeleteTodo) (*model.Todo, error) {
index := -1
for i, todos := range r.TodosList {
if todos.ID == input.TodoID {
index = i
}
}
if index == -1 {
return nil, errors.New("cannot find todo you are looking for")
}
todos := r.TodosList[index]
r.TodosList = append(r.TodosList[:index], r.TodosList[index+1:]...)
return todos, nil
}
// FindTodo is the resolver for the findTodo field.
func (r *queryResolver) FindTodo(ctx context.Context, input model.FindTodo) (*model.Todo, error) {
for _, todos := range r.TodosList {
if todos.ID == input.TodoID {
return todos, nil
}
}
return nil, errors.New("cannot find todo you are looking for")
}
Снова запустите сервер с go run server.go и попробуйте эти два преобразователя:
НайтиTodo
Запрос:
query {
findTodo(input: {todoId: "1"}) {
text
done,
user{
name
}
}
}
Ответ:
{
"data": {
"findTodo": {
"text": "Test Graphql 1",
"done": false,
"user": {
"name": "Elmo1"
}
}
}
}
УдалитьTodo
Запрос:
mutation {
removeTodo(input: {
todoId: "3"
})
{
id
text
done
}
}
Ответ:
{
"data": {
"removeTodo": {
"id": "3",
"text": "Test Graphql 3",
"done": false
}
}
}
Теперь, если вы снова сделаете запрос, вы получите в ответ только две задачи:
Запрос:
query {
todos{
text
done,
user{
name
}
}
}
Ответ:
{
"data": {
"todos": [
{
"text": "Test Graphql 1",
"done": false,
"user": {
"name": "Elmo1"
}
},
{
"text": "Test Graphql 2",
"done": false,
"user": {
"name": "Elmo2"
}
}
]
}
}
В этом вы можете многому научиться. Gqlgen – рекомендуемый инструмент для новичков, которые хотят попробовать GraphQL в своих проектах. Он имеет приятную и простую функцию создания лесов и отличную документацию. Я добавляю другие ресурсы, которые помогут вам глубже погрузиться в это. Удачного обучения 🙂
Другие источники
- Кристофер Бискарди на Gophercon UK 2018
- Представляем gqlgen: генератор серверов GraphQL для Go
- Погружение в GraphQL от Ивана Корралеса Солера
- Пример проекта на gqlgen с Postgres от Олега Шалыгина
- Сервер Hackernews GraphQL с gqlgen от Shayegan Hooshyari
- GraphQL с Голангом