Автор: Роналдолиу, инженер-разработчик серверной части Tencent IEG
trpc-go — это среда разработки, широко используемая компанией. Она поддерживает расширение нескольких протоколов и может интегрировать функции существующих платформ различных компаний одним щелчком мыши, что очень удобно. Так как же он это делает?
trpc-go в настоящее время является очень популярной средой разработки в компании. Она объединяет множество готовых функций и очень удобна. Объем кода в trpc-go не слишком велик, но его написание все еще немного запутано, и его непосредственное чтение может сбить с толку. Поэтому в этой статье в основном рассказывается о конструкции модуля trpc-go, чтобы помочь каждому составить общее представление. При необходимости вы можете целенаправленно прочитать исходный код каждого модуля.
Студенты, занимающиеся серверной разработкой, наверняка знакомы со многими фреймворками. Возьмем, к примеру, Go. Наиболее распространенными фреймворками являются: gin beego, echo iris martini и так далее. Помимо Go, в Python также есть знаменитый Django Tornado, Laravel на PHP и Express на nodejs. Все эти знакомые фреймворки имеют общее имя — «Веб-фреймворк». Веб-фреймворк в основном ориентирован на веб-разработку, а бизнес-обработка по умолчанию — это HTTP-запросы. trpc-go немного отличается: это фреймворк RPC. По объективным историческим причинам большинство прямых взаимодействий между клиентами и серверами по-прежнему осуществляются через http-запросы, однако взаимодействия между большим количеством микросервисов внутри системы не ограничиваются протоколом http. С точки зрения производительности и читаемости такие протоколы, как thrift dubbo, явно являются лучшим выбором, чем http. Будучи совершенно новой средой разработки, trpc-go, очевидно, будет иметь очень ограниченную аудиторию, если будет привязывать только http. Поэтому наиболее важным и ключевым моментом в его конструкции является поддержка нескольких протоколов. Большая часть абстракции структуры данных в trpc-go сосредоточена на поддержке нескольких протоколов. Понимание этого может облегчить вам понимание trpc-go.
Возвращаясь к нашей повседневной работе по разработке, мы на самом деле не заботимся о базовом протоколе связи при разработке бизнес-логики. Вообще говоря, бизнес-логика в идеале начинается со следующих функций:
func handleLogin(req LoginReq) (LoginRsp, error) {
// your logic
}
Итак, с точки зрения разработчика платформы, если мы хотим предоставить пользователям такой опыт разработки, мы должны сделать следующее:
Фактически, почти все веб-фреймворки, включая стандартную библиотеку, следуют этому процессу, и это не кажется очень сложным. Но если вы хотите рассмотреть возможность поддержки нескольких протоколов, все становится проблематичным. Первый вопрос, с которым мы сталкиваемся: как платформа определяет тип протокола этого запроса из потока байтов сокета?
Идентификация протокола вещь не простая. Если сделать это хорошо, то можно продать за деньги! ! ! обратитесь к wireshark и fiddler……
Но если подумать о проблеме из практического применения, то действительно существует клиент, который использует этот протокол в соединении. A Отправлять данные по протоколу на некоторое время B ? Очевидно, что для бизнес-логики это псевдотребование! Никто бы этого не сделал. Но запросить услугу A использовать A договор, услуга B использовать B Соглашение вполне возможно. так trpc-go Была сделана первая абстракция — сервис. один trpc-go Сервисы могут содержать несколько обслуживание, каждый service Прослушивание порта требует использования протокола! Итак, для trpc-go Идентификацию делать не нужно протоколаиз,Тип протокола может быть определен при существовании Создать экземпляр службы.,Последующее кодирование и декодирование будет осуществляться с использованием этого протокола.
Действительно нужно провести Идентификацию Протоколаиз - это какой-то шлюз. Служить, поскольку для различных протоколов запросы будут перенаправляться на шлюз с определенного порта, шлюз должен выполнить Идентификацию. Затем протокол выполняет возможное преобразование и пересылку протокола. Конкретный метод выходит за рамки этой статьи. Если вам интересно, вы можете обратиться к нему. envoy и linkerd а также mosn Ждем шлюз с открытым исходным кодом.
В любом случае Идентификация протоколаэтотиметь значение,trpc-go ненужный.
Анализ протокола является важной частью. Для разных протоколов методы парсинга совершенно разные. Следовательно, на уровне фреймворка мы можем рассмотреть возможность абстрагирования интерфейс (кодек), кодирование и декодирование различных протоколов необходимо только для реализации этого interface Вот и все. Благодаря этому уровню абстракции пользователи могут использовать существующий файл конфигурации, чтобы определить, какой протокол кодировать и декодировать, а затем, когда платформа инициализируется, service Загрузите модуль кодека соответствующего протокола. Этот дизайн выглядит красиво, содержит достаточно деталей и обеспечивает возможности быстрого расширения.
Но суть дела вот в чем Codec Как должен быть спроектирован интерфейс? Кодировать и Decode Каковы входные параметры и что возвращается? Например, когда мы получаем запрос на соединение:
func (svr *Service) readLoop(c net.Conn) {
for svr.NotShutDown() {
??? = svr.Codec.Decode(c) // мы должны Что возвращает метод ожиданияDecode?
// ...
}
}
мы должны ожидать Decode Что возвращает метод? Это большая проблема! Вообще говоря, использовать в RPC Протоколы связи можно условно разделить на 3 части:
Заголовок пакета предназначен для декодера, с помощью которого можно узнать, сколько байт составляет длина всего пакета, какие байты являются заголовком, а какие телом. Следовательно, мы можем спроектировать метод Decode следующим образом:
type Decoder interface {
Decode(r io.Reader) (Header, []byte)
}
Но проблема в том, Header Как следует определить эту структуру данных? из-за разных протоколов header Все поля разные, и header Какие поля, скорее всего, будут неперечисляемыми. Таким образом, все протоколы могут быть header Нажмите оба http абстрагировано вот так map<string, []string>,Decode метод напрямую анализирует запрос Header и body。body Тип — []byte, поскольку он часто сериализуется по-разному. Например в HTTP В договоре орган может быть k1=v1&k2=v2 Этот формат также означает, что может быть{"k1":"v1","k2":"v2"}такиз JSON. Конкретный используемый метод сериализации: header поля в . существовать http Середина Тип контента. так body Анализируйте непосредственно, как есть, а затем используйте его на основе header Для выполнения различной десериализации это очень разумный метод проектирования.
Поэтому наш Header Это можно определить так: Затем предоставляется ряд общих методов, которые позволяют вызывающей стороне читать данные, а не читать их напрямую. map:
type Header struct {
data map[string][]string
}
func (h *Header) GetStatus() int {
return GetInt(h.data["status"]).or(200)
}
// ... Другие геттеры
Encode Таким же образом, в связи с необходимостью кодирования header и тело, следовательно:
type Encoder interface {
Encode(Header, []byte) []byte
}
наконецвесь Codec Его можно определить как:
type Codec interface {
Encoder
Decoder
}
Понятно Codec эта абстракция, на уровне платформы разные кодеки могут быть реализованы для разных протоколов, а затем соответствии сиспользовать Конфигурацию пользователя можно инициализировать, например:
func (svr *Service) Init(cfg Config) {
protocol := cfg.Protocol // eg: http trpc thrift
svr.Codec = codec.Get(protocol)
}
Это кажется весьма разумной абстракцией, но она не соответствует тенденции развития времени. Этот дизайн может поддерживать только такие вещи, как http1.x Такое простое соглашение можно заменить http2 этот Абстракции скоро станет недостаточноиспользовать Понятно!!
Почему этого набора абстрактных пар http2 Недостаточно? В основном из-за того, что для подключения используется мультиплексирование (мультиплексирование), давайте кратко поговорим об этом здесь.
Для начала вспомним понятие «пул соединений». пул соединений самом делето есть客户端维护Понятно很多приезжать某个Служитьиздолгое соединение,В пуле есть все свободные соединения.,Всякий раз, когда делается запрос, соединение извлекается из пула.,Подождите, пока использование не завершится, прежде чем возвращать его в пул соединений.
В это время вы задумываетесь над вопросом, почему вам каждый раз при отправке запроса приходится получать «незанятое соединение» из пула соединений? Какие проблемы это могло бы вызвать, если бы запрос был отправлен по «небездействующему соединению»?
Если несколько потоков одновременно отправляют запросы к соединению, самом С отправкой клиента проблем нет. Ключевым моментом является то, что при получении возврата он не может определить, на какой запрос он отвечает. Это самая большая проблема, поэтому у нас нет другого выбора, кроме как разрешить использование соединения взаимоисключающим образом, гарантируя, что никакой другой запрос не будет отправлен до того, как соединение получит предыдущий запрос и вернет результат. на самом деле包括 http、mysql В настоящее время большинство протоколов связи представляют собой этот единственный rping-pong режим, один запрос на один возврат, чтобы избежать частых 3 Во время рукопожатия клиент поддерживает пул соединений из нескольких соединений.
Но в последние годы было проведено много новых исследований, таких как HTTP2 и HTTP3. Когда браузер загружает страницу, ему может потребоваться множество файлов изображений и файлов js. Есть еще файлы html Файлы, если соединение установлено, могут загружаться только последовательно, что очень медленно. При параллельной загрузке необходимо установить множество соединений. Более того, в браузере повышена безопасность и ограничено максимальное количество подключений (6 индивидуальный? ), эта домашняя страница должна загружать много ресурсов, поэтому ее использование является большой проблемой. Затем кто-то начал думать о способе: использовать несколько соединений для отправки запросов, потому что для может отправлять только один запрос за раз. Если отправлено несколько запросов, конец Служить вернет несколько ответов. В настоящее время нет возможности. определить, какой ответ соответствует какому запросу. Может лучше изменить протокол и приложить streamID, ответ, возвращаемый сервером, также добавляется к соответствующему запросу StreamID, сможем ли мы его отличить?
Это на самом деле HTTP2 Основная концепция серединаиз! Так называемое измультиплексирование соединений。
Если добавлено в протокол связи streamID эквивалентен созданию множества виртуальных каналов (использовать) при физическом соединении. StreamID логотип). Таким образом, вы можете использовать одно соединение для одновременной отправки нескольких запросов. Однако оно ограничено TCP Из-за ограничений протокола, если пакет потерян в запросе, он будет передан повторно, а повторная передача заблокирует другие запросы. Эту характеристику как процветания, так и потерь можно рассматривать как http2 из Большой изъян, поэтому Понятно основано на UDP из HTTP3... Не буду здесь вдаваться в подробности.
Сказав так много о подключении из мультиплекса, на самом Дело в основном для. Пожалуйста, поймите приведенное выше определение и определение для. Codec Интерфейс сталкивается с проблемами. брать http2 Например, следующее http2 из Кадр данных:
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+-+-------------------------------------------------------------+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
На упаковке из Баотоу указана длина посылки, спинафейс также подписался Type Flag а также по одному 4 Байт из Идентификатор потока. существовать Frame Payload , разделенный на HEADERs и ДАННЫЕ,соответственно соответствующие http1.x серединаиз header и тело. Следовательно, при разборе протокола нам фактически необходимо разобрать 3 Отделение:
Кроме http2, ведь многие внутренние протоколы предприятия поддерживают мультиплексирование соединений,Например трпк:
Кромемультиплексиспользоватьнуждатьсяиспользоватьприезжатьиз StreamID, для RPC Для запросов заголовок также должен включать rpcname укажите, какой удаленный метод вы хотите настроить. Конечно, он также может содержать множество дополнительных полей, например, использовать для отслеживания ссылок из traceID、caller callee Подождите, используйте, чтобы отличить тестовую среду от онлайн-среды. EnvName……
Если вы примете во внимание это, то Header Как это следует определить? Можем ли мы продолжить в духе использования? map<string, []string>этот Определять??Это явно проблемаиз,на всякий случайиспользоватьсамопередачаиз key Конфликт управляющих слов? И это Header Анализируемый результат — это не просто запрос Дават. Когда бизнес-логика выполняется и запрос возвращается клиенту Дават, сериализация также требует этого. Заголовок, запрошенный для для мультиплексного использования, как минимум StreamID получить и запросить из StreamID То же самое, да? только гарантия StreamID Может ли быть то же самое? Трудно сказать, кто знает, что сделают частные протоколы? так Header Как его спроектировать — действительно большая проблема
Для решения этой проблемы используйте trpc-go Решение состоит в том, чтобы изолировать некоторые общие управляющие слова, другие поля и разные протоколы. Codec Если нужно, вставьте metadata Здесь эффект финального выглядит следующим образом (для упрощенного кода используйте Вместо этого С#):
class Msg {
public string envName {get; set;}
public HashTable<string, []string> metadata {get; set;}
public string rpcname {get; set;}
public string caller {get; set;}
public string callee {get; set;}
// ...
}
финальный trpc-go из Codec Определение следующее:
// Codec Интерфейс распаковки бизнес-протокола, Деловой договор разделен на голову и основную часть.
// Здесь анализируется только двоичное тело, а конкретная структура бизнес-тела обрабатывается сериализатором.
// Как правило, тело pb json jce и т. д. В особых случаях предприятие может зарегистрировать сериализатор самостоятельно.
type Codec interface {
Encode(message Msg, body []byte) (buffer []byte, err error)
Decode(message Msg, buffer []byte) (body []byte, err error)
}
здесь из Msg это огромное из интерфейс, всего существует 69 метод. Однако чем больше interface Его способность абстрагировать на самом деле слабее, здесь имеется в виду использовать interface на самом делеииспользовать struct Разницы больше нет. интерфейс Суть за динамическое распределение, но в условиях необходимости реализации 69 метод interface,Почти ни один разработчик не способен реализовать абстракцию для многопротокольных расширений.,Я предпочитаю муравей с открытым исходным кодомmosnиздизайн。Но опять же,trpc-go Эта конструкция на самом деле оказалась использованной, возможно, that's enough
На основании этого Codec из дизайна, для Реализовать функции кодирования и декодирования с использованием различных протоколов относительно легко. При разборе, если именно Msg После определения соответствующих полей вы можете установить их напрямую. Msg, если не определено, выдается в Msg В структуре «использовать» последующие шаги требуют существования type cast。
Codec Он отвечает только за кодирование и декодирование протокола. Transport Понятие из также важно. Я уже упоминал об этом, хотя http2 относительно http1.x это огромное из прогресса,Но у него все еще есть свои недостатки. Поэтому Понятно позже из http3(QUIC). Благодаря своей превосходной производительности, которая особенно подходит для сценариев с высокой скоростью передачи данных в реальном времени, этот протокол был внедрен в производственные среды многими производителями коротких видео. Но проблема в том, http3 не основано на TCP протокольно, но на основании UDP из. потому что trpc-go Чтобы иметь возможность адаптироваться к различным протоколам, затем на основе UDP из также необходимо учитывать.
в целомиз TCP Есть одна услуга AcceptLoop, например:
func ListenAndServe(addr) {
fd, _ := net.Listen("tcp", addr)
for {
conn, err := fd.Accept()
setting(conn)
go serveConn(conn)
}
}
существовать TCP В Служить, из-за «концепции соединения», нам нужно каждый раз получать новое соединение на существующем дескрипторе прослушивания. Итак, понятно выше из Принять цикл. но UDP Понятие соединения отсутствует. Данные считываются сразу после прослушивания, поэтому его нет. AceeptLoop из. поэтому trpc-go представил transport изAbstract, разные протоколы (udp tcp) имеет себя из транспорта, каждый реализует свой из ListenAndServe:
// ServerTransport интерфейс уровня связи сервера
type ServerTransport interface {
ListenAndServe(ctx context.Context, opts ...ListenServeOption) error
}
проходить Transport и Codec Эти два уровня абстракции, trpc-go Он может в основном адаптироваться к различным протоколам сетевой связи, будь то UDP все еще ПТС, будь то Мультиплексирование может поддерживаться. При развитии бизнеса не нужно заботиться о нижележащем слое из Кодек. протокола Функция,Great!
принимая во внимание Codec.Decode Это также относительно трудоемкий процесс, поэтому trpc-go существовать transport Дополнительный Framer абстрактный:
// FramerBuilder Обычно для каждого соединения создается один Framer.
type FramerBuilder interface {
New(io.Reader) Framer
}
// Framer Чтение и запись кадров данных
type Framer interface {
ReadFrame() ([]byte, error)
}
Ituse используется для вырезания полного пакета запроса из сети.,Но никакого дальнейшего анализа проводиться не будет. Это позволяет быстро прочитать заявку и всю посылку.,Затем основной цикл может продолжить чтение следующего пакета.,И пусть время отнимаетиз Codec.Decode Можетсуществоватьновыйиз goroutine В процессе, распараллеливаем! если не фреймер, тогда придется подождать codec.Decode После завершения вы можете продолжить обработку следующего пакета. Это небольшая оптимизация!
Вышеупомянутое в основном trpc-go обеспечивает абстракцию уровня протокола. Благодаря этому уровню абстракции последующий бизнес-код не должен заботиться о деталях базового протокола связи.
Протокол связи в основном предназначен для лучшей передачи данных, но каковы конкретные данные? В настоящее время мы все используем[]byte Такой поток байтов, который нужно сохранить. На самом деле существуют различные способы получения бизнес-данных, существуют только http В соглашении распространенными методами искодирования являются:
Помимо http из этих методов сериализации мы часто также включаем:
Serializer изделатьиспользовать Просто поставь[]byte Десериализуйте его в конкретный объект структуры, чтобы облегчить бизнес-обработку. И соглашение из payload Конкретный метод сериализации и десериализации обычно указывается в заголовке кадра запроса. поэтому Codec Когда существование необходимо для разбора запроса на настройку Msg из SerializeType,Последующая обработка SerializeType находит соответствующий инструмент сериализации.,Выполните реальную сериализацию.
Здесь есть один KM Резюме моего коллеги представляет собой очень подробную изблок-схему (автор: @smallhowcao):
После разговора об общем процессе, на самом деле trpc-go Я почти все понял. Оставшееся более важно и труднее для понимания. Filter。Filter На первый взгляд это не сложно, на самом деле это разные вещи Web в кадре middleware модель. но trpc-go существуют, что следует учитывать при проектировании pb генерироватьиз service handler Сигнатура функции из-за неопределенности делает все middleware Цепочка выглядит немного неуклюже.
Шаблон промежуточного программного обеспечения похож на луковицу:
Запросы принимаются в первую очередь обработка фильтра (промежуточного программного обеспечения), а затем передача ее по очереди. фильтр, наконец и Давать обработку бизнес-логики. После обработки удалите его с внутреннего слоя. filter Возвращаемся на внешний слой слой за слоем, наконец возвращаемся к клиенту Дават. Этот режим самом делето есть trpc-go В документе сказано, что из поддерживается существующая бизнес-логика до и после Крючок, это можно понять, посмотрев на эту диаграмму. Запрос сначала поступает в лук слой за слоем, а потом слой за слоем выходит. Логика может быть написана в обоих местах, это так называемый из. hook。
Определение и подпись фильтра Дават trpc-go следующие:
type Filter func(ctx context.Context, req interface{}, rsp interface{}, f HandleFunc) (err error)
// HandleFunc Интерфейс функции фильтра (перехватчика)
type HandleFunc func(ctx context.Context, req interface{}, rsp interface{}) (err error)
При инициализации существования фреймворка пользовательская конфигурация из фильтра Нажмите оба последовательно сохраняется в массив:
// WithFilters Добавлен перехватчик «Служить конец» для поддержки существования. До и после функции обработки бизнес-обработчика Обработка перехвата
func WithFilters(fs []filter.Filter) Option {
return func(o *Options) {
o.Filters = append(o.Filters, fs...)
}
}
для filter из сигнатуры функции, на самом деле ctx req rsp Это все легко понять, ключ наконецэтот f Что это такое? на самом деле f Это означает filter Следующий в цепочке фильтр, который является следующим слоем лука на картинке. Например, напишем простейшее из filter:
func CostFilter(ctx context.Context, req interface{}, rsp interface{}, f HandleFunc) (err error) {
start:= time.Now()
err = f(ctx, req, rsp)
cost := time.Sub(start)
fmt.Println("Запрос выполнен для: %d миллисекунд", cost.Milliseconds())
return err
}
этот filter Запишите время начала, а затем непосредственно выполните следующее: фильтр. Следовать за filter Он будет рекурсивно переведен на следующий уровень, пока бизнес обработчик...наконец возвращает слой за слоем вверх. ждать f После выполнения он эквивалентен всем последующим filter и业务逻辑都执行完Понятно,Теперь посчитайте разницу во времени,Это время, необходимое для обработки этого запроса.
этот f Это кажется немного волшебным, оно может сделать целое filter Цепочка связана звено за звеном, как этого добиться? Вообще говоря onion Режим из middleware Существует два метода реализации: trpc-go Метод майнинга представляет собой нисходящий подход:
// Chain цепной фильтр
type Chain []Filter
// Handle цепной поток рекурсивной обработки фильтра
func (fc Chain) Handle(ctx context.Context, req interface{}, rsp interface{}, f HandleFunc) (err error) {
n := len(fc)
curI := -1
// Несколько фильтров, выполняемых рекурсивно
var chainFunc HandleFunc
chainFunc = func(ctx context.Context, req interface{}, rsp interface{}) error {
if curI == n-1 {
return f(ctx, req, rsp)
}
curI++
return fc[curI](ctx, req, rsp, chainFunc)
}
return chainFunc(ctx, req, rsp)
}
Handle из f на самом желе - это центр луковицы - бизнес-логика, это существующее использование Handle Время устанавливается рамками. Затем Handle Внутри находится рекурсивное замыкание, если все filter Как только все будет выполнено, выполните его е, если есть другой filter Просто выполните отфильтровать и настроить себя на выполнение filter из f функция……
Реализация здесь кажется немного запутанной, главным образом потому, что Filter из Подпись функции HandleFunc сигнатура функции противоречива, поэтому требуется chainFunc Приходите и заверните это. существуют Во многих других структурах фильтровать и handler Подпись соответствует,В настоящее время наблюдается лучшее понимание и реализация,Может参见:chi
trpc-go Еще одним важным понятием в из является Плагин, предназначенный для различных плагин, мы из trpc-go Служить позволяет беспрепятственно соединить различные системы компании, а для достижения требуемых функций необходима всего одна функция. "_ import pkg» подойдет.
на самом деле Plugin и trpc-go Он относительно слабо связан, или, можно сказать, «в принципе не имеет значения». Однако очень важной проблемой является то, что большинство плагинов необходимо настроить для их использования, например log metrics 007 и т. д. если plugin и trpc-go Полная изоляция, затем для Создать экземпляр Эти плагины могут храниться в нашем проекте. N разные файлы конфигурации. Чтобы упростить управление конфигурацией, пользователям необходимо создать несколько колесиков для объединения этих конфигураций в один файл для упрощения управления. Это на самом деле trpc-go Рамка и plugin из отношений.
trpc-go Формат конфигурации согласован, настройка плагина Нажмите @Этот формат настроен на trpc.yaml в, тогда trpc-go в соответствии с Имя плагина, чтобы найти соответствующий плагин, а затем отделить плагин от конфигурации, использовать эту часть небольшой конфигурации для создания. экземпляр плагина. Единственное ограничение на плагин заключается в том, что плагин должен поддерживать передачу yaml Конфигурация формата, как для yaml Конфигурация формата полностью настраивается.
В частности, плагин Конфигурациясуществовать trpc-go сохранить как type Config map[string]map[string]yaml.Node
Фактическая конфигурация выглядит следующим образом:
trpc-go это будет пройдено во время существования подключаемого модуля инициализации платформы. Config Настройте, а затем проверьте, есть ли init Зарегистрирован Type для pluginType,name для pluginName из плагина, если есть вызов, используйте конструктор плагина из Setup Метод, поставьте следующее, соответствующее комку yaml Конфигурация扔Даватьэто,Позаботьтесь об этом сами. Таким образом, очень легко реализовать существующий плагин.,Потому что конфигурация, которую он получает, управляема сама по себе.
И все конфигурации объединены в trpc.yaml файл, очень прост в управлении. Еще одним преимуществом является то, что для этого trpc.yaml файле, вы можете предварительно задать некоторые системные переменные в контейнере Дават, например {port], ..., а затем платформа развертывания заменяет эти переменные в соответствии с средой контейнера.
существования В своей предыдущей работе мне также приходилось много работать над разработкой фреймворков, но я чувствую trpc-go Это действительно превосходно спроектированный фреймворк (хотя некоторые реализации кода недостаточно хороши, все они представляют собой незначительные проблемы и могут быть исправлены медленно). Он очень масштабируем и имеет очень мощную экосистему. Его можно легко комбинировать с различными системами мониторинга, ведения журналов и публикации для взаимодействия. 123 Платформа с возможностями для развития бизнеса оказывает большую помощь!
Эта статья - всего лишь мое личное мнение trpc-go из Некоторое понимание, пожалуйста, поправьте меня, если есть какие-либо ошибки.