[{"data":1,"prerenderedAt":2253},["ShallowReactive",2],{"content:\u002F07-rpc-grpc\u002F02-protobuf":3},{"title":4,"description":5,"path":6,"body":7},"Protobuf: схема, сообщения и совместимость","Protocol Buffers - это язык описания данных и бинарный формат сериализации. В gRPC protobuf обычно играет роль контракта: в .proto файле описываются сервисы, методы, request\u002Fresponse сообщения и правила совместимости.","\u002F07-rpc-grpc\u002F02-protobuf",{"type":8,"value":9,"toc":2223},"minimark",[10,14,28,34,41,47,78,81,88,258,261,267,275,282,287,295,302,308,316,329,331,336,341,380,383,421,427,442,456,458,462,465,577,595,601,619,622,625,658,668,672,683,686,709,722,725,743,746,748,758,782,790,804,807,816,819,828,842,844,848,851,854,868,871,891,894,932,935,938,982,989,993,996,999,1013,1016,1018,1022,1025,1044,1053,1061,1064,1077,1083,1085,1089,1092,1151,1163,1166,1176,1181,1215,1221,1230,1235,1244,1250,1257,1271,1274,1276,1280,1283,1286,1397,1407,1410,1435,1441,1443,1447,1450,1453,1470,1477,1479,1483,1489,1537,1540,1542,1549,1552,1668,1671,1704,1706,1710,1743,1745,1749,1781,1783,1787,1821,2026,2219],[11,12,4],"h1",{"id":13},"protobuf-схема-сообщения-и-совместимость",[15,16,17,18,22,23,27],"p",{},"Protocol Buffers - это язык описания данных и бинарный формат сериализации. В gRPC protobuf обычно играет роль ",[19,20,21],"strong",{},"контракта",": в ",[24,25,26],"code",{},".proto"," файле описываются сервисы, методы, request\u002Fresponse сообщения и правила совместимости.",[15,29,30,31,33],{},"Если в REST контракт часто живёт в OpenAPI, README и договорённостях, то в gRPC ",[24,32,26],{}," становится исходником, из которого генерируются Go-типы, клиент и серверный интерфейс.",[15,35,36,37,40],{},"В production проблема не в том, чтобы \"сериализовать быстрее JSON\". Проблема в том, что сервисы деплоятся независимо. Сегодня новый сервер может получить запрос от старого клиента, завтра новый клиент может читать ответ от старого сервера, а часть трафика может идти через разные версии одновременно. Хорошая protobuf-схема помогает пережить такой rollout без массовых ",[24,38,39],{},"Internal",", silent data loss и ручных договорённостей в чатах.",[15,42,43,44,46],{},"Наивный ",[24,45,26],{}," ломается не сразу. Он ломается позже:",[48,49,50,54,69,72,75],"ul",{},[51,52,53],"li",{},"когда номер удалённого поля заняли новым смыслом, и старые клиенты начали отправлять \"валидные\" байты не туда;",[51,55,56,57,60,61,64,65,68],{},"когда ",[24,58,59],{},"string status"," разъехался между сервисами: один пишет ",[24,62,63],{},"published",", другой ждёт ",[24,66,67],{},"PUBLISHED",";",[51,70,71],{},"когда отсутствие поля нельзя отличить от нулевого значения;",[51,73,74],{},"когда в схему попали internal\u002Fdebug поля, PII или transport metadata, которые потом нельзя безопасно удалить;",[51,76,77],{},"когда контракт повторяет domain entity один в один и каждое внутреннее изменение домена становится breaking change для клиентов.",[79,80],"hr",{},[82,83,85,86],"h2",{"id":84},"первый-proto","Первый ",[24,87,26],{},[89,90,95],"pre",{"className":91,"code":92,"language":93,"meta":94,"style":94},"language-proto shiki shiki-themes github-dark","syntax = \"proto3\";\n\npackage calculator.v1;\n\noption go_package = \"example.com\u002Fgrpc-demo\u002Fgen\u002Fcalculator\u002Fv1;calculatorv1\";\n\nservice CalculatorService {\n  rpc Add(AddRequest) returns (AddResponse);\n  rpc Divide(DivideRequest) returns (DivideResponse);\n}\n\nmessage AddRequest {\n  int64 a = 1;\n  int64 b = 2;\n}\n\nmessage AddResponse {\n  int64 result = 1;\n}\n\nmessage DivideRequest {\n  int64 dividend = 1;\n  int64 divisor = 2;\n}\n\nmessage DivideResponse {\n  double result = 1;\n}\n","proto","",[24,96,97,105,112,118,123,129,134,140,146,152,158,163,169,175,181,186,191,197,203,208,213,219,225,231,236,241,247,253],{"__ignoreMap":94},[98,99,102],"span",{"class":100,"line":101},"line",1,[98,103,104],{},"syntax = \"proto3\";\n",[98,106,108],{"class":100,"line":107},2,[98,109,111],{"emptyLinePlaceholder":110},true,"\n",[98,113,115],{"class":100,"line":114},3,[98,116,117],{},"package calculator.v1;\n",[98,119,121],{"class":100,"line":120},4,[98,122,111],{"emptyLinePlaceholder":110},[98,124,126],{"class":100,"line":125},5,[98,127,128],{},"option go_package = \"example.com\u002Fgrpc-demo\u002Fgen\u002Fcalculator\u002Fv1;calculatorv1\";\n",[98,130,132],{"class":100,"line":131},6,[98,133,111],{"emptyLinePlaceholder":110},[98,135,137],{"class":100,"line":136},7,[98,138,139],{},"service CalculatorService {\n",[98,141,143],{"class":100,"line":142},8,[98,144,145],{},"  rpc Add(AddRequest) returns (AddResponse);\n",[98,147,149],{"class":100,"line":148},9,[98,150,151],{},"  rpc Divide(DivideRequest) returns (DivideResponse);\n",[98,153,155],{"class":100,"line":154},10,[98,156,157],{},"}\n",[98,159,161],{"class":100,"line":160},11,[98,162,111],{"emptyLinePlaceholder":110},[98,164,166],{"class":100,"line":165},12,[98,167,168],{},"message AddRequest {\n",[98,170,172],{"class":100,"line":171},13,[98,173,174],{},"  int64 a = 1;\n",[98,176,178],{"class":100,"line":177},14,[98,179,180],{},"  int64 b = 2;\n",[98,182,184],{"class":100,"line":183},15,[98,185,157],{},[98,187,189],{"class":100,"line":188},16,[98,190,111],{"emptyLinePlaceholder":110},[98,192,194],{"class":100,"line":193},17,[98,195,196],{},"message AddResponse {\n",[98,198,200],{"class":100,"line":199},18,[98,201,202],{},"  int64 result = 1;\n",[98,204,206],{"class":100,"line":205},19,[98,207,157],{},[98,209,211],{"class":100,"line":210},20,[98,212,111],{"emptyLinePlaceholder":110},[98,214,216],{"class":100,"line":215},21,[98,217,218],{},"message DivideRequest {\n",[98,220,222],{"class":100,"line":221},22,[98,223,224],{},"  int64 dividend = 1;\n",[98,226,228],{"class":100,"line":227},23,[98,229,230],{},"  int64 divisor = 2;\n",[98,232,234],{"class":100,"line":233},24,[98,235,157],{},[98,237,239],{"class":100,"line":238},25,[98,240,111],{"emptyLinePlaceholder":110},[98,242,244],{"class":100,"line":243},26,[98,245,246],{},"message DivideResponse {\n",[98,248,250],{"class":100,"line":249},27,[98,251,252],{},"  double result = 1;\n",[98,254,256],{"class":100,"line":255},28,[98,257,157],{},[15,259,260],{},"Разберём элементы.",[262,263,265],"h3",{"id":264},"syntax",[24,266,264],{},[89,268,269],{"className":91,"code":104,"language":93,"meta":94,"style":94},[24,270,271],{"__ignoreMap":94},[98,272,273],{"class":100,"line":101},[98,274,104],{},[15,276,277,278,281],{},"Указывает версию языка protobuf. В новом Go-коде почти всегда используют ",[24,279,280],{},"proto3",".",[262,283,285],{"id":284},"package",[24,286,284],{},[89,288,289],{"className":91,"code":117,"language":93,"meta":94,"style":94},[24,290,291],{"__ignoreMap":94},[98,292,293],{"class":100,"line":101},[98,294,117],{},[15,296,297,298,301],{},"Это protobuf namespace. Он защищает от конфликтов имён между схемами. Версия в package (",[24,299,300],{},"v1",") помогает явно развивать контракт.",[262,303,305],{"id":304},"option-go_package",[24,306,307],{},"option go_package",[89,309,310],{"className":91,"code":128,"language":93,"meta":94,"style":94},[24,311,312],{"__ignoreMap":94},[98,313,314],{"class":100,"line":101},[98,315,128],{},[15,317,318,319,321,322,324,325,328],{},"Это Go-specific настройка генерации. До ",[24,320,68],{}," указывается import path, после ",[24,323,68],{}," - имя Go package. Без ",[24,326,327],{},"go_package"," генератору сложнее корректно разложить Go-код.",[79,330],{},[82,332,334],{"id":333},"message",[24,335,333],{},[15,337,338,340],{},[24,339,333],{}," похож на DTO:",[89,342,344],{"className":91,"code":343,"language":93,"meta":94,"style":94},"message Lesson {\n  string slug = 1;\n  string title = 2;\n  string body_markdown = 3;\n  int32 estimated_minutes = 4;\n  repeated string tags = 5;\n}\n",[24,345,346,351,356,361,366,371,376],{"__ignoreMap":94},[98,347,348],{"class":100,"line":101},[98,349,350],{},"message Lesson {\n",[98,352,353],{"class":100,"line":107},[98,354,355],{},"  string slug = 1;\n",[98,357,358],{"class":100,"line":114},[98,359,360],{},"  string title = 2;\n",[98,362,363],{"class":100,"line":120},[98,364,365],{},"  string body_markdown = 3;\n",[98,367,368],{"class":100,"line":125},[98,369,370],{},"  int32 estimated_minutes = 4;\n",[98,372,373],{"class":100,"line":131},[98,374,375],{},"  repeated string tags = 5;\n",[98,377,378],{"class":100,"line":136},[98,379,157],{},[15,381,382],{},"После генерации в Go появится структура с getters:",[89,384,388],{"className":385,"code":386,"language":387,"meta":94,"style":94},"language-go shiki shiki-themes github-dark","lesson.GetSlug()\nlesson.GetTitle()\nlesson.GetTags()\n","go",[24,389,390,403,412],{"__ignoreMap":94},[98,391,392,396,400],{"class":100,"line":101},[98,393,395],{"class":394},"s95oV","lesson.",[98,397,399],{"class":398},"svObZ","GetSlug",[98,401,402],{"class":394},"()\n",[98,404,405,407,410],{"class":100,"line":107},[98,406,395],{"class":394},[98,408,409],{"class":398},"GetTitle",[98,411,402],{"class":394},[98,413,414,416,419],{"class":100,"line":114},[98,415,395],{"class":394},[98,417,418],{"class":398},"GetTags",[98,420,402],{"class":394},[15,422,423,424,281],{},"В protobuf важны не только имена, но и ",[19,425,426],{},"номера полей",[89,428,430],{"className":91,"code":429,"language":93,"meta":94,"style":94},"string slug = 1;\nstring title = 2;\n",[24,431,432,437],{"__ignoreMap":94},[98,433,434],{"class":100,"line":101},[98,435,436],{},"string slug = 1;\n",[98,438,439],{"class":100,"line":107},[98,440,441],{},"string title = 2;\n",[15,443,444,445,448,449,452,453,455],{},"Номер поля - часть бинарного контракта. Если клиент отправляет поле ",[24,446,447],{},"1",", сервер должен понимать, что это всё ещё ",[24,450,451],{},"slug",". Переименовать поле в ",[24,454,26],{}," иногда можно без поломки wire-формата, а вот переиспользовать номер под другой смысл нельзя.",[79,457],{},[82,459,461],{"id":460},"базовые-типы","Базовые типы",[15,463,464],{},"Частые типы:",[466,467,468,481],"table",{},[469,470,471],"thead",{},[472,473,474,478],"tr",{},[475,476,477],"th",{},"Protobuf",[475,479,480],{},"Go",[482,483,484,496,507,518,529,541,553,565],"tbody",{},[472,485,486,492],{},[487,488,489],"td",{},[24,490,491],{},"string",[487,493,494],{},[24,495,491],{},[472,497,498,503],{},[487,499,500],{},[24,501,502],{},"bool",[487,504,505],{},[24,506,502],{},[472,508,509,514],{},[487,510,511],{},[24,512,513],{},"int32",[487,515,516],{},[24,517,513],{},[472,519,520,525],{},[487,521,522],{},[24,523,524],{},"int64",[487,526,527],{},[24,528,524],{},[472,530,531,536],{},[487,532,533],{},[24,534,535],{},"double",[487,537,538],{},[24,539,540],{},"float64",[472,542,543,548],{},[487,544,545],{},[24,546,547],{},"bytes",[487,549,550],{},[24,551,552],{},"[]byte",[472,554,555,560],{},[487,556,557],{},[24,558,559],{},"repeated T",[487,561,562],{},[24,563,564],{},"[]T",[472,566,567,572],{},[487,568,569],{},[24,570,571],{},"map\u003CK, V>",[487,573,574],{},[24,575,576],{},"map[K]V",[15,578,579,580,582,583,586,587,590,591,594],{},"Тип в ",[24,581,26],{}," - это формат передачи, а не гарантия бизнес-валидности. ",[24,584,585],{},"int64 amount = 1"," не говорит, что сумма положительная. ",[24,588,589],{},"string currency = 2"," не говорит, что это ISO 4217. ",[24,592,593],{},"google.protobuf.Timestamp"," не говорит, что время не из будущего. Эти правила должны проверяться в transport\u002Fapplication слое, а не предполагаться из самого protobuf.",[15,596,597,598,600],{},"Для финансовых значений не используйте ",[24,599,535],{},": бинарная floating-point арифметика плохо подходит для точных денег, комиссий и курсов, которые потом сравниваются, округляются и аудируются. Обычно выбирают один из вариантов:",[48,602,603,610,616],{},[51,604,605,606,609],{},"minor units: ",[24,607,608],{},"int64 amount_minor = 1",", например копейки или центы;",[51,611,612,613,68],{},"decimal как строка с явным форматом: ",[24,614,615],{},"string rate = 1",[51,617,618],{},"отдельный message с units\u002Fnanos, если команда готова поддерживать такой формат.",[15,620,621],{},"Главное - описать scale, rounding policy и допустимый диапазон рядом с полем или в документации контракта. Иначе два корректных protobuf-клиента могут считать одну и ту же сумму по-разному.",[15,623,624],{},"Для времени обычно используют well-known type:",[89,626,628],{"className":91,"code":627,"language":93,"meta":94,"style":94},"import \"google\u002Fprotobuf\u002Ftimestamp.proto\";\n\nmessage ProgressEvent {\n  string lesson_slug = 1;\n  google.protobuf.Timestamp happened_at = 2;\n}\n",[24,629,630,635,639,644,649,654],{"__ignoreMap":94},[98,631,632],{"class":100,"line":101},[98,633,634],{},"import \"google\u002Fprotobuf\u002Ftimestamp.proto\";\n",[98,636,637],{"class":100,"line":107},[98,638,111],{"emptyLinePlaceholder":110},[98,640,641],{"class":100,"line":114},[98,642,643],{},"message ProgressEvent {\n",[98,645,646],{"class":100,"line":120},[98,647,648],{},"  string lesson_slug = 1;\n",[98,650,651],{"class":100,"line":125},[98,652,653],{},"  google.protobuf.Timestamp happened_at = 2;\n",[98,655,656],{"class":100,"line":131},[98,657,157],{},[15,659,660,661,664,665,281],{},"В Go это будет ",[24,662,663],{},"*timestamppb.Timestamp",", который можно конвертировать из ",[24,666,667],{},"time.Time",[262,669,671],{"id":670},"presence-и-zero-value-в-proto3","Presence и zero value в proto3",[15,673,674,675,678,679,682],{},"В proto3 scalar-поля по умолчанию имеют zero value: пустая строка, ",[24,676,677],{},"0",", ",[24,680,681],{},"false",". Это удобно для простых DTO, но опасно, когда бизнесу важно различать \"клиент не прислал значение\" и \"клиент явно прислал ноль\".",[15,684,685],{},"Например:",[89,687,689],{"className":91,"code":688,"language":93,"meta":94,"style":94},"message UpdateLessonRequest {\n  string slug = 1;\n  int32 estimated_minutes = 2;\n}\n",[24,690,691,696,700,705],{"__ignoreMap":94},[98,692,693],{"class":100,"line":101},[98,694,695],{},"message UpdateLessonRequest {\n",[98,697,698],{"class":100,"line":107},[98,699,355],{},[98,701,702],{"class":100,"line":114},[98,703,704],{},"  int32 estimated_minutes = 2;\n",[98,706,707],{"class":100,"line":120},[98,708,157],{},[15,710,711,712,715,716,718,719,721],{},"В Go ",[24,713,714],{},"req.GetEstimatedMinutes()"," вернёт ",[24,717,677],{}," и для отсутствующего поля, и для явно переданного ",[24,720,677],{},". Для update\u002Fpatch-методов это часто ломает смысл операции.",[15,723,724],{},"Есть несколько вариантов:",[48,726,727,730,737,740],{},[51,728,729],{},"сделать отдельный RPC с явным действием вместо универсального patch;",[51,731,732,733,736],{},"использовать ",[24,734,735],{},"optional"," для scalar-поля, если нужно presence;",[51,738,739],{},"использовать wrapper\u002Fwell-known types там, где это принято в проекте;",[51,741,742],{},"вынести изменяемые поля в отдельный message и явно договориться о semantics.",[15,744,745],{},"Выбор зависит от контракта. Важно не притворяться, что zero value всегда означает валидное бизнес-значение.",[79,747],{},[82,749,751,754,755],{"id":750},"service-и-rpc",[24,752,753],{},"service"," и ",[24,756,757],{},"rpc",[89,759,761],{"className":91,"code":760,"language":93,"meta":94,"style":94},"service CourseService {\n  rpc GetLesson(GetLessonRequest) returns (GetLessonResponse);\n  rpc ListLessons(ListLessonsRequest) returns (stream Lesson);\n}\n",[24,762,763,768,773,778],{"__ignoreMap":94},[98,764,765],{"class":100,"line":101},[98,766,767],{},"service CourseService {\n",[98,769,770],{"class":100,"line":107},[98,771,772],{},"  rpc GetLesson(GetLessonRequest) returns (GetLessonResponse);\n",[98,774,775],{"class":100,"line":114},[98,776,777],{},"  rpc ListLessons(ListLessonsRequest) returns (stream Lesson);\n",[98,779,780],{"class":100,"line":120},[98,781,157],{},[15,783,784,786,787,789],{},[24,785,753],{}," описывает группу удалённых методов. ",[24,788,757],{}," описывает конкретный метод. В generated Go-коде появятся:",[48,791,792,795,798,801],{},[51,793,794],{},"client interface;",[51,796,797],{},"server interface;",[51,799,800],{},"функция регистрации сервера;",[51,802,803],{},"типы stream-клиентов и stream-серверов.",[15,805,806],{},"Пример unary метода:",[89,808,810],{"className":91,"code":809,"language":93,"meta":94,"style":94},"rpc GetLesson(GetLessonRequest) returns (GetLessonResponse);\n",[24,811,812],{"__ignoreMap":94},[98,813,814],{"class":100,"line":101},[98,815,809],{},[15,817,818],{},"Пример server-side streaming:",[89,820,822],{"className":91,"code":821,"language":93,"meta":94,"style":94},"rpc ListLessons(ListLessonsRequest) returns (stream Lesson);\n",[24,823,824],{"__ignoreMap":94},[98,825,826],{"class":100,"line":101},[98,827,821],{},[15,829,830,831,834,835,838,839,281],{},"Слово ",[24,832,833],{},"stream"," меняет сигнатуру generated Go-кода: вместо обычного response вы получаете stream с методами ",[24,836,837],{},"Send"," или ",[24,840,841],{},"Recv",[79,843],{},[82,845,847],{"id":846},"совместимость-схем","Совместимость схем",[15,849,850],{},"Главное правило: старый клиент и новый сервер должны уметь пережить постепенный rollout.",[15,852,853],{},"Можно:",[48,855,856,859,862,865],{},[51,857,858],{},"добавлять новое поле с новым номером;",[51,860,861],{},"перестать использовать поле, но оставить его номер занятым;",[51,863,864],{},"добавлять новый RPC метод;",[51,866,867],{},"добавлять новое значение enum аккуратно, если клиенты готовы к неизвестному значению.",[15,869,870],{},"Нельзя:",[48,872,873,876,879,882,885],{},[51,874,875],{},"переиспользовать номер удалённого поля;",[51,877,878],{},"менять смысл существующего поля;",[51,880,881],{},"менять тип поля на несовместимый;",[51,883,884],{},"удалять RPC, которым ещё пользуются клиенты;",[51,886,887,888,890],{},"считать, что порядок полей в ",[24,889,26],{}," важнее их номеров.",[15,892,893],{},"Совместимость бывает не только бинарной. Wire-формат может не сломаться, но поведение всё равно станет несовместимым:",[48,895,896,909,919,926,929],{},[51,897,898,899,901,902,905,906,68],{},"поле осталось ",[24,900,491],{},", но формат поменялся с ",[24,903,904],{},"RUB"," на ",[24,907,908],{},"643",[51,910,911,912,915,916,918],{},"поле ",[24,913,914],{},"amount_minor"," осталось ",[24,917,524],{},", но scale поменялся с копеек на рубли;",[51,920,921,922,925],{},"enum получил новое значение, а старый клиент считает ",[24,923,924],{},"default"," успешным состоянием;",[51,927,928],{},"сервер начал требовать поле, которое старые клиенты не отправляют;",[51,930,931],{},"response перестал заполнять поле, на которое завязана логика клиента.",[15,933,934],{},"Поэтому \"не поменяли номер поля\" - это только минимальная техническая защита. Для production-контракта нужно описывать смысл, единицы измерения, формат строк, диапазоны и политику миграции.",[15,936,937],{},"Если поле удалили, номер и имя лучше зарезервировать:",[89,939,941],{"className":91,"code":940,"language":93,"meta":94,"style":94},"message Lesson {\n  reserved 4;\n  reserved \"legacy_duration\";\n\n  string slug = 1;\n  string title = 2;\n  string body_markdown = 3;\n  int32 estimated_minutes = 5;\n}\n",[24,942,943,947,952,957,961,965,969,973,978],{"__ignoreMap":94},[98,944,945],{"class":100,"line":101},[98,946,350],{},[98,948,949],{"class":100,"line":107},[98,950,951],{},"  reserved 4;\n",[98,953,954],{"class":100,"line":114},[98,955,956],{},"  reserved \"legacy_duration\";\n",[98,958,959],{"class":100,"line":120},[98,960,111],{"emptyLinePlaceholder":110},[98,962,963],{"class":100,"line":125},[98,964,355],{},[98,966,967],{"class":100,"line":131},[98,968,360],{},[98,970,971],{"class":100,"line":136},[98,972,365],{},[98,974,975],{"class":100,"line":142},[98,976,977],{},"  int32 estimated_minutes = 5;\n",[98,979,980],{"class":100,"line":148},[98,981,157],{},[15,983,984,985,988],{},"Так следующий разработчик не займёт номер ",[24,986,987],{},"4"," новым смыслом.",[262,990,992],{"id":991},"unknown-fields-и-rollout","Unknown fields и rollout",[15,994,995],{},"Protobuf-клиент может получить поле, которого нет в его локальной версии схемы. Это нормальная ситуация при постепенном rollout: новый сервер уже пишет поле, старый клиент ещё не знает его имени.",[15,997,998],{},"Практический вывод:",[48,1000,1001,1004,1007,1010],{},[51,1002,1003],{},"добавляйте поля так, чтобы старые клиенты могли их игнорировать;",[51,1005,1006],{},"не делайте новое поле обязательным для корректной обработки старого request;",[51,1008,1009],{},"не переносите критичное бизнес-решение только в новое поле без migration window;",[51,1011,1012],{},"не полагайтесь на то, что промежуточный сервис сохранит unknown fields при read-modify-write. В разных языках, версиях runtime и способах преобразования в JSON это может вести себя по-разному.",[15,1014,1015],{},"Если изменение должно пройти через несколько сервисов, планируйте expand\u002Fcontract: сначала все участники учатся читать новое поле, потом кто-то начинает писать, и только после этого старый путь можно убирать.",[79,1017],{},[82,1019,1021],{"id":1020},"версионирование","Версионирование",[15,1023,1024],{},"Обычно версию кладут в package и путь:",[89,1026,1028],{"className":91,"code":1027,"language":93,"meta":94,"style":94},"package course.v1;\n\noption go_package = \"example.com\u002Fmentor\u002Fgen\u002Fcourse\u002Fv1;coursev1\";\n",[24,1029,1030,1035,1039],{"__ignoreMap":94},[98,1031,1032],{"class":100,"line":101},[98,1033,1034],{},"package course.v1;\n",[98,1036,1037],{"class":100,"line":107},[98,1038,111],{"emptyLinePlaceholder":110},[98,1040,1041],{"class":100,"line":114},[98,1042,1043],{},"option go_package = \"example.com\u002Fmentor\u002Fgen\u002Fcourse\u002Fv1;coursev1\";\n",[15,1045,1046,1047,1050,1051,281],{},"Если изменения обратно несовместимы, лучше создать ",[24,1048,1049],{},"course.v2",", а не ломать ",[24,1052,300],{},[89,1054,1059],{"className":1055,"code":1057,"language":1058,"meta":94},[1056],"language-text","proto\u002Fcourse\u002Fv1\u002Fcourse.proto\nproto\u002Fcourse\u002Fv2\u002Fcourse.proto\n","text",[24,1060,1057],{"__ignoreMap":94},[15,1062,1063],{},"Версия в package честно говорит клиентам: это другой контракт.",[15,1065,1066,1067,1069,1070,1073,1074,1076],{},"Версионирование не отменяет совместимость внутри ",[24,1068,300],{},". Если каждое неудобное изменение сразу уносить в ",[24,1071,1072],{},"v2",", у команды появится несколько живых API, несколько наборов generated-кода и дорогая миграция клиентов. Обычно ",[24,1075,1072],{}," нужен, когда меняется модель данных или semantics метода так, что старые клиенты невозможно поддержать честно.",[15,1078,1079,1080,1082],{},"Хороший признак для ",[24,1081,1072],{},": старый и новый клиент не могут одинаково понять один и тот же request\u002Fresponse даже при аккуратных optional fields и migration window.",[79,1084],{},[82,1086,1088],{"id":1087},"enum-и-zero-value","Enum и zero value",[15,1090,1091],{},"Если поле имеет ограниченный набор состояний, лучше использовать enum, а не свободную строку:",[89,1093,1095],{"className":91,"code":1094,"language":93,"meta":94,"style":94},"enum LessonStatus {\n  LESSON_STATUS_UNSPECIFIED = 0;\n  LESSON_STATUS_DRAFT = 1;\n  LESSON_STATUS_PUBLISHED = 2;\n  LESSON_STATUS_ARCHIVED = 3;\n}\n\nmessage Lesson {\n  string slug = 1;\n  string title = 2;\n  LessonStatus status = 3;\n}\n",[24,1096,1097,1102,1107,1112,1117,1122,1126,1130,1134,1138,1142,1147],{"__ignoreMap":94},[98,1098,1099],{"class":100,"line":101},[98,1100,1101],{},"enum LessonStatus {\n",[98,1103,1104],{"class":100,"line":107},[98,1105,1106],{},"  LESSON_STATUS_UNSPECIFIED = 0;\n",[98,1108,1109],{"class":100,"line":114},[98,1110,1111],{},"  LESSON_STATUS_DRAFT = 1;\n",[98,1113,1114],{"class":100,"line":120},[98,1115,1116],{},"  LESSON_STATUS_PUBLISHED = 2;\n",[98,1118,1119],{"class":100,"line":125},[98,1120,1121],{},"  LESSON_STATUS_ARCHIVED = 3;\n",[98,1123,1124],{"class":100,"line":131},[98,1125,157],{},[98,1127,1128],{"class":100,"line":136},[98,1129,111],{"emptyLinePlaceholder":110},[98,1131,1132],{"class":100,"line":142},[98,1133,350],{},[98,1135,1136],{"class":100,"line":148},[98,1137,355],{},[98,1139,1140],{"class":100,"line":154},[98,1141,360],{},[98,1143,1144],{"class":100,"line":160},[98,1145,1146],{},"  LessonStatus status = 3;\n",[98,1148,1149],{"class":100,"line":165},[98,1150,157],{},[15,1152,1153,1154,838,1157,1160,1161,281],{},"В proto3 первое значение enum должно быть нулевым. Практический стиль - делать его ",[24,1155,1156],{},"*_UNSPECIFIED",[24,1158,1159],{},"*_UNKNOWN",", чтобы нулевое значение не означало реальное бизнес-состояние. Иначе пустое\u002Fнеинициализированное поле может случайно стать, например, ",[24,1162,67],{},[15,1164,1165],{},"Ещё один плюс enum: если клиент получил неизвестное новое значение от более свежего сервера, он хотя бы понимает, что поле относится к фиксированному набору состояний, а не парсит произвольную строку.",[262,1167,1169,678,1172,1175],{"id":1168},"oneof-map-и-repeated-поля",[24,1170,1171],{},"oneof",[24,1173,1174],{},"map"," и repeated поля",[15,1177,1178,1180],{},[24,1179,1171],{}," полезен, когда у сообщения есть несколько взаимоисключающих вариантов:",[89,1182,1184],{"className":91,"code":1183,"language":93,"meta":94,"style":94},"message RateLookupRequest {\n  oneof query {\n    string pair = 1;\n    string rate_id = 2;\n  }\n}\n",[24,1185,1186,1191,1196,1201,1206,1211],{"__ignoreMap":94},[98,1187,1188],{"class":100,"line":101},[98,1189,1190],{},"message RateLookupRequest {\n",[98,1192,1193],{"class":100,"line":107},[98,1194,1195],{},"  oneof query {\n",[98,1197,1198],{"class":100,"line":114},[98,1199,1200],{},"    string pair = 1;\n",[98,1202,1203],{"class":100,"line":120},[98,1204,1205],{},"    string rate_id = 2;\n",[98,1207,1208],{"class":100,"line":125},[98,1209,1210],{},"  }\n",[98,1212,1213],{"class":100,"line":131},[98,1214,157],{},[15,1216,1217,1218,1220],{},"Так контракт явно говорит: клиент должен выбрать один способ поиска. Без ",[24,1219,1171],{}," легко получить request, где заполнены оба поля, и серверу придётся угадывать приоритет.",[15,1222,1223,1224,1226,1227,1229],{},"Но ",[24,1225,1171],{}," тоже надо эволюционировать осторожно. Нельзя менять смысл существующего варианта, а удалённые номера лучше резервировать. Если старый клиент получает новый вариант ",[24,1228,1171],{},", он может не знать, как с ним работать, поэтому у сервера должен быть безопасный fallback или понятная ошибка.",[15,1231,1232,1234],{},[24,1233,1174],{}," удобен для небольших словарей, но не стоит делать из него универсальный escape hatch:",[89,1236,1238],{"className":91,"code":1237,"language":93,"meta":94,"style":94},"map\u003Cstring, string> metadata = 10;\n",[24,1239,1240],{"__ignoreMap":94},[98,1241,1242],{"class":100,"line":101},[98,1243,1237],{},[15,1245,1246,1247,1249],{},"Такое поле быстро превращается в невалидируемый JSON внутри protobuf: непонятные ключи, разные форматы значений, высокая кардинальность в логах и метриках. Для бизнес-данных лучше явные поля. ",[24,1248,1174],{}," оставляйте для действительно открытого набора атрибутов и заранее определите ограничения на ключи, размер и чувствительные значения.",[15,1251,1252,1253,1256],{},"Для ",[24,1254,1255],{},"repeated"," полей отдельно договоритесь:",[48,1258,1259,1262,1265,1268],{},[51,1260,1261],{},"важен ли порядок элементов;",[51,1263,1264],{},"допускаются ли дубликаты;",[51,1266,1267],{},"есть ли лимит размера;",[51,1269,1270],{},"что делает сервер с пустым списком.",[15,1272,1273],{},"Без этих правил клиенты могут отправить корректный protobuf, который создаст лишнюю нагрузку или неоднозначное бизнес-поведение.",[79,1275],{},[82,1277,1279],{"id":1278},"валидация-на-границе","Валидация на границе",[15,1281,1282],{},"Generated Go-типы не заменяют validation. Они гарантируют, что bytes можно распаковать в структуру, но не гарантируют, что request допустим для вашего usecase.",[15,1284,1285],{},"Пример плохой надежды:",[89,1287,1289],{"className":385,"code":1288,"language":387,"meta":94,"style":94},"func (s *Server) GetLesson(ctx context.Context, req *coursev1.GetLessonRequest) (*coursev1.GetLessonResponse, error) {\n    lesson, err := s.usecase.GetLesson(ctx, req.GetSlug())\n    \u002F\u002F ...\n}\n",[24,1290,1291,1366,1387,1393],{"__ignoreMap":94},[98,1292,1293,1297,1300,1304,1307,1310,1313,1316,1319,1322,1325,1327,1330,1332,1335,1338,1341,1343,1346,1349,1351,1353,1355,1358,1360,1363],{"class":100,"line":101},[98,1294,1296],{"class":1295},"snl16","func",[98,1298,1299],{"class":394}," (",[98,1301,1303],{"class":1302},"s9osk","s ",[98,1305,1306],{"class":1295},"*",[98,1308,1309],{"class":398},"Server",[98,1311,1312],{"class":394},") ",[98,1314,1315],{"class":398},"GetLesson",[98,1317,1318],{"class":394},"(",[98,1320,1321],{"class":1302},"ctx",[98,1323,1324],{"class":398}," context",[98,1326,281],{"class":394},[98,1328,1329],{"class":398},"Context",[98,1331,678],{"class":394},[98,1333,1334],{"class":1302},"req",[98,1336,1337],{"class":1295}," *",[98,1339,1340],{"class":398},"coursev1",[98,1342,281],{"class":394},[98,1344,1345],{"class":398},"GetLessonRequest",[98,1347,1348],{"class":394},") (",[98,1350,1306],{"class":1295},[98,1352,1340],{"class":398},[98,1354,281],{"class":394},[98,1356,1357],{"class":398},"GetLessonResponse",[98,1359,678],{"class":394},[98,1361,1362],{"class":1295},"error",[98,1364,1365],{"class":394},") {\n",[98,1367,1368,1371,1374,1377,1379,1382,1384],{"class":100,"line":107},[98,1369,1370],{"class":394},"    lesson, err ",[98,1372,1373],{"class":1295},":=",[98,1375,1376],{"class":394}," s.usecase.",[98,1378,1315],{"class":398},[98,1380,1381],{"class":394},"(ctx, req.",[98,1383,399],{"class":398},[98,1385,1386],{"class":394},"())\n",[98,1388,1389],{"class":100,"line":114},[98,1390,1392],{"class":1391},"sAwPA","    \u002F\u002F ...\n",[98,1394,1395],{"class":100,"line":120},[98,1396,157],{"class":394},[15,1398,1399,1400,1402,1403,1406],{},"Если ",[24,1401,451],{}," пустой, слишком длинный или содержит неожиданные символы, это должно быть остановлено на transport\u002Fapplication границе и превращено в понятную ошибку, обычно ",[24,1404,1405],{},"InvalidArgument",". Сам protobuf этого не сделает.",[15,1408,1409],{},"Что стоит проверять:",[48,1411,1412,1415,1418,1421,1424,1430],{},[51,1413,1414],{},"required-by-business поля: slug, id, currency, amount;",[51,1416,1417],{},"диапазоны чисел и лимиты размеров списков;",[51,1419,1420],{},"формат строк: id, currency, locale, version;",[51,1422,1423],{},"корректность timestamp: timezone semantics, будущие\u002Fпрошлые значения;",[51,1425,1426,1427,1429],{},"enum: ",[24,1428,1156],{}," в request часто должен быть ошибкой, а не \"авто-режимом\";",[51,1431,1432,1433,281],{},"взаимные ограничения полей, особенно без ",[24,1434,1171],{},[15,1436,1437,1438,1440],{},"В больших проектах validation часто описывают рядом со схемой через линтеры или validation-плагины, а затем всё равно проверяют важные инварианты в application layer. Не переносите бизнес-правила полностью в ",[24,1439,26],{},": схема должна помогать контракту, но домен не должен зависеть от generated DTO.",[79,1442],{},[82,1444,1446],{"id":1445},"наблюдаемость-и-безопасность-контракта","Наблюдаемость и безопасность контракта",[15,1448,1449],{},"Protobuf бинарный, но это не шифрование. Любой сервис, прокси, логгер или debug-инструмент с доступом к payload может прочитать сообщение по схеме. Поэтому не кладите в обычные сообщения секреты, raw tokens, пароли и лишние PII \"на всякий случай\".",[15,1451,1452],{},"Для production-контракта полезны правила:",[48,1454,1455,1458,1461,1464,1467],{},[51,1456,1457],{},"request id, trace id и auth обычно передают через gRPC metadata\u002Finterceptors, а не копируют в каждое бизнес-сообщение;",[51,1459,1460],{},"поля с PII должны быть явно названы и попадать под masking\u002Fredaction в логах;",[51,1462,1463],{},"не логируйте весь protobuf request целиком на error path;",[51,1465,1466],{},"не добавляйте debug-only поля в публичный\u002Fмежсервисный контракт, если их нельзя поддерживать годами;",[51,1468,1469],{},"следите за cardinality: свободные строки из protobuf не должны становиться label values в метриках.",[15,1471,1472,1473,1476],{},"Метрики и traces обычно строятся вокруг RPC method, status code, latency и размера сообщений. Содержимое protobuf лучше использовать в логах точечно и после фильтрации. Иначе одна удобная строка ",[24,1474,1475],{},"slog.Info(\"request\", \"req\", req)"," может превратить контракт в источник утечек и дорогих индексов.",[79,1478],{},[82,1480,1482],{"id":1481},"что-проверяет-senior-reviewer","Что проверяет senior reviewer",[15,1484,1485,1486,1488],{},"При ревью ",[24,1487,26],{}," смотрят не только на синтаксис:",[48,1490,1491,1499,1502,1505,1508,1514,1517,1524,1531,1534],{},[51,1492,1493,1494,1496,1497,68],{},"есть ли версия в ",[24,1495,284],{}," и корректный ",[24,1498,327],{},[51,1500,1501],{},"не переиспользуются ли field numbers и reserved names;",[51,1503,1504],{},"понятны ли единицы измерения, scale, rounding и формат строк;",[51,1506,1507],{},"можно ли добавить поле без поломки старых клиентов;",[51,1509,1510,1511,1513],{},"нет ли ",[24,1512,535],{}," для денег и точных значений;",[51,1515,1516],{},"не протекают ли domain\u002Finternal\u002Fdebug поля наружу;",[51,1518,1519,1520,1523],{},"не стал ли ",[24,1521,1522],{},"map\u003Cstring, string>"," способом спрятать неописанный контракт;",[51,1525,1526,1527,754,1529,68],{},"есть ли limits для ",[24,1528,1255],{},[24,1530,547],{},[51,1532,1533],{},"различаются ли \"не прислали поле\" и \"прислали zero value\", если это важно;",[51,1535,1536],{},"где будут validation, error mapping, metrics и redaction.",[15,1538,1539],{},"Если команда использует Buf или другой contract workflow, в CI обычно добавляют lint и breaking-change check. Это не заменяет ревью смысла полей, но ловит самые дорогие механические ошибки: переиспользование номеров, неправильные имена, проблемы package\u002Flayout.",[79,1541],{},[82,1543,1545,1546],{"id":1544},"практика-описать-calculatorservice","Практика: описать ",[24,1547,1548],{},"CalculatorService",[15,1550,1551],{},"Создайте protobuf-контракт:",[89,1553,1554],{"className":91,"code":92,"language":93,"meta":94,"style":94},[24,1555,1556,1560,1564,1568,1572,1576,1580,1584,1588,1592,1596,1600,1604,1608,1612,1616,1620,1624,1628,1632,1636,1640,1644,1648,1652,1656,1660,1664],{"__ignoreMap":94},[98,1557,1558],{"class":100,"line":101},[98,1559,104],{},[98,1561,1562],{"class":100,"line":107},[98,1563,111],{"emptyLinePlaceholder":110},[98,1565,1566],{"class":100,"line":114},[98,1567,117],{},[98,1569,1570],{"class":100,"line":120},[98,1571,111],{"emptyLinePlaceholder":110},[98,1573,1574],{"class":100,"line":125},[98,1575,128],{},[98,1577,1578],{"class":100,"line":131},[98,1579,111],{"emptyLinePlaceholder":110},[98,1581,1582],{"class":100,"line":136},[98,1583,139],{},[98,1585,1586],{"class":100,"line":142},[98,1587,145],{},[98,1589,1590],{"class":100,"line":148},[98,1591,151],{},[98,1593,1594],{"class":100,"line":154},[98,1595,157],{},[98,1597,1598],{"class":100,"line":160},[98,1599,111],{"emptyLinePlaceholder":110},[98,1601,1602],{"class":100,"line":165},[98,1603,168],{},[98,1605,1606],{"class":100,"line":171},[98,1607,174],{},[98,1609,1610],{"class":100,"line":177},[98,1611,180],{},[98,1613,1614],{"class":100,"line":183},[98,1615,157],{},[98,1617,1618],{"class":100,"line":188},[98,1619,111],{"emptyLinePlaceholder":110},[98,1621,1622],{"class":100,"line":193},[98,1623,196],{},[98,1625,1626],{"class":100,"line":199},[98,1627,202],{},[98,1629,1630],{"class":100,"line":205},[98,1631,157],{},[98,1633,1634],{"class":100,"line":210},[98,1635,111],{"emptyLinePlaceholder":110},[98,1637,1638],{"class":100,"line":215},[98,1639,218],{},[98,1641,1642],{"class":100,"line":221},[98,1643,224],{},[98,1645,1646],{"class":100,"line":227},[98,1647,230],{},[98,1649,1650],{"class":100,"line":233},[98,1651,157],{},[98,1653,1654],{"class":100,"line":238},[98,1655,111],{"emptyLinePlaceholder":110},[98,1657,1658],{"class":100,"line":243},[98,1659,246],{},[98,1661,1662],{"class":100,"line":249},[98,1663,252],{},[98,1665,1666],{"class":100,"line":255},[98,1667,157],{},[15,1669,1670],{},"Проверьте себя:",[48,1672,1673,1687,1694,1697],{},[51,1674,1675,1676,1679,1680,1683,1684,68],{},"почему ",[24,1677,1678],{},"divisor"," нельзя потом заменить на ",[24,1681,1682],{},"lesson_slug",", оставив номер ",[24,1685,1686],{},"2",[51,1688,1689,1690,1693],{},"где лучше держать ",[24,1691,1692],{},"trace_id",": в message или в gRPC metadata, и почему;",[51,1695,1696],{},"что лучше сделать, если поле удалили;",[51,1698,1675,1699,1701,1702,281],{},[24,1700,327],{}," не то же самое, что ",[24,1703,284],{},[79,1705],{},[82,1707,1709],{"id":1708},"типичные-ошибки","Типичные ошибки",[48,1711,1712,1715,1718,1724,1727,1734],{},[51,1713,1714],{},"Использовать номера полей как \"красивый порядок\", а не как контракт.",[51,1716,1717],{},"Удалить поле и потом занять его номер новым смыслом.",[51,1719,1720,1721,1723],{},"Забыть ",[24,1722,327],{},", а потом бороться с import path generated-кода.",[51,1725,1726],{},"Тащить доменную модель один в один в protobuf. Proto messages - это transport DTO, а не обязательно ваша domain entity.",[51,1728,1729,1730,1733],{},"Делать один гигантский ",[24,1731,1732],{},"CommonMessage"," на все методы.",[51,1735,1736,1737,1740,1741,281],{},"Добавлять ",[24,1738,1739],{},"string status = 1",", хотя лучше enum с явными значениями и нулевым ",[24,1742,1156],{},[79,1744],{},[82,1746,1748],{"id":1747},"источники","Источники",[48,1750,1751,1760,1767,1774],{},[51,1752,1753],{},[1754,1755,1759],"a",{"href":1756,"rel":1757},"https:\u002F\u002Fprotobuf.dev\u002Freference\u002Fprotobuf\u002Fproto3-spec\u002F",[1758],"nofollow","Protocol Buffers Proto3 Specification",[51,1761,1762],{},[1754,1763,1766],{"href":1764,"rel":1765},"https:\u002F\u002Fprotobuf.dev\u002Fgetting-started\u002Fgotutorial\u002F",[1758],"Protocol Buffer Basics: Go",[51,1768,1769],{},[1754,1770,1773],{"href":1771,"rel":1772},"https:\u002F\u002Fprotobuf.dev\u002Freference\u002Fgo\u002Fgo-generated\u002F",[1758],"Go Generated Code Guide",[51,1775,1776],{},[1754,1777,1780],{"href":1778,"rel":1779},"https:\u002F\u002Fgrpc.io\u002Fdocs\u002Flanguages\u002Fgo\u002Fgenerated-code\u002F",[1758],"gRPC Go Generated Code Reference",[79,1782],{},[82,1784,1786],{"id":1785},"интерактивная-практика","Интерактивная практика",[1788,1789,1793,1796,1816],"quiz",{"answer":1790,"id":1791,"xp":1792},"3","rpc-protobuf-q1","10",[15,1794,1795],{},"Что нельзя делать с номером поля protobuf после удаления поля?",[1797,1798,1799],"template",{"v-slot:options":94},[48,1800,1801,1804,1807,1810],{},[51,1802,1803],{},"Оставить комментарий рядом с message",[51,1805,1806],{},"Добавить поле с новым именем и новым номером",[51,1808,1809],{},"Переиспользовать старый номер под новый смысл",[51,1811,1812,1813],{},"Зарезервировать старый номер через ",[24,1814,1815],{},"reserved",[1797,1817,1818],{"v-slot:explanation":94},[15,1819,1820],{},"Номер поля — часть wire-contract. Если переиспользовать номер под другой смысл, старые и новые клиенты начнут читать данные неправильно.",[1822,1823,1827,1830,2021],"predict",{"answer":1824,"id":1825,"xp":1826},"safe\\nbreaking","rpc-protobuf-p1","15",[15,1828,1829],{},"Что выведет программа?",[1797,1831,1832],{"v-slot:code":94},[89,1833,1835],{"className":385,"code":1834,"language":387,"meta":94,"style":94},"package main\n\nimport \"fmt\"\n\nfunc changeKind(oldNumber, newNumber int, sameMeaning bool) string {\n    if oldNumber == newNumber && !sameMeaning {\n        return \"breaking\"\n    }\n    return \"safe\"\n}\n\nfunc main() {\n    fmt.Println(changeKind(2, 3, false))\n    fmt.Println(changeKind(2, 2, false))\n}\n",[24,1836,1837,1844,1848,1863,1867,1902,1925,1933,1938,1946,1950,1954,1964,1993,2017],{"__ignoreMap":94},[98,1838,1839,1841],{"class":100,"line":101},[98,1840,284],{"class":1295},[98,1842,1843],{"class":398}," main\n",[98,1845,1846],{"class":100,"line":107},[98,1847,111],{"emptyLinePlaceholder":110},[98,1849,1850,1853,1857,1860],{"class":100,"line":114},[98,1851,1852],{"class":1295},"import",[98,1854,1856],{"class":1855},"sU2Wk"," \"",[98,1858,1859],{"class":398},"fmt",[98,1861,1862],{"class":1855},"\"\n",[98,1864,1865],{"class":100,"line":120},[98,1866,111],{"emptyLinePlaceholder":110},[98,1868,1869,1871,1874,1876,1879,1881,1884,1887,1889,1892,1895,1897,1899],{"class":100,"line":125},[98,1870,1296],{"class":1295},[98,1872,1873],{"class":398}," changeKind",[98,1875,1318],{"class":394},[98,1877,1878],{"class":1302},"oldNumber",[98,1880,678],{"class":394},[98,1882,1883],{"class":1302},"newNumber",[98,1885,1886],{"class":1295}," int",[98,1888,678],{"class":394},[98,1890,1891],{"class":1302},"sameMeaning",[98,1893,1894],{"class":1295}," bool",[98,1896,1312],{"class":394},[98,1898,491],{"class":1295},[98,1900,1901],{"class":394}," {\n",[98,1903,1904,1907,1910,1913,1916,1919,1922],{"class":100,"line":131},[98,1905,1906],{"class":1295},"    if",[98,1908,1909],{"class":394}," oldNumber ",[98,1911,1912],{"class":1295},"==",[98,1914,1915],{"class":394}," newNumber ",[98,1917,1918],{"class":1295},"&&",[98,1920,1921],{"class":1295}," !",[98,1923,1924],{"class":394},"sameMeaning {\n",[98,1926,1927,1930],{"class":100,"line":136},[98,1928,1929],{"class":1295},"        return",[98,1931,1932],{"class":1855}," \"breaking\"\n",[98,1934,1935],{"class":100,"line":142},[98,1936,1937],{"class":394},"    }\n",[98,1939,1940,1943],{"class":100,"line":148},[98,1941,1942],{"class":1295},"    return",[98,1944,1945],{"class":1855}," \"safe\"\n",[98,1947,1948],{"class":100,"line":154},[98,1949,157],{"class":394},[98,1951,1952],{"class":100,"line":160},[98,1953,111],{"emptyLinePlaceholder":110},[98,1955,1956,1958,1961],{"class":100,"line":165},[98,1957,1296],{"class":1295},[98,1959,1960],{"class":398}," main",[98,1962,1963],{"class":394},"() {\n",[98,1965,1966,1969,1972,1974,1977,1979,1982,1984,1986,1988,1990],{"class":100,"line":171},[98,1967,1968],{"class":394},"    fmt.",[98,1970,1971],{"class":398},"Println",[98,1973,1318],{"class":394},[98,1975,1976],{"class":398},"changeKind",[98,1978,1318],{"class":394},[98,1980,1686],{"class":1981},"sDLfK",[98,1983,678],{"class":394},[98,1985,1790],{"class":1981},[98,1987,678],{"class":394},[98,1989,681],{"class":1981},[98,1991,1992],{"class":394},"))\n",[98,1994,1995,1997,1999,2001,2003,2005,2007,2009,2011,2013,2015],{"class":100,"line":177},[98,1996,1968],{"class":394},[98,1998,1971],{"class":398},[98,2000,1318],{"class":394},[98,2002,1976],{"class":398},[98,2004,1318],{"class":394},[98,2006,1686],{"class":1981},[98,2008,678],{"class":394},[98,2010,1686],{"class":1981},[98,2012,678],{"class":394},[98,2014,681],{"class":1981},[98,2016,1992],{"class":394},[98,2018,2019],{"class":100,"line":183},[98,2020,157],{"class":394},[1797,2022,2023],{"v-slot:hint":94},[15,2024,2025],{},"Новый номер для нового смысла безопаснее. Старый номер с новым смыслом ломает совместимость.",[2027,2028,2032,2046,2206],"code-task",{"expected":2029,"id":2030,"xp":2031},"ok\\nbad\\nok","rpc-protobuf-ct1","20",[15,2033,2034,2035,2038,2039,2042,2043,281],{},"Реализуй ",[24,2036,2037],{},"FieldChangeReview",": если номер тот же, но смысл изменился, верни ",[24,2040,2041],{},"\"bad\"",", иначе ",[24,2044,2045],{},"\"ok\"",[1797,2047,2048],{"v-slot:template":94},[89,2049,2051],{"className":385,"code":2050,"language":387,"meta":94,"style":94},"package main\n\nimport \"fmt\"\n\nfunc FieldChangeReview(oldNumber, newNumber int, sameMeaning bool) string {\n    return \"ok\"\n}\n\nfunc main() {\n    fmt.Println(FieldChangeReview(1, 1, true))\n    fmt.Println(FieldChangeReview(2, 2, false))\n    fmt.Println(FieldChangeReview(3, 4, false))\n}\n",[24,2052,2053,2059,2063,2073,2077,2106,2113,2117,2121,2129,2154,2178,2202],{"__ignoreMap":94},[98,2054,2055,2057],{"class":100,"line":101},[98,2056,284],{"class":1295},[98,2058,1843],{"class":398},[98,2060,2061],{"class":100,"line":107},[98,2062,111],{"emptyLinePlaceholder":110},[98,2064,2065,2067,2069,2071],{"class":100,"line":114},[98,2066,1852],{"class":1295},[98,2068,1856],{"class":1855},[98,2070,1859],{"class":398},[98,2072,1862],{"class":1855},[98,2074,2075],{"class":100,"line":120},[98,2076,111],{"emptyLinePlaceholder":110},[98,2078,2079,2081,2084,2086,2088,2090,2092,2094,2096,2098,2100,2102,2104],{"class":100,"line":125},[98,2080,1296],{"class":1295},[98,2082,2083],{"class":398}," FieldChangeReview",[98,2085,1318],{"class":394},[98,2087,1878],{"class":1302},[98,2089,678],{"class":394},[98,2091,1883],{"class":1302},[98,2093,1886],{"class":1295},[98,2095,678],{"class":394},[98,2097,1891],{"class":1302},[98,2099,1894],{"class":1295},[98,2101,1312],{"class":394},[98,2103,491],{"class":1295},[98,2105,1901],{"class":394},[98,2107,2108,2110],{"class":100,"line":131},[98,2109,1942],{"class":1295},[98,2111,2112],{"class":1855}," \"ok\"\n",[98,2114,2115],{"class":100,"line":136},[98,2116,157],{"class":394},[98,2118,2119],{"class":100,"line":142},[98,2120,111],{"emptyLinePlaceholder":110},[98,2122,2123,2125,2127],{"class":100,"line":148},[98,2124,1296],{"class":1295},[98,2126,1960],{"class":398},[98,2128,1963],{"class":394},[98,2130,2131,2133,2135,2137,2139,2141,2143,2145,2147,2149,2152],{"class":100,"line":154},[98,2132,1968],{"class":394},[98,2134,1971],{"class":398},[98,2136,1318],{"class":394},[98,2138,2037],{"class":398},[98,2140,1318],{"class":394},[98,2142,447],{"class":1981},[98,2144,678],{"class":394},[98,2146,447],{"class":1981},[98,2148,678],{"class":394},[98,2150,2151],{"class":1981},"true",[98,2153,1992],{"class":394},[98,2155,2156,2158,2160,2162,2164,2166,2168,2170,2172,2174,2176],{"class":100,"line":160},[98,2157,1968],{"class":394},[98,2159,1971],{"class":398},[98,2161,1318],{"class":394},[98,2163,2037],{"class":398},[98,2165,1318],{"class":394},[98,2167,1686],{"class":1981},[98,2169,678],{"class":394},[98,2171,1686],{"class":1981},[98,2173,678],{"class":394},[98,2175,681],{"class":1981},[98,2177,1992],{"class":394},[98,2179,2180,2182,2184,2186,2188,2190,2192,2194,2196,2198,2200],{"class":100,"line":165},[98,2181,1968],{"class":394},[98,2183,1971],{"class":398},[98,2185,1318],{"class":394},[98,2187,2037],{"class":398},[98,2189,1318],{"class":394},[98,2191,1790],{"class":1981},[98,2193,678],{"class":394},[98,2195,987],{"class":1981},[98,2197,678],{"class":394},[98,2199,681],{"class":1981},[98,2201,1992],{"class":394},[98,2203,2204],{"class":100,"line":171},[98,2205,157],{"class":394},[1797,2207,2208],{"v-slot:hints":94},[48,2209,2210,2213,2216],{},[51,2211,2212],{},"Один и тот же номер с тем же смыслом — нормально",[51,2214,2215],{},"Один и тот же номер с новым смыслом — плохо",[51,2217,2218],{},"Новый смысл лучше выносить на новый номер",[2220,2221,2222],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}",{"title":94,"searchDepth":107,"depth":107,"links":2224},[2225,2231,2232,2235,2237,2240,2241,2245,2246,2247,2248,2250,2251,2252],{"id":84,"depth":107,"text":2226,"children":2227},"Первый .proto",[2228,2229,2230],{"id":264,"depth":114,"text":264},{"id":284,"depth":114,"text":284},{"id":304,"depth":114,"text":307},{"id":333,"depth":107,"text":333},{"id":460,"depth":107,"text":461,"children":2233},[2234],{"id":670,"depth":114,"text":671},{"id":750,"depth":107,"text":2236},"service и rpc",{"id":846,"depth":107,"text":847,"children":2238},[2239],{"id":991,"depth":114,"text":992},{"id":1020,"depth":107,"text":1021},{"id":1087,"depth":107,"text":1088,"children":2242},[2243],{"id":1168,"depth":114,"text":2244},"oneof, map и repeated поля",{"id":1278,"depth":107,"text":1279},{"id":1445,"depth":107,"text":1446},{"id":1481,"depth":107,"text":1482},{"id":1544,"depth":107,"text":2249},"Практика: описать CalculatorService",{"id":1708,"depth":107,"text":1709},{"id":1747,"depth":107,"text":1748},{"id":1785,"depth":107,"text":1786},1781022064728]