Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 3 из 25 ~20 мин +50 XP

Первый Connect-сервис

Первый Connect-сервис в самом прямом смысле: по запросу connect первый сервис поисковик ведёт на официальный hello world — болтуна ElizaService, и сейчас мы соберём свой connect go сервис от схемы до curl-ответа. Добавим к проекту из прошлого урока схему eliza.v1, сгенерируем handler, оживим его в main на net/http и проверим curl-ом; заодно выполним обещание первого урока — полностью разберём notes-сервер построчно вместе с типизированным клиентом NewNoteServiceClient.

Содержание урока · 9 разделов

Урок первый показал код Connect-сервера как плакат — целиком, без разбора, и пообещал: третий урок соберёт первый Connect-сервис целиком и разберёт каждый элемент. Обещание выполняю, но с одним поворотом: вместо сухого hello world с методом Ping мы построим hello world настоящий — ElizaService, болтуна-«психотерапевта», которого официальная документация Connect использует как первого гостя в каждом гайде. Почему именно он: метод Say принимает предложение и возвращает предложение — минимальная форма unary-вызова «запрос — ответ», ноль побочных эффектов, ноль состояния, и при этом есть живой референс на demo.connectrpc.com, к которому мы уже прикладывали curl в первом уроке. План урока такой: дописываем схему eliza.v1 в проект из прошлого урока, прогоняем buf generate, реализуем handler с ответами болтуна, монтируем его в тот же mux рядом с NoteService, проверяем curl-ом — а затем возвращаемся к notes-серверу и разбираем его по косточкам вместе с клиентом: интерфейс handler-а, встроенная Unimplemented-заглушка, конверты запросов и коды ошибок на стороне клиента.

ElizaService: hello world официальной документации#

Немного истории, потому что она объясняет, почему RPC-фреймворк шестидесятых лет не существовал, а болтун — да. ELIZA написала Джозеф Вейценбаум в MIT в 1966 году: программа переформулировала фразы пациента в вопросы и отвечала на них — примитивный имитатор психотерапевта, названный в честь Элизы Дулиттл из «Пигмалиона». Пользователи засыпали её признаниями, и этот феномен даже получил собственное имя — «эффект Элизы». Демо-сервер connectrpc.com держит образцовую реализацию connectrpc.eliza.v1.ElizaService с тремя методами: Say (unary — реплика на реплику), Introduce (тоже unary) и Converse (двунаправленный стрим). Для первого сервиса нужен только Say: в документации Connect эту пару «Say плюс curl» так и используют — это connectrpc hello world проекта, и мы воспроизведём его локально, своими руками. Обратите внимание на дисциплину схемы: service ElizaService в пакете connectrpc.eliza.v1, один rpc Say, message-типы SayRequest и SayResponse с единственным строковым полем sentence. Никаких connect-специфичных опций в схеме нет и не будет: каким протоколом клиент поедет к сервису — решается на стороне вызова, схеме всё равно.

Схема eliza/v1/eliza.proto

GO
syntax = "proto3";

package connectrpc.eliza.v1;

option go_package = "example.com/notes/gen/eliza/v1;elizav1";

message SayRequest {
  string sentence = 1;
}

message SayResponse {
  string sentence = 1;
}

service ElizaService {
  rpc Say (SayRequest) returns (SayResponse);
}

Файл кладём в proto/eliza/v1/eliza.proto — правило PACKAGE_DIRECTORY_MATCH из прошлого урока требует, чтобы путь повторял package. go_package указывает пакет elizav1 по пути example.com/notes/gen/eliza/v1: то же соглашение, что у notes.v1, только версия пакета живёт в отдельном дереве каталогов, чтобы сервисы не мешали друг другу эволюционировать. Поле sentence под номером 1 и в запросе, и в ответе — нумерация полей в protobuf обязательна, имена должны совпадать по смыслу, но живут независимо.

buf generate: те же плагины, новый пакет#

Никакой новой настройки не требуется — buf.gen.yaml из урока об установке уже описывает оба плагина и каталог gen, buf.yaml объявляет модуль path: proto, значит, новая схема в proto/eliza/v1/ попадает в модуль автоматически. buf generate соберёт четыре файла: gen/eliza/v1/eliza.pb.go с message-типами SayRequest и SayResponse от protoc-gen-go, и gen/eliza/v1/elizav1connect/eliza.connect.go от protoc-gen-connect-go — с интерфейсом ElizaServiceHandler, встроенной заглушкой UnimplementedElizaServiceHandler, конструктором NewElizaServiceHandler и типизированным клиентом NewElizaServiceClient. Обратите внимание на имя пакета: elizav1connect — суффикс connect добавляется к имени protobuf-пакета elizav1, точно так же, как notesv1connect строился от notesv1. buf lint, кстати, тоже подхватит новую схему без правок конфига — версии пакета и каталог уже сходятся. Это одна из тихих радостей buf-а: добавление сервиса в проект — это копирование файла схемы и одна команда генерации, вся конфигурация остаётся на месте.

Что именно появилось в gen/eliza/v1/elizav1connect/eliza.connect.go?

Реализация: elizaServer

GO
package main

import (
        "context"

        elizav1 "example.com/notes/gen/eliza/v1"
        elizav1connect "example.com/notes/gen/eliza/v1/elizav1connect"
)

// Реализация ElizaService — болтун уровня 1966 года.
type elizaServer struct {
        // Заглушка: новые rpc в схеме не сломают компиляцию.
        elizav1connect.UnimplementedElizaServiceHandler
}

// Классические рефлексы ELIZA: переспросить и переформулировать.
var replies = []string{
        "How long have you felt this way?",
        "Why do you say that?",
        "What would it mean if that were true?",
}

func (s *elizaServer) Say(ctx context.Context, req *elizav1.SayRequest) (*elizav1.SayResponse, error) {
        // «Анализ» детерминированный: фразу выбирает длина ввода.
        reply := replies[len(req.Sentence)%len(replies)]
        return &elizav1.SayResponse{Sentence: reply}, nil
}

Это ВСЁ, что нужно реализовать: один метод с сигнатурой, которую продиктовала схема — контекст, типизированный запрос, типизированный ответ и ошибка. Указатель на структуру, метод на указателе, встроенная заглушка — та же форма, что у gRPC-сервера, только типы из пакета elizav1. Ответ болтуна детерминированный: длина входной фразы делится по модулю на три реплики, поэтому «I feel happy.» (13 символов) всегда получает replies[1] — «Why do you say that?». Детерминизм здесь не дурной тон, а удобство тестов: curl-проверка ниже повторяема, и вы можете писать httptest-тест на точный ответ уже сегодня.

main: два сервиса на одном mux

GO
func main() {
        mux := http.NewServeMux()

        // Первый сервис: ElizaService на /connectrpc.eliza.v1.ElizaService/
        path, handler := elizav1connect.NewElizaServiceHandler(&elizaServer{})
        mux.Handle(path, handler)

        // Второй на том же порту: NoteService из уроков 01-02.
        notesPath, notesHandler := notesv1connect.NewNoteServiceHandler(&notesServer{})
        mux.Handle(notesPath, notesHandler)

        log.Fatal(http.ListenAndServe("localhost:8080", mux))
}

Форма та же, что в первом уроке, но теперь в mux живут два сервиса: каждый конструктор вернул свой путь и свой handler, каждый смонтирован одной строкой. Пути не пересекаются, потому что они выведены из package и имени сервиса: /connectrpc.eliza.v1.ElizaService/ и /notes.v1.NoteService/. Никакого реестра сервисов, никаких портов на сервис: столько mux.Handle, сколько схем. ListenAndServe — обычный net/http со всеми его последствиями: поверх этого же mux работают middleware, /healthz, статика и любые другие HTTP-маршруты вашего приложения. Структуру notesServer мы разберём ниже в полном разборе — это та же реализация из кода первого урока.

Тот же вызов, но метод набрали строчными: /say вместо /Say. Что вернёт сервер?

Прочитайте код и выберите, что он выведет:

$ curl -s -o /dev/null -w "%{http_code}" \
    -X POST http://localhost:8080/connectrpc.eliza.v1.ElizaService/say

curl-проверка: RPC без SDK

GO
$ curl -i \
    --header "Content-Type: application/json" \
    --data '{"sentence": "I feel happy."}' \
    http://localhost:8080/connectrpc.eliza.v1.ElizaService/Say

HTTP/1.1 200 OK
Content-Type: application/json

{"sentence":"Why do you say that?"}

Вот он, первый живой вызов: POST по пути из схемы, JSON-тело без обёрток и фреймов, HTTP 200 со смыслом «вызов удался». Content-Type application/json включил собственный протокол Connect — тот самый, что дружит с curl и devtools; заголовок Content-Type: application/grpc превратил бы тот же вызов в классический gRPC, но руками его не собрать, и об этом — следующий урок. Ответ повторил ввод детерминированно: 13 символов в фразе дали replies[1]. Сравните с демо из первого урока: там тот же метод Say отвечал на demo.connectrpc.com, теперь у вас такой же сервис на localhost:8080.

Анатомия вызова Say#

Элемент вызоваЧто видно на проводеОткуда берётся
Путь/connectrpc.eliza.v1.ElizaService/Saypackage + имя сервиса + имя rpc из схемы, регистрозависим
HTTP-методPOSTunary-вызовы Connect шлют POST по умолчанию
Content-Typeapplication/jsonвыбор протокола и формата: здесь собственный протокол Connect, JSON
Тело запроса{"sentence": "I feel happy."}SayRequest, сериализованный выбранным кодеком, без фреймов
HTTP-статус ответа200 OKитог unary-вызова; ошибка была бы 400/404/... по таблице кодов урока 01
Тело ответа{"sentence": "Why do you say that?"}SayResponse в том же формате, что и запрос

Полный разбор notes-сервера из первого урока#

Обещание первого урока — пора выполнять: разбираем тот файл построчно, теперь когда под рукой есть второй пример для сравнения. Начнём с интерфейса NoteServiceHandler из notes.connect.go: у него по методу на каждый rpc схемы — GetNote и CreateNote — с сигнатурой (context.Context, notesv1.GetNoteRequest) (notesv1.Note, error); типы запроса и ответа — сгенерированные message-типы, никаких обёрток. Дальше заглушка UnimplementedNoteServiceHandler: она встраивается в вашу структуру не для галочки, а ради обратной совместимости. Схема вырастет — добавится rpc DeleteNote, плагин перегенерирует интерфейс с третьим методом, и код с заглушкой продолжит компилироваться: незанятые методы ответят ошибкой unimplemented (HTTP 501 из таблицы первого урока) вместо красной сборки. Уберёте заглушку — получите ошибку компиляции при каждом расширении схемы, что честно, но болезненно. Структура notesServer — обычный Go-тип с указателем-приёмником: метод GetNote возвращает &notesv1.Note{Id: req.Id, Title: "Первая заметка"}, и здесь же место для вашей логики — хранилище в полях структуры, мьютексы, кеш, что угодно; состояние сервиса живёт в вашей структуре, а не в сгенерированном коде. Наконец main: конструктор NewNoteServiceHandler вернул строку пути и handler, mux смонтировал, ListenAndServe слушает — всё это обычный net/http, который вы писали до всякого RPC.

Зачем встраивать UnimplementedNoteServiceHandler в свою реализацию?

Клиент: NewNoteServiceClient

GO
func callNotes() {
        client := notesv1connect.NewNoteServiceClient(http.DefaultClient, "http://localhost:8080")

        res, err := client.GetNote(context.Background(),
                connect.NewRequest(&notesv1.GetNoteRequest{Id: "1"}))
        if err != nil {
                // Код ошибки RPC достаётся из err без парсинга статусов.
                if connect.IsError(err, connect.CodeNotFound) {
                        log.Print("нет такой заметки")
                        return
                }
                log.Fatal(err)
        }
        log.Print(res.Msg.Title) // Первая заметка
}

Клиент симметричен серверу: конструктор NewNoteServiceClient принимает обычный http.Client и адрес сервера, а методы повторяют rpc схемы с типизированными аргументами. connect.NewRequest — конверт, который несёт ваше сообщение плюс место под заголовки; ответ приходит конвертом connect.Response, само сообщение — в res.Msg. Ошибки: при неудаче err != nil и res == nil, а код RPC достаётся функцией connect.IsError или connect.CodeOf — никакой возни с HTTP-статусами руками. Проверьте связку: сервер из первого урока возвращает заглушку с Title «Первая заметка», и клиент печатает ровно её. Для сравнения: первый gRPC-клиент в одноимённом курсе строился вокруг grpc.Dial и долгоживущего соединения — здесь же клиенту достаточно http.Client, потому что транспорт обычный HTTP.

Разбор клиента по аргументам#

Каждый аргумент клиента — точка настройки, пройдём по ним. Первый аргумент — http.Client: можно оставить http.DefaultClient, а можно принести свой с таймаутами, транспортным слоем и джар-файлами; всё, что вы знаете про HTTP-клиент из продвинутого курса Go, применяется без адаптеров, потому что Connect ходит обычным HTTP. Адрес сервера — второй аргумент: корень без пути сервиса, конструктор сам приклеит /notes.v1.NoteService/GetNote; для локальной отладки это http://localhost:8080, в продакшене — адрес балансировщика. Третий элемент — connect.NewRequest: конверт запроса, куда кроме сообщения можно положить заголовки; дедлайн вызова задаётся не клиентом, а контекстом — context.WithTimeout вокруг вызова, ровно как в уроке про контекст продвинутого курса. Ответ: connect.Response с полем Msg — типизированный Note; при ошибке Msg не существует, есть err. И последний нюанс: клиент thread-safe, один экземпляр можно держать на всё приложение и делить между горутинами — создавать его на каждый вызов не нужно.

GetNote вернула такой ответ. Что увидит клиент после вызова client.GetNote?

Прочитайте код и выберите, что он выведет:

// Сервер, метод GetNote:
return nil, connect.NewError(connect.CodeNotFound,
        errors.New("заметка не найдена"))

Соберите вызов GetNote из клиентского кода: конструктор, метод, конверт и поле ответа.

client := notesv1connect.(http.DefaultClient, "http://localhost:8080") res, err := client.(ctx, connect.(&notesv1.GetNoteRequest{Id: "1"})) if err != nil { log.Fatal(err) } log.Print(res..Title)

Соберите путь от схемы eliza.proto до JSON-ответа в терминале — по порядку.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Где должна жить логика Say и GetNote — и что можно править в gen/?

Карточки: первый сервис

Термин

ElizaService

нажмите, чтобы перевернуть
Определение

Официальный hello world Connect: болтун connectrpc.eliza.v1 с rpc Say (unary). Демо — demo.connectrpc.com; в уроке собран свой экземпляр на localhost:8080.

1 / 6

Что дальше по курсу#

Первый сервис работает, и он уже умеет больше, чем кажется: один и тот же handler обслуживает curl с JSON, gRPC-клиентов и браузер — но пока вы верите в это на слово. Следующий урок — «Протоколы: gRPC, gRPC-Web, Connect» (04-protokoly) — спустит нас на уровень проводов: разберём, как handler выбирает протокол по Content-Type, из чего состоят фреймы gRPC, куда gRPC-Web прячет трейлеры и почему у Connect для unary-вызовов есть даже GET-кеш. Дальше по курсу: пятый урок — клиенты и их настройки, шестой — ошибки и connect.NewError в полную силу, седьмой — interceptors, девятый — браузерные клиенты на TypeScript. Домашняя работа урока небольшая, но не пропускайте её: соединить три файла руками — лучший способ запомнить, где чья ответственность.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Первый сервис — ElizaService, официальный hello world Connect: схема с rpc Say, сгенерированный handler, реализация болтуна, curl-проверка.
  • Кодогенерация не потребовала новых настроек: схема в proto/eliza/v1/, пакеты elizav1 и elizav1connect собираются теми же buf.yaml и buf.gen.yaml.
  • Реализация — структура с встроенной Unimplemented-заглушкой и методом Say; сигнатуру диктует схема, состояние живёт в ваших полях.
  • main: каждый конструктор возвращает путь и handler; два сервиса монтируются в один ServeMux и живут на localhost:8080.
  • Путь /package.Service/Method регистрозависим: /Say отвечает 200 JSON-ом, /say упадёт в 404.
  • Клиент NewNoteServiceClient(httpClient, baseURL) типизирован message-типами; запрос — connect.NewRequest, ответ — res.Msg, коды ошибок — connect.IsError.
  • В gen/ ничего не правим: логика — в вашей реализации, схема — первоисточник контракта, генерат перегенерируется.

Домашнее задание

Соберите оба сервиса в одном main: добавьте proto/eliza/v1/eliza.proto из урока, прогоните buf generate и buf lint (должны молчать), реализуйте elizaServer и смонтируйте ElizaService рядом с NoteService на localhost:8080. Проверьте curl-ом три сценария: успешный Say с фразой «I feel happy.» (ожидаем Why do you say that?), путь /say строчными (ожидаем 404) и GetNote с несуществующим id, добавив в свою реализацию возврат connect.NewError(CodeNotFound, ...) — убедитесь, что connect.IsError у клиента ловит именно этот код. Запишите в заметки, какой заголовок и какой путь использовал каждый запрос — это входной билет на следующий урок про протоколы.

Личные заметкиПусто
Хранятся только в вашем браузере (Connect · Первый Connect-сервис)

Вопросы и ответы по уроку

Как написать первый Connect-сервис на Go?

Добавить .proto-схему сервиса в proto/, прогнать buf generate, реализовать сгенерированный интерфейс (структура с Unimplemented-заглушкой), смонтировать NewElizaServiceHandler или аналогичный конструктор в http.ServeMux и запустить http.ListenAndServe. Проверка — curl с JSON по пути /package.Service/Method.

Что такое ElizaService в документации Connect?

Демонстрационный сервис connectrpc.eliza.v1 — болтун-«психотерапевт» отсылкой к ELIZA 1966 года; метод Say принимает предложение и возвращает ответ. Это официальный hello world Connect-а: живой пример на demo.connectrpc.com и первый сервис в гайдах.

Чем Connect handler отличается от gRPC-сервера?

Connect handler — обычный http.Handler: монтируется в ServeMux, работает под net/http, не требует grpc.NewServer и своего транспорта. Сигнатуры методов и message-типы те же, что у gRPC, но транспорт, регистрация и клиенты — стандартные HTTP.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Fyne25 уроков~13 чпрактика25 уроков уже доступны

Fyne: десктопные и мобильные GUI на Go

Курс Fyne: окна, виджеты, контейнеры, canvas, темы, binding, сборка под мобильные и десктоп. 25 уроков и итоговый менеджер задач.

  • Создавать окна и интерфейсы из виджетов
  • Управлять компоновкой: Box, Grid, Form
  • Рисовать примитивами canvas и строить свои виджеты
  • Связывать данные через binding