[{"data":1,"prerenderedAt":2648},["ShallowReactive",2],{"content:\u002F07-rpc-grpc\u002F06-grpc-streaming-production":3},{"title":4,"description":5,"path":6,"body":7},"Streaming, observability и production-практики gRPC","Streaming - одна из причин выбирать gRPC. Вместо одного request и одного response RPC может передавать много сообщений: от сервера к клиенту, от клиента к серверу или в обе стороны сразу.","\u002F07-rpc-grpc\u002F06-grpc-streaming-production",{"type":8,"value":9,"toc":2636},"minimark",[10,14,17,59,73,76,81,84,93,96,322,325,328,457,460,632,635,638,802,807,809,813,816,825,827,1052,1054,1212,1219,1221,1225,1228,1237,1245,1248,1664,1667,1669,1673,1676,1695,1703,1765,1767,1771,1774,1974,1977,1979,1983,1986,2013,2016,2018,2022,2105,2107,2111,2146,2148,2152,2198,2200,2204,2242,2454,2632],[11,12,4],"h1",{"id":13},"streaming-observability-и-production-практики-grpc",[15,16,5],"p",{},[18,19,24],"pre",{"className":20,"code":21,"language":22,"meta":23,"style":23},"language-proto shiki shiki-themes github-dark","service CourseService {\n  rpc ListLessons(ListLessonsRequest) returns (stream Lesson);\n  rpc UploadProgress(stream ProgressEvent) returns (ProgressSummary);\n  rpc MentorChat(stream ChatMessage) returns (stream ChatMessage);\n}\n","proto","",[25,26,27,35,41,47,53],"code",{"__ignoreMap":23},[28,29,32],"span",{"class":30,"line":31},"line",1,[28,33,34],{},"service CourseService {\n",[28,36,38],{"class":30,"line":37},2,[28,39,40],{},"  rpc ListLessons(ListLessonsRequest) returns (stream Lesson);\n",[28,42,44],{"class":30,"line":43},3,[28,45,46],{},"  rpc UploadProgress(stream ProgressEvent) returns (ProgressSummary);\n",[28,48,50],{"class":30,"line":49},4,[28,51,52],{},"  rpc MentorChat(stream ChatMessage) returns (stream ChatMessage);\n",[28,54,56],{"class":30,"line":55},5,[28,57,58],{},"}\n",[15,60,61,62,65,66,65,69,72],{},"Главная мысль: stream - это не массив. Это последовательность сообщений во времени. У stream есть backpressure, cancellation, ",[25,63,64],{},"Send",", ",[25,67,68],{},"Recv",[25,70,71],{},"io.EOF"," и свои сценарии зависаний.",[74,75],"hr",{},[77,78,80],"h2",{"id":79},"server-side-streaming","Server-side streaming",[15,82,83],{},"Клиент отправляет один request, сервер отправляет много response.",[18,85,87],{"className":20,"code":86,"language":22,"meta":23,"style":23},"rpc ListLessons(ListLessonsRequest) returns (stream Lesson);\n",[25,88,89],{"__ignoreMap":23},[28,90,91],{"class":30,"line":31},[28,92,86],{},[15,94,95],{},"Сервер:",[18,97,101],{"className":98,"code":99,"language":100,"meta":23,"style":23},"language-go shiki shiki-themes github-dark","func (s *CourseServer) ListLessons(\n    req *coursev1.ListLessonsRequest,\n    stream coursev1.CourseService_ListLessonsServer,\n) error {\n    return s.usecase.ForEachLesson(\n        stream.Context(),\n        req.GetModule(),\n        func(lesson application.Lesson) error {\n            if err := stream.Send(toProtoLesson(lesson)); err != nil {\n                return err\n            }\n\n            return nil\n        },\n    )\n}\n","go",[25,102,103,133,153,168,178,191,203,214,240,274,283,289,296,305,311,317],{"__ignoreMap":23},[28,104,105,109,113,117,120,124,127,130],{"class":30,"line":31},[28,106,108],{"class":107},"snl16","func",[28,110,112],{"class":111},"s95oV"," (",[28,114,116],{"class":115},"s9osk","s ",[28,118,119],{"class":107},"*",[28,121,123],{"class":122},"svObZ","CourseServer",[28,125,126],{"class":111},") ",[28,128,129],{"class":122},"ListLessons",[28,131,132],{"class":111},"(\n",[28,134,135,138,141,144,147,150],{"class":30,"line":37},[28,136,137],{"class":115},"    req",[28,139,140],{"class":107}," *",[28,142,143],{"class":122},"coursev1",[28,145,146],{"class":111},".",[28,148,149],{"class":122},"ListLessonsRequest",[28,151,152],{"class":111},",\n",[28,154,155,158,161,163,166],{"class":30,"line":43},[28,156,157],{"class":115},"    stream",[28,159,160],{"class":122}," coursev1",[28,162,146],{"class":111},[28,164,165],{"class":122},"CourseService_ListLessonsServer",[28,167,152],{"class":111},[28,169,170,172,175],{"class":30,"line":49},[28,171,126],{"class":111},[28,173,174],{"class":107},"error",[28,176,177],{"class":111}," {\n",[28,179,180,183,186,189],{"class":30,"line":55},[28,181,182],{"class":107},"    return",[28,184,185],{"class":111}," s.usecase.",[28,187,188],{"class":122},"ForEachLesson",[28,190,132],{"class":111},[28,192,194,197,200],{"class":30,"line":193},6,[28,195,196],{"class":111},"        stream.",[28,198,199],{"class":122},"Context",[28,201,202],{"class":111},"(),\n",[28,204,206,209,212],{"class":30,"line":205},7,[28,207,208],{"class":111},"        req.",[28,210,211],{"class":122},"GetModule",[28,213,202],{"class":111},[28,215,217,220,223,226,229,231,234,236,238],{"class":30,"line":216},8,[28,218,219],{"class":107},"        func",[28,221,222],{"class":111},"(",[28,224,225],{"class":115},"lesson",[28,227,228],{"class":122}," application",[28,230,146],{"class":111},[28,232,233],{"class":122},"Lesson",[28,235,126],{"class":111},[28,237,174],{"class":107},[28,239,177],{"class":111},[28,241,243,246,249,252,255,257,259,262,265,268,272],{"class":30,"line":242},9,[28,244,245],{"class":107},"            if",[28,247,248],{"class":111}," err ",[28,250,251],{"class":107},":=",[28,253,254],{"class":111}," stream.",[28,256,64],{"class":122},[28,258,222],{"class":111},[28,260,261],{"class":122},"toProtoLesson",[28,263,264],{"class":111},"(lesson)); err ",[28,266,267],{"class":107},"!=",[28,269,271],{"class":270},"sDLfK"," nil",[28,273,177],{"class":111},[28,275,277,280],{"class":30,"line":276},10,[28,278,279],{"class":107},"                return",[28,281,282],{"class":111}," err\n",[28,284,286],{"class":30,"line":285},11,[28,287,288],{"class":111},"            }\n",[28,290,292],{"class":30,"line":291},12,[28,293,295],{"emptyLinePlaceholder":294},true,"\n",[28,297,299,302],{"class":30,"line":298},13,[28,300,301],{"class":107},"            return",[28,303,304],{"class":270}," nil\n",[28,306,308],{"class":30,"line":307},14,[28,309,310],{"class":111},"        },\n",[28,312,314],{"class":30,"line":313},15,[28,315,316],{"class":111},"    )\n",[28,318,320],{"class":30,"line":319},16,[28,321,58],{"class":111},[15,323,324],{},"Здесь usecase не обязан сначала собирать весь список в slice. Он может читать уроки постранично из БД, получать их из очереди или отдавать через iterator. Так stream остаётся потоком, а не массивом, который просто отправили по одному элементу.",[15,326,327],{},"Если callback\u002Fiterator возвращает доменную ошибку, adapter всё равно мапит её в gRPC status:",[18,329,331],{"className":98,"code":330,"language":100,"meta":23,"style":23},"err := s.usecase.ForEachLesson(stream.Context(), req.GetModule(), func(lesson application.Lesson) error {\n    if err := stream.Send(toProtoLesson(lesson)); err != nil {\n        return err\n    }\n    return nil\n})\nif err != nil {\n    return toGRPCError(err)\n}\nreturn nil\n",[25,332,333,375,400,407,412,418,423,436,446,450],{"__ignoreMap":23},[28,334,335,338,340,342,344,347,349,352,354,357,359,361,363,365,367,369,371,373],{"class":30,"line":31},[28,336,337],{"class":111},"err ",[28,339,251],{"class":107},[28,341,185],{"class":111},[28,343,188],{"class":122},[28,345,346],{"class":111},"(stream.",[28,348,199],{"class":122},[28,350,351],{"class":111},"(), req.",[28,353,211],{"class":122},[28,355,356],{"class":111},"(), ",[28,358,108],{"class":107},[28,360,222],{"class":111},[28,362,225],{"class":115},[28,364,228],{"class":122},[28,366,146],{"class":111},[28,368,233],{"class":122},[28,370,126],{"class":111},[28,372,174],{"class":107},[28,374,177],{"class":111},[28,376,377,380,382,384,386,388,390,392,394,396,398],{"class":30,"line":37},[28,378,379],{"class":107},"    if",[28,381,248],{"class":111},[28,383,251],{"class":107},[28,385,254],{"class":111},[28,387,64],{"class":122},[28,389,222],{"class":111},[28,391,261],{"class":122},[28,393,264],{"class":111},[28,395,267],{"class":107},[28,397,271],{"class":270},[28,399,177],{"class":111},[28,401,402,405],{"class":30,"line":43},[28,403,404],{"class":107},"        return",[28,406,282],{"class":111},[28,408,409],{"class":30,"line":49},[28,410,411],{"class":111},"    }\n",[28,413,414,416],{"class":30,"line":55},[28,415,182],{"class":107},[28,417,304],{"class":270},[28,419,420],{"class":30,"line":193},[28,421,422],{"class":111},"})\n",[28,424,425,428,430,432,434],{"class":30,"line":205},[28,426,427],{"class":107},"if",[28,429,248],{"class":111},[28,431,267],{"class":107},[28,433,271],{"class":270},[28,435,177],{"class":111},[28,437,438,440,443],{"class":30,"line":216},[28,439,182],{"class":107},[28,441,442],{"class":122}," toGRPCError",[28,444,445],{"class":111},"(err)\n",[28,447,448],{"class":30,"line":242},[28,449,58],{"class":111},[28,451,452,455],{"class":30,"line":276},[28,453,454],{"class":107},"return",[28,456,304],{"class":270},[15,458,459],{},"В длинной генерации данных полезно проверять context до дорогой работы:",[18,461,463],{"className":98,"code":462,"language":100,"meta":23,"style":23},"for iterator.Next() {\n    select {\n    case \u003C-stream.Context().Done():\n        return status.FromContextError(stream.Context().Err()).Err()\n    default:\n    }\n\n    if err := stream.Send(toProtoLesson(iterator.Lesson())); err != nil {\n        return err\n    }\n}\n\nif err := iterator.Err(); err != nil {\n    return toGRPCError(err)\n}\n",[25,464,465,479,486,508,535,543,547,551,581,587,591,595,599,620,628],{"__ignoreMap":23},[28,466,467,470,473,476],{"class":30,"line":31},[28,468,469],{"class":107},"for",[28,471,472],{"class":111}," iterator.",[28,474,475],{"class":122},"Next",[28,477,478],{"class":111},"() {\n",[28,480,481,484],{"class":30,"line":37},[28,482,483],{"class":107},"    select",[28,485,177],{"class":111},[28,487,488,491,494,497,499,502,505],{"class":30,"line":43},[28,489,490],{"class":107},"    case",[28,492,493],{"class":107}," \u003C-",[28,495,496],{"class":111},"stream.",[28,498,199],{"class":122},[28,500,501],{"class":111},"().",[28,503,504],{"class":122},"Done",[28,506,507],{"class":111},"():\n",[28,509,510,512,515,518,520,522,524,527,530,532],{"class":30,"line":49},[28,511,404],{"class":107},[28,513,514],{"class":111}," status.",[28,516,517],{"class":122},"FromContextError",[28,519,346],{"class":111},[28,521,199],{"class":122},[28,523,501],{"class":111},[28,525,526],{"class":122},"Err",[28,528,529],{"class":111},"()).",[28,531,526],{"class":122},[28,533,534],{"class":111},"()\n",[28,536,537,540],{"class":30,"line":55},[28,538,539],{"class":107},"    default",[28,541,542],{"class":111},":\n",[28,544,545],{"class":30,"line":193},[28,546,411],{"class":111},[28,548,549],{"class":30,"line":205},[28,550,295],{"emptyLinePlaceholder":294},[28,552,553,555,557,559,561,563,565,567,570,572,575,577,579],{"class":30,"line":216},[28,554,379],{"class":107},[28,556,248],{"class":111},[28,558,251],{"class":107},[28,560,254],{"class":111},[28,562,64],{"class":122},[28,564,222],{"class":111},[28,566,261],{"class":122},[28,568,569],{"class":111},"(iterator.",[28,571,233],{"class":122},[28,573,574],{"class":111},"())); err ",[28,576,267],{"class":107},[28,578,271],{"class":270},[28,580,177],{"class":111},[28,582,583,585],{"class":30,"line":242},[28,584,404],{"class":107},[28,586,282],{"class":111},[28,588,589],{"class":30,"line":276},[28,590,411],{"class":111},[28,592,593],{"class":30,"line":285},[28,594,58],{"class":111},[28,596,597],{"class":30,"line":291},[28,598,295],{"emptyLinePlaceholder":294},[28,600,601,603,605,607,609,611,614,616,618],{"class":30,"line":298},[28,602,427],{"class":107},[28,604,248],{"class":111},[28,606,251],{"class":107},[28,608,472],{"class":111},[28,610,526],{"class":122},[28,612,613],{"class":111},"(); err ",[28,615,267],{"class":107},[28,617,271],{"class":270},[28,619,177],{"class":111},[28,621,622,624,626],{"class":30,"line":307},[28,623,182],{"class":107},[28,625,442],{"class":122},[28,627,445],{"class":111},[28,629,630],{"class":30,"line":313},[28,631,58],{"class":111},[15,633,634],{},"В примерах часто показывают slice ради краткости, но в production streaming ценен именно тогда, когда данные появляются постепенно или их слишком много для одного response.",[15,636,637],{},"Клиент:",[18,639,642],{"className":98,"code":640,"language":100,"meta":641,"style":23},"stream, err := client.ListLessons(ctx, &coursev1.ListLessonsRequest{\n    Module: \"rpc-grpc\",\n})\nif err != nil {\n    return err\n}\n\nfor {\n    lesson, err := stream.Recv()\n    if errors.Is(err, io.EOF) {\n        break\n    }\n    if err != nil {\n        return err\n    }\n\n    fmt.Println(lesson.GetTitle())\n}\n","no-run",[25,643,644,671,682,686,698,704,708,712,718,731,744,749,753,765,771,775,779,797],{"__ignoreMap":23},[28,645,646,649,651,654,656,659,662,664,666,668],{"class":30,"line":31},[28,647,648],{"class":111},"stream, err ",[28,650,251],{"class":107},[28,652,653],{"class":111}," client.",[28,655,129],{"class":122},[28,657,658],{"class":111},"(ctx, ",[28,660,661],{"class":107},"&",[28,663,143],{"class":122},[28,665,146],{"class":111},[28,667,149],{"class":122},[28,669,670],{"class":111},"{\n",[28,672,673,676,680],{"class":30,"line":37},[28,674,675],{"class":111},"    Module: ",[28,677,679],{"class":678},"sU2Wk","\"rpc-grpc\"",[28,681,152],{"class":111},[28,683,684],{"class":30,"line":43},[28,685,422],{"class":111},[28,687,688,690,692,694,696],{"class":30,"line":49},[28,689,427],{"class":107},[28,691,248],{"class":111},[28,693,267],{"class":107},[28,695,271],{"class":270},[28,697,177],{"class":111},[28,699,700,702],{"class":30,"line":55},[28,701,182],{"class":107},[28,703,282],{"class":111},[28,705,706],{"class":30,"line":193},[28,707,58],{"class":111},[28,709,710],{"class":30,"line":205},[28,711,295],{"emptyLinePlaceholder":294},[28,713,714,716],{"class":30,"line":216},[28,715,469],{"class":107},[28,717,177],{"class":111},[28,719,720,723,725,727,729],{"class":30,"line":242},[28,721,722],{"class":111},"    lesson, err ",[28,724,251],{"class":107},[28,726,254],{"class":111},[28,728,68],{"class":122},[28,730,534],{"class":111},[28,732,733,735,738,741],{"class":30,"line":276},[28,734,379],{"class":107},[28,736,737],{"class":111}," errors.",[28,739,740],{"class":122},"Is",[28,742,743],{"class":111},"(err, io.EOF) {\n",[28,745,746],{"class":30,"line":285},[28,747,748],{"class":107},"        break\n",[28,750,751],{"class":30,"line":291},[28,752,411],{"class":111},[28,754,755,757,759,761,763],{"class":30,"line":298},[28,756,379],{"class":107},[28,758,248],{"class":111},[28,760,267],{"class":107},[28,762,271],{"class":270},[28,764,177],{"class":111},[28,766,767,769],{"class":30,"line":307},[28,768,404],{"class":107},[28,770,282],{"class":111},[28,772,773],{"class":30,"line":313},[28,774,411],{"class":111},[28,776,777],{"class":30,"line":319},[28,778,295],{"emptyLinePlaceholder":294},[28,780,782,785,788,791,794],{"class":30,"line":781},17,[28,783,784],{"class":111},"    fmt.",[28,786,787],{"class":122},"Println",[28,789,790],{"class":111},"(lesson.",[28,792,793],{"class":122},"GetTitle",[28,795,796],{"class":111},"())\n",[28,798,800],{"class":30,"line":799},18,[28,801,58],{"class":111},[15,803,804,806],{},[25,805,71],{}," здесь не авария. Это нормальный конец потока.",[74,808],{},[77,810,812],{"id":811},"client-side-streaming","Client-side streaming",[15,814,815],{},"Клиент отправляет много messages, сервер возвращает один summary.",[18,817,819],{"className":20,"code":818,"language":22,"meta":23,"style":23},"rpc UploadProgress(stream ProgressEvent) returns (ProgressSummary);\n",[25,820,821],{"__ignoreMap":23},[28,822,823],{"class":30,"line":31},[28,824,818],{},[15,826,95],{},[18,828,830],{"className":98,"code":829,"language":100,"meta":23,"style":23},"func (s *ProgressServer) UploadProgress(\n    stream progressv1.ProgressService_UploadProgressServer,\n) error {\n    var events []application.ProgressEvent\n\n    for {\n        req, err := stream.Recv()\n        if errors.Is(err, io.EOF) {\n            summary, err := s.usecase.SaveProgress(stream.Context(), events)\n            if err != nil {\n                return toGRPCError(err)\n            }\n\n            return stream.SendAndClose(toProtoSummary(summary))\n        }\n        if err != nil {\n            return err\n        }\n\n        events = append(events, toProgressEvent(req))\n    }\n}\n",[25,831,832,852,866,874,890,894,901,914,925,944,956,964,968,972,989,994,1006,1012,1016,1021,1042,1047],{"__ignoreMap":23},[28,833,834,836,838,840,842,845,847,850],{"class":30,"line":31},[28,835,108],{"class":107},[28,837,112],{"class":111},[28,839,116],{"class":115},[28,841,119],{"class":107},[28,843,844],{"class":122},"ProgressServer",[28,846,126],{"class":111},[28,848,849],{"class":122},"UploadProgress",[28,851,132],{"class":111},[28,853,854,856,859,861,864],{"class":30,"line":37},[28,855,157],{"class":115},[28,857,858],{"class":122}," progressv1",[28,860,146],{"class":111},[28,862,863],{"class":122},"ProgressService_UploadProgressServer",[28,865,152],{"class":111},[28,867,868,870,872],{"class":30,"line":43},[28,869,126],{"class":111},[28,871,174],{"class":107},[28,873,177],{"class":111},[28,875,876,879,882,885,887],{"class":30,"line":49},[28,877,878],{"class":107},"    var",[28,880,881],{"class":111}," events []",[28,883,884],{"class":122},"application",[28,886,146],{"class":111},[28,888,889],{"class":122},"ProgressEvent\n",[28,891,892],{"class":30,"line":55},[28,893,295],{"emptyLinePlaceholder":294},[28,895,896,899],{"class":30,"line":193},[28,897,898],{"class":107},"    for",[28,900,177],{"class":111},[28,902,903,906,908,910,912],{"class":30,"line":205},[28,904,905],{"class":111},"        req, err ",[28,907,251],{"class":107},[28,909,254],{"class":111},[28,911,68],{"class":122},[28,913,534],{"class":111},[28,915,916,919,921,923],{"class":30,"line":216},[28,917,918],{"class":107},"        if",[28,920,737],{"class":111},[28,922,740],{"class":122},[28,924,743],{"class":111},[28,926,927,930,932,934,937,939,941],{"class":30,"line":242},[28,928,929],{"class":111},"            summary, err ",[28,931,251],{"class":107},[28,933,185],{"class":111},[28,935,936],{"class":122},"SaveProgress",[28,938,346],{"class":111},[28,940,199],{"class":122},[28,942,943],{"class":111},"(), events)\n",[28,945,946,948,950,952,954],{"class":30,"line":276},[28,947,245],{"class":107},[28,949,248],{"class":111},[28,951,267],{"class":107},[28,953,271],{"class":270},[28,955,177],{"class":111},[28,957,958,960,962],{"class":30,"line":285},[28,959,279],{"class":107},[28,961,442],{"class":122},[28,963,445],{"class":111},[28,965,966],{"class":30,"line":291},[28,967,288],{"class":111},[28,969,970],{"class":30,"line":298},[28,971,295],{"emptyLinePlaceholder":294},[28,973,974,976,978,981,983,986],{"class":30,"line":307},[28,975,301],{"class":107},[28,977,254],{"class":111},[28,979,980],{"class":122},"SendAndClose",[28,982,222],{"class":111},[28,984,985],{"class":122},"toProtoSummary",[28,987,988],{"class":111},"(summary))\n",[28,990,991],{"class":30,"line":313},[28,992,993],{"class":111},"        }\n",[28,995,996,998,1000,1002,1004],{"class":30,"line":319},[28,997,918],{"class":107},[28,999,248],{"class":111},[28,1001,267],{"class":107},[28,1003,271],{"class":270},[28,1005,177],{"class":111},[28,1007,1008,1010],{"class":30,"line":781},[28,1009,301],{"class":107},[28,1011,282],{"class":111},[28,1013,1014],{"class":30,"line":799},[28,1015,993],{"class":111},[28,1017,1019],{"class":30,"line":1018},19,[28,1020,295],{"emptyLinePlaceholder":294},[28,1022,1024,1027,1030,1033,1036,1039],{"class":30,"line":1023},20,[28,1025,1026],{"class":111},"        events ",[28,1028,1029],{"class":107},"=",[28,1031,1032],{"class":122}," append",[28,1034,1035],{"class":111},"(events, ",[28,1037,1038],{"class":122},"toProgressEvent",[28,1040,1041],{"class":111},"(req))\n",[28,1043,1045],{"class":30,"line":1044},21,[28,1046,411],{"class":111},[28,1048,1050],{"class":30,"line":1049},22,[28,1051,58],{"class":111},[15,1053,637],{},[18,1055,1057],{"className":98,"code":1056,"language":100,"meta":641,"style":23},"stream, err := client.UploadProgress(ctx)\nif err != nil {\n    return err\n}\n\nfor _, event := range events {\n    if err := stream.Send(toProtoEvent(event)); err != nil {\n        return err\n    }\n}\n\nsummary, err := stream.CloseAndRecv()\nif err != nil {\n    return err\n}\n\nfmt.Println(summary.GetCompletedCount())\n",[25,1058,1059,1072,1084,1090,1094,1098,1113,1139,1145,1149,1153,1157,1171,1183,1189,1193,1197],{"__ignoreMap":23},[28,1060,1061,1063,1065,1067,1069],{"class":30,"line":31},[28,1062,648],{"class":111},[28,1064,251],{"class":107},[28,1066,653],{"class":111},[28,1068,849],{"class":122},[28,1070,1071],{"class":111},"(ctx)\n",[28,1073,1074,1076,1078,1080,1082],{"class":30,"line":37},[28,1075,427],{"class":107},[28,1077,248],{"class":111},[28,1079,267],{"class":107},[28,1081,271],{"class":270},[28,1083,177],{"class":111},[28,1085,1086,1088],{"class":30,"line":43},[28,1087,182],{"class":107},[28,1089,282],{"class":111},[28,1091,1092],{"class":30,"line":49},[28,1093,58],{"class":111},[28,1095,1096],{"class":30,"line":55},[28,1097,295],{"emptyLinePlaceholder":294},[28,1099,1100,1102,1105,1107,1110],{"class":30,"line":193},[28,1101,469],{"class":107},[28,1103,1104],{"class":111}," _, event ",[28,1106,251],{"class":107},[28,1108,1109],{"class":107}," range",[28,1111,1112],{"class":111}," events {\n",[28,1114,1115,1117,1119,1121,1123,1125,1127,1130,1133,1135,1137],{"class":30,"line":205},[28,1116,379],{"class":107},[28,1118,248],{"class":111},[28,1120,251],{"class":107},[28,1122,254],{"class":111},[28,1124,64],{"class":122},[28,1126,222],{"class":111},[28,1128,1129],{"class":122},"toProtoEvent",[28,1131,1132],{"class":111},"(event)); err ",[28,1134,267],{"class":107},[28,1136,271],{"class":270},[28,1138,177],{"class":111},[28,1140,1141,1143],{"class":30,"line":216},[28,1142,404],{"class":107},[28,1144,282],{"class":111},[28,1146,1147],{"class":30,"line":242},[28,1148,411],{"class":111},[28,1150,1151],{"class":30,"line":276},[28,1152,58],{"class":111},[28,1154,1155],{"class":30,"line":285},[28,1156,295],{"emptyLinePlaceholder":294},[28,1158,1159,1162,1164,1166,1169],{"class":30,"line":291},[28,1160,1161],{"class":111},"summary, err ",[28,1163,251],{"class":107},[28,1165,254],{"class":111},[28,1167,1168],{"class":122},"CloseAndRecv",[28,1170,534],{"class":111},[28,1172,1173,1175,1177,1179,1181],{"class":30,"line":298},[28,1174,427],{"class":107},[28,1176,248],{"class":111},[28,1178,267],{"class":107},[28,1180,271],{"class":270},[28,1182,177],{"class":111},[28,1184,1185,1187],{"class":30,"line":307},[28,1186,182],{"class":107},[28,1188,282],{"class":111},[28,1190,1191],{"class":30,"line":313},[28,1192,58],{"class":111},[28,1194,1195],{"class":30,"line":319},[28,1196,295],{"emptyLinePlaceholder":294},[28,1198,1199,1202,1204,1207,1210],{"class":30,"line":781},[28,1200,1201],{"class":111},"fmt.",[28,1203,787],{"class":122},[28,1205,1206],{"class":111},"(summary.",[28,1208,1209],{"class":122},"GetCompletedCount",[28,1211,796],{"class":111},[15,1213,1214,1215,1218],{},"Ключевая операция на клиенте - ",[25,1216,1217],{},"CloseAndRecv()",". Без неё клиент может не получить финальный response.",[74,1220],{},[77,1222,1224],{"id":1223},"bidirectional-streaming","Bidirectional streaming",[15,1226,1227],{},"Обе стороны отправляют поток messages.",[18,1229,1231],{"className":20,"code":1230,"language":22,"meta":23,"style":23},"rpc MentorChat(stream ChatMessage) returns (stream ChatMessage);\n",[25,1232,1233],{"__ignoreMap":23},[28,1234,1235],{"class":30,"line":31},[28,1236,1230],{},[15,1238,1239,1240,1242,1243,146],{},"Это самый гибкий и самый опасный режим: легко получить deadlock, если обе стороны ждут ",[25,1241,68],{},", но никто не делает ",[25,1244,64],{},[15,1246,1247],{},"Типичный клиент читает и пишет в разных горутинах. Канал ошибок закрывать не нужно: в него пишут две горутины, а владелец читает два результата.",[18,1249,1251],{"className":98,"code":1250,"language":100,"meta":641,"style":23},"ctx, cancel := context.WithCancel(ctx)\ndefer cancel()\n\nstream, err := client.MentorChat(ctx)\nif err != nil {\n    return err\n}\n\nerrCh := make(chan error, 2)\n\ngo func() {\n    for {\n        msg, err := stream.Recv()\n        if errors.Is(err, io.EOF) {\n            errCh \u003C- nil\n            return\n        }\n        if err != nil {\n            errCh \u003C- err\n            return\n        }\n\n        fmt.Println(\"mentor:\", msg.GetText())\n    }\n}()\n\ngo func() {\n    for _, text := range userMessages {\n        if err := stream.Send(&chatv1.ChatMessage{Text: text}); err != nil {\n            errCh \u003C- err\n            return\n        }\n    }\n\n    errCh \u003C- stream.CloseSend()\n}()\n\nfor i := 0; i \u003C 2; i++ {\n    if err := \u003C-errCh; err != nil {\n        cancel()\n        return err\n    }\n}\n",[25,1252,1253,1268,1278,1282,1295,1307,1313,1317,1321,1347,1351,1360,1366,1379,1389,1399,1404,1408,1420,1428,1432,1436,1440,1461,1466,1472,1477,1486,1501,1535,1544,1549,1554,1559,1564,1579,1584,1589,1619,1639,1647,1654,1659],{"__ignoreMap":23},[28,1254,1255,1258,1260,1263,1266],{"class":30,"line":31},[28,1256,1257],{"class":111},"ctx, cancel ",[28,1259,251],{"class":107},[28,1261,1262],{"class":111}," context.",[28,1264,1265],{"class":122},"WithCancel",[28,1267,1071],{"class":111},[28,1269,1270,1273,1276],{"class":30,"line":37},[28,1271,1272],{"class":107},"defer",[28,1274,1275],{"class":122}," cancel",[28,1277,534],{"class":111},[28,1279,1280],{"class":30,"line":43},[28,1281,295],{"emptyLinePlaceholder":294},[28,1283,1284,1286,1288,1290,1293],{"class":30,"line":49},[28,1285,648],{"class":111},[28,1287,251],{"class":107},[28,1289,653],{"class":111},[28,1291,1292],{"class":122},"MentorChat",[28,1294,1071],{"class":111},[28,1296,1297,1299,1301,1303,1305],{"class":30,"line":55},[28,1298,427],{"class":107},[28,1300,248],{"class":111},[28,1302,267],{"class":107},[28,1304,271],{"class":270},[28,1306,177],{"class":111},[28,1308,1309,1311],{"class":30,"line":193},[28,1310,182],{"class":107},[28,1312,282],{"class":111},[28,1314,1315],{"class":30,"line":205},[28,1316,58],{"class":111},[28,1318,1319],{"class":30,"line":216},[28,1320,295],{"emptyLinePlaceholder":294},[28,1322,1323,1326,1328,1331,1333,1336,1339,1341,1344],{"class":30,"line":242},[28,1324,1325],{"class":111},"errCh ",[28,1327,251],{"class":107},[28,1329,1330],{"class":122}," make",[28,1332,222],{"class":111},[28,1334,1335],{"class":107},"chan",[28,1337,1338],{"class":107}," error",[28,1340,65],{"class":111},[28,1342,1343],{"class":270},"2",[28,1345,1346],{"class":111},")\n",[28,1348,1349],{"class":30,"line":276},[28,1350,295],{"emptyLinePlaceholder":294},[28,1352,1353,1355,1358],{"class":30,"line":285},[28,1354,100],{"class":107},[28,1356,1357],{"class":107}," func",[28,1359,478],{"class":111},[28,1361,1362,1364],{"class":30,"line":291},[28,1363,898],{"class":107},[28,1365,177],{"class":111},[28,1367,1368,1371,1373,1375,1377],{"class":30,"line":298},[28,1369,1370],{"class":111},"        msg, err ",[28,1372,251],{"class":107},[28,1374,254],{"class":111},[28,1376,68],{"class":122},[28,1378,534],{"class":111},[28,1380,1381,1383,1385,1387],{"class":30,"line":307},[28,1382,918],{"class":107},[28,1384,737],{"class":111},[28,1386,740],{"class":122},[28,1388,743],{"class":111},[28,1390,1391,1394,1397],{"class":30,"line":313},[28,1392,1393],{"class":111},"            errCh ",[28,1395,1396],{"class":107},"\u003C-",[28,1398,304],{"class":270},[28,1400,1401],{"class":30,"line":319},[28,1402,1403],{"class":107},"            return\n",[28,1405,1406],{"class":30,"line":781},[28,1407,993],{"class":111},[28,1409,1410,1412,1414,1416,1418],{"class":30,"line":799},[28,1411,918],{"class":107},[28,1413,248],{"class":111},[28,1415,267],{"class":107},[28,1417,271],{"class":270},[28,1419,177],{"class":111},[28,1421,1422,1424,1426],{"class":30,"line":1018},[28,1423,1393],{"class":111},[28,1425,1396],{"class":107},[28,1427,282],{"class":111},[28,1429,1430],{"class":30,"line":1023},[28,1431,1403],{"class":107},[28,1433,1434],{"class":30,"line":1044},[28,1435,993],{"class":111},[28,1437,1438],{"class":30,"line":1049},[28,1439,295],{"emptyLinePlaceholder":294},[28,1441,1443,1446,1448,1450,1453,1456,1459],{"class":30,"line":1442},23,[28,1444,1445],{"class":111},"        fmt.",[28,1447,787],{"class":122},[28,1449,222],{"class":111},[28,1451,1452],{"class":678},"\"mentor:\"",[28,1454,1455],{"class":111},", msg.",[28,1457,1458],{"class":122},"GetText",[28,1460,796],{"class":111},[28,1462,1464],{"class":30,"line":1463},24,[28,1465,411],{"class":111},[28,1467,1469],{"class":30,"line":1468},25,[28,1470,1471],{"class":111},"}()\n",[28,1473,1475],{"class":30,"line":1474},26,[28,1476,295],{"emptyLinePlaceholder":294},[28,1478,1480,1482,1484],{"class":30,"line":1479},27,[28,1481,100],{"class":107},[28,1483,1357],{"class":107},[28,1485,478],{"class":111},[28,1487,1489,1491,1494,1496,1498],{"class":30,"line":1488},28,[28,1490,898],{"class":107},[28,1492,1493],{"class":111}," _, text ",[28,1495,251],{"class":107},[28,1497,1109],{"class":107},[28,1499,1500],{"class":111}," userMessages {\n",[28,1502,1504,1506,1508,1510,1512,1514,1516,1518,1521,1523,1526,1529,1531,1533],{"class":30,"line":1503},29,[28,1505,918],{"class":107},[28,1507,248],{"class":111},[28,1509,251],{"class":107},[28,1511,254],{"class":111},[28,1513,64],{"class":122},[28,1515,222],{"class":111},[28,1517,661],{"class":107},[28,1519,1520],{"class":122},"chatv1",[28,1522,146],{"class":111},[28,1524,1525],{"class":122},"ChatMessage",[28,1527,1528],{"class":111},"{Text: text}); err ",[28,1530,267],{"class":107},[28,1532,271],{"class":270},[28,1534,177],{"class":111},[28,1536,1538,1540,1542],{"class":30,"line":1537},30,[28,1539,1393],{"class":111},[28,1541,1396],{"class":107},[28,1543,282],{"class":111},[28,1545,1547],{"class":30,"line":1546},31,[28,1548,1403],{"class":107},[28,1550,1552],{"class":30,"line":1551},32,[28,1553,993],{"class":111},[28,1555,1557],{"class":30,"line":1556},33,[28,1558,411],{"class":111},[28,1560,1562],{"class":30,"line":1561},34,[28,1563,295],{"emptyLinePlaceholder":294},[28,1565,1567,1570,1572,1574,1577],{"class":30,"line":1566},35,[28,1568,1569],{"class":111},"    errCh ",[28,1571,1396],{"class":107},[28,1573,254],{"class":111},[28,1575,1576],{"class":122},"CloseSend",[28,1578,534],{"class":111},[28,1580,1582],{"class":30,"line":1581},36,[28,1583,1471],{"class":111},[28,1585,1587],{"class":30,"line":1586},37,[28,1588,295],{"emptyLinePlaceholder":294},[28,1590,1592,1594,1597,1599,1602,1605,1608,1611,1614,1617],{"class":30,"line":1591},38,[28,1593,469],{"class":107},[28,1595,1596],{"class":111}," i ",[28,1598,251],{"class":107},[28,1600,1601],{"class":270}," 0",[28,1603,1604],{"class":111},"; i ",[28,1606,1607],{"class":107},"\u003C",[28,1609,1610],{"class":270}," 2",[28,1612,1613],{"class":111},"; i",[28,1615,1616],{"class":107},"++",[28,1618,177],{"class":111},[28,1620,1622,1624,1626,1628,1630,1633,1635,1637],{"class":30,"line":1621},39,[28,1623,379],{"class":107},[28,1625,248],{"class":111},[28,1627,251],{"class":107},[28,1629,493],{"class":107},[28,1631,1632],{"class":111},"errCh; err ",[28,1634,267],{"class":107},[28,1636,271],{"class":270},[28,1638,177],{"class":111},[28,1640,1642,1645],{"class":30,"line":1641},40,[28,1643,1644],{"class":122},"        cancel",[28,1646,534],{"class":111},[28,1648,1650,1652],{"class":30,"line":1649},41,[28,1651,404],{"class":107},[28,1653,282],{"class":111},[28,1655,1657],{"class":30,"line":1656},42,[28,1658,411],{"class":111},[28,1660,1662],{"class":30,"line":1661},43,[28,1663,58],{"class":111},[15,1665,1666],{},"В реальном коде нужен аккуратный lifecycle: context cancellation, закрытие каналов, логирование ошибок и понятное правило, кто закрывает отправку.",[74,1668],{},[77,1670,1672],{"id":1671},"stream-и-context","Stream и context",[15,1674,1675],{},"В streaming RPC context особенно важен:",[1677,1678,1679,1683,1686,1689,1692],"ul",{},[1680,1681,1682],"li",{},"клиент может закрыть страницу или CLI;",[1680,1684,1685],{},"deadline может истечь в середине потока;",[1680,1687,1688],{},"сервер может остановиться;",[1680,1690,1691],{},"network может оборваться;",[1680,1693,1694],{},"получатель может читать медленнее отправителя.",[15,1696,1697,1698,1700,1701,146],{},"В длинных циклах проверяйте context и корректно обрабатывайте ошибки ",[25,1699,64],{},"\u002F",[25,1702,68],{},[18,1704,1706],{"className":98,"code":1705,"language":100,"meta":23,"style":23},"select {\ncase \u003C-stream.Context().Done():\n    return status.FromContextError(stream.Context().Err()).Err()\ndefault:\n}\n",[25,1707,1708,1715,1732,1754,1761],{"__ignoreMap":23},[28,1709,1710,1713],{"class":30,"line":31},[28,1711,1712],{"class":107},"select",[28,1714,177],{"class":111},[28,1716,1717,1720,1722,1724,1726,1728,1730],{"class":30,"line":37},[28,1718,1719],{"class":107},"case",[28,1721,493],{"class":107},[28,1723,496],{"class":111},[28,1725,199],{"class":122},[28,1727,501],{"class":111},[28,1729,504],{"class":122},[28,1731,507],{"class":111},[28,1733,1734,1736,1738,1740,1742,1744,1746,1748,1750,1752],{"class":30,"line":43},[28,1735,182],{"class":107},[28,1737,514],{"class":111},[28,1739,517],{"class":122},[28,1741,346],{"class":111},[28,1743,199],{"class":122},[28,1745,501],{"class":111},[28,1747,526],{"class":122},[28,1749,529],{"class":111},[28,1751,526],{"class":122},[28,1753,534],{"class":111},[28,1755,1756,1759],{"class":30,"line":49},[28,1757,1758],{"class":107},"default",[28,1760,542],{"class":111},[28,1762,1763],{"class":30,"line":55},[28,1764,58],{"class":111},[74,1766],{},[77,1768,1770],{"id":1769},"interceptors-для-stream","Interceptors для stream",[15,1772,1773],{},"Unary interceptor не покрывает streaming RPC. Для stream нужны stream interceptors:",[18,1775,1777],{"className":98,"code":1776,"language":100,"meta":23,"style":23},"func StreamLoggingInterceptor(\n    srv any,\n    ss grpc.ServerStream,\n    info *grpc.StreamServerInfo,\n    handler grpc.StreamHandler,\n) error {\n    start := time.Now()\n    err := handler(srv, ss)\n\n    slog.Info(\"grpc stream\",\n        \"method\", info.FullMethod,\n        \"code\", status.Code(err).String(),\n        \"duration\", time.Since(start),\n        \"client_stream\", info.IsClientStream,\n        \"server_stream\", info.IsServerStream,\n    )\n\n    return err\n}\n",[25,1778,1779,1788,1798,1813,1830,1844,1852,1867,1880,1884,1899,1907,1926,1940,1948,1956,1960,1964,1970],{"__ignoreMap":23},[28,1780,1781,1783,1786],{"class":30,"line":31},[28,1782,108],{"class":107},[28,1784,1785],{"class":122}," StreamLoggingInterceptor",[28,1787,132],{"class":111},[28,1789,1790,1793,1796],{"class":30,"line":37},[28,1791,1792],{"class":115},"    srv",[28,1794,1795],{"class":122}," any",[28,1797,152],{"class":111},[28,1799,1800,1803,1806,1808,1811],{"class":30,"line":43},[28,1801,1802],{"class":115},"    ss",[28,1804,1805],{"class":122}," grpc",[28,1807,146],{"class":111},[28,1809,1810],{"class":122},"ServerStream",[28,1812,152],{"class":111},[28,1814,1815,1818,1820,1823,1825,1828],{"class":30,"line":49},[28,1816,1817],{"class":115},"    info",[28,1819,140],{"class":107},[28,1821,1822],{"class":122},"grpc",[28,1824,146],{"class":111},[28,1826,1827],{"class":122},"StreamServerInfo",[28,1829,152],{"class":111},[28,1831,1832,1835,1837,1839,1842],{"class":30,"line":55},[28,1833,1834],{"class":115},"    handler",[28,1836,1805],{"class":122},[28,1838,146],{"class":111},[28,1840,1841],{"class":122},"StreamHandler",[28,1843,152],{"class":111},[28,1845,1846,1848,1850],{"class":30,"line":193},[28,1847,126],{"class":111},[28,1849,174],{"class":107},[28,1851,177],{"class":111},[28,1853,1854,1857,1859,1862,1865],{"class":30,"line":205},[28,1855,1856],{"class":111},"    start ",[28,1858,251],{"class":107},[28,1860,1861],{"class":111}," time.",[28,1863,1864],{"class":122},"Now",[28,1866,534],{"class":111},[28,1868,1869,1872,1874,1877],{"class":30,"line":216},[28,1870,1871],{"class":111},"    err ",[28,1873,251],{"class":107},[28,1875,1876],{"class":122}," handler",[28,1878,1879],{"class":111},"(srv, ss)\n",[28,1881,1882],{"class":30,"line":242},[28,1883,295],{"emptyLinePlaceholder":294},[28,1885,1886,1889,1892,1894,1897],{"class":30,"line":276},[28,1887,1888],{"class":111},"    slog.",[28,1890,1891],{"class":122},"Info",[28,1893,222],{"class":111},[28,1895,1896],{"class":678},"\"grpc stream\"",[28,1898,152],{"class":111},[28,1900,1901,1904],{"class":30,"line":285},[28,1902,1903],{"class":678},"        \"method\"",[28,1905,1906],{"class":111},", info.FullMethod,\n",[28,1908,1909,1912,1915,1918,1921,1924],{"class":30,"line":291},[28,1910,1911],{"class":678},"        \"code\"",[28,1913,1914],{"class":111},", status.",[28,1916,1917],{"class":122},"Code",[28,1919,1920],{"class":111},"(err).",[28,1922,1923],{"class":122},"String",[28,1925,202],{"class":111},[28,1927,1928,1931,1934,1937],{"class":30,"line":298},[28,1929,1930],{"class":678},"        \"duration\"",[28,1932,1933],{"class":111},", time.",[28,1935,1936],{"class":122},"Since",[28,1938,1939],{"class":111},"(start),\n",[28,1941,1942,1945],{"class":30,"line":307},[28,1943,1944],{"class":678},"        \"client_stream\"",[28,1946,1947],{"class":111},", info.IsClientStream,\n",[28,1949,1950,1953],{"class":30,"line":313},[28,1951,1952],{"class":678},"        \"server_stream\"",[28,1954,1955],{"class":111},", info.IsServerStream,\n",[28,1957,1958],{"class":30,"line":319},[28,1959,316],{"class":111},[28,1961,1962],{"class":30,"line":781},[28,1963,295],{"emptyLinePlaceholder":294},[28,1965,1966,1968],{"class":30,"line":799},[28,1967,182],{"class":107},[28,1969,282],{"class":111},[28,1971,1972],{"class":30,"line":1018},[28,1973,58],{"class":111},[15,1975,1976],{},"Не логируйте каждое сообщение целиком по умолчанию. В stream может быть много данных, персональная информация или бинарный payload.",[74,1978],{},[77,1980,1982],{"id":1981},"health-reflection-observability","Health, reflection, observability",[15,1984,1985],{},"Для production gRPC обычно добавляют:",[1677,1987,1988,1991,1994,1997,2000,2003,2010],{},[1680,1989,1990],{},"health service, чтобы infrastructure могла проверять живость сервиса;",[1680,1992,1993],{},"reflection в dev\u002Fstaging, чтобы инструменты могли смотреть доступные сервисы;",[1680,1995,1996],{},"logging interceptors;",[1680,1998,1999],{},"metrics по method\u002Fcode\u002Fduration;",[1680,2001,2002],{},"tracing с propagation через metadata;",[1680,2004,2005,2006,2009],{},"recovery interceptor, который превращает panic в ",[25,2007,2008],{},"codes.Internal",";",[1680,2011,2012],{},"limits на размер messages и длительность streams.",[15,2014,2015],{},"Retry надо проектировать осторожно. Повторять безопаснее idempotent операции. Для streaming retry сложнее: нужно понимать, какие messages уже были обработаны.",[74,2017],{},[77,2019,2021],{"id":2020},"практика","Практика",[2023,2024,2025,2045,2067,2088],"ol",{},[1680,2026,2027,2028,146,2031],{},"Реализуйте ",[25,2029,2030],{},"ListLessons(Filter) returns (stream Lesson)",[1677,2032,2033,2036,2039],{},[1680,2034,2035],{},"Сервер отправляет уроки по одному.",[1680,2037,2038],{},"При отмене клиента прекращает работу.",[1680,2040,2041,2042,146],{},"Невалидный фильтр возвращает ",[25,2043,2044],{},"InvalidArgument",[1680,2046,2027,2047,146,2050],{},[25,2048,2049],{},"UploadProgress(stream ProgressEvent) returns (ProgressSummary)",[1677,2051,2052,2057,2062],{},[1680,2053,2054,2055,146],{},"Сервер читает до ",[25,2056,71],{},[1680,2058,2059,2060,146],{},"Пустой stream возвращает ",[25,2061,2044],{},[1680,2063,2064,2065,146],{},"Финальный ответ отправляется через ",[25,2066,980],{},[1680,2068,2027,2069,146,2072],{},[25,2070,2071],{},"MentorChat(stream ChatMessage) returns (stream ChatMessage)",[1677,2073,2074,2077,2082],{},[1680,2075,2076],{},"Клиент параллельно читает и пишет.",[1680,2078,2079,2080,146],{},"Клиент вызывает ",[25,2081,1576],{},[1680,2083,2084,2085,2087],{},"Сервер завершает stream при ",[25,2086,71],{}," или cancellation.",[1680,2089,2090,2091],{},"Добавьте unary и stream logging interceptors.",[1677,2092,2093,2096,2099],{},[1680,2094,2095],{},"Логируйте method, duration, code.",[1680,2097,2098],{},"Payload не логируйте.",[1680,2100,2101,2102,146],{},"Panic превращайте в ",[25,2103,2104],{},"Internal",[74,2106],{},[77,2108,2110],{"id":2109},"типичные-ошибки","Типичные ошибки",[1677,2112,2113,2119,2125,2131,2134,2137,2140,2143],{},[1680,2114,2115,2116,2118],{},"Считать ",[25,2117,71],{}," ошибкой бизнес-логики.",[1680,2120,2121,2122,2124],{},"Забыть ",[25,2123,1217],{}," в client-side streaming.",[1680,2126,2121,2127,2130],{},[25,2128,2129],{},"CloseSend()"," в bidirectional streaming.",[1680,2132,2133],{},"Делать read\u002Fwrite в таком порядке, что обе стороны ждут друг друга.",[1680,2135,2136],{},"Не проверять cancellation в длинном server-side stream.",[1680,2138,2139],{},"Поставить только unary interceptor и думать, что streaming тоже покрыт.",[1680,2141,2142],{},"Логировать весь stream payload.",[1680,2144,2145],{},"Повторять streaming RPC без понимания, какие сообщения уже применились.",[74,2147],{},[77,2149,2151],{"id":2150},"источники","Источники",[1677,2153,2154,2163,2170,2177,2184,2191],{},[1680,2155,2156],{},[2157,2158,2162],"a",{"href":2159,"rel":2160},"https:\u002F\u002Fgrpc.io\u002Fdocs\u002Flanguages\u002Fgo\u002Fbasics\u002F",[2161],"nofollow","gRPC Go Basics",[1680,2164,2165],{},[2157,2166,2169],{"href":2167,"rel":2168},"https:\u002F\u002Fgrpc.io\u002Fdocs\u002Fguides\u002Fflow-control\u002F",[2161],"gRPC Flow Control",[1680,2171,2172],{},[2157,2173,2176],{"href":2174,"rel":2175},"https:\u002F\u002Fgrpc.io\u002Fdocs\u002Fguides\u002Finterceptors\u002F",[2161],"gRPC Interceptors",[1680,2178,2179],{},[2157,2180,2183],{"href":2181,"rel":2182},"https:\u002F\u002Fgrpc.io\u002Fdocs\u002Fguides\u002Fhealth-checking\u002F",[2161],"gRPC Health Checking",[1680,2185,2186],{},[2157,2187,2190],{"href":2188,"rel":2189},"https:\u002F\u002Fgrpc.io\u002Fdocs\u002Fguides\u002Freflection\u002F",[2161],"gRPC Reflection",[1680,2192,2193],{},[2157,2194,2197],{"href":2195,"rel":2196},"https:\u002F\u002Fgrpc.io\u002Fdocs\u002Fguides\u002Fstatus-codes\u002F",[2161],"gRPC Status Codes",[74,2199],{},[77,2201,2203],{"id":2202},"интерактивная-практика","Интерактивная практика",[2205,2206,2209,2215,2234],"quiz",{"answer":1343,"id":2207,"xp":2208},"rpc-streaming-q1","10",[15,2210,2211,2212,2214],{},"Что означает ",[25,2213,71],{}," при чтении client-side stream на сервере?",[2216,2217,2218],"template",{"v-slot:options":23},[1677,2219,2220,2225,2228,2231],{},[1680,2221,2222,2223],{},"Сервер обязан вернуть ",[25,2224,2104],{},[1680,2226,2227],{},"Клиент закончил отправку сообщений, можно сформировать финальный ответ",[1680,2229,2230],{},"Нужно немедленно перезапустить stream",[1680,2232,2233],{},"Это всегда потеря данных",[2216,2235,2236],{"v-slot:explanation":23},[15,2237,2238,2239,2241],{},"Для client-side streaming ",[25,2240,71],{}," обычно означает нормальное завершение входящего потока. После него сервер может вернуть агрегированный ответ.",[2243,2244,2248,2251,2449],"predict",{"answer":2245,"id":2246,"xp":2247},"server\\nclient\\nbidi","rpc-streaming-p1","15",[15,2249,2250],{},"Что выведет программа?",[2216,2252,2253],{"v-slot:code":23},[18,2254,2256],{"className":98,"code":2255,"language":100,"meta":23,"style":23},"package main\n\nimport \"fmt\"\n\nfunc streamKind(clientSendsMany, serverSendsMany bool) string {\n    if clientSendsMany && serverSendsMany {\n        return \"bidi\"\n    }\n    if clientSendsMany {\n        return \"client\"\n    }\n    return \"server\"\n}\n\nfunc main() {\n    fmt.Println(streamKind(false, true))\n    fmt.Println(streamKind(true, false))\n    fmt.Println(streamKind(true, true))\n}\n",[25,2257,2258,2266,2270,2284,2288,2315,2328,2335,2339,2346,2353,2357,2364,2368,2372,2381,2405,2425,2445],{"__ignoreMap":23},[28,2259,2260,2263],{"class":30,"line":31},[28,2261,2262],{"class":107},"package",[28,2264,2265],{"class":122}," main\n",[28,2267,2268],{"class":30,"line":37},[28,2269,295],{"emptyLinePlaceholder":294},[28,2271,2272,2275,2278,2281],{"class":30,"line":43},[28,2273,2274],{"class":107},"import",[28,2276,2277],{"class":678}," \"",[28,2279,2280],{"class":122},"fmt",[28,2282,2283],{"class":678},"\"\n",[28,2285,2286],{"class":30,"line":49},[28,2287,295],{"emptyLinePlaceholder":294},[28,2289,2290,2292,2295,2297,2300,2302,2305,2308,2310,2313],{"class":30,"line":55},[28,2291,108],{"class":107},[28,2293,2294],{"class":122}," streamKind",[28,2296,222],{"class":111},[28,2298,2299],{"class":115},"clientSendsMany",[28,2301,65],{"class":111},[28,2303,2304],{"class":115},"serverSendsMany",[28,2306,2307],{"class":107}," bool",[28,2309,126],{"class":111},[28,2311,2312],{"class":107},"string",[28,2314,177],{"class":111},[28,2316,2317,2319,2322,2325],{"class":30,"line":193},[28,2318,379],{"class":107},[28,2320,2321],{"class":111}," clientSendsMany ",[28,2323,2324],{"class":107},"&&",[28,2326,2327],{"class":111}," serverSendsMany {\n",[28,2329,2330,2332],{"class":30,"line":205},[28,2331,404],{"class":107},[28,2333,2334],{"class":678}," \"bidi\"\n",[28,2336,2337],{"class":30,"line":216},[28,2338,411],{"class":111},[28,2340,2341,2343],{"class":30,"line":242},[28,2342,379],{"class":107},[28,2344,2345],{"class":111}," clientSendsMany {\n",[28,2347,2348,2350],{"class":30,"line":276},[28,2349,404],{"class":107},[28,2351,2352],{"class":678}," \"client\"\n",[28,2354,2355],{"class":30,"line":285},[28,2356,411],{"class":111},[28,2358,2359,2361],{"class":30,"line":291},[28,2360,182],{"class":107},[28,2362,2363],{"class":678}," \"server\"\n",[28,2365,2366],{"class":30,"line":298},[28,2367,58],{"class":111},[28,2369,2370],{"class":30,"line":307},[28,2371,295],{"emptyLinePlaceholder":294},[28,2373,2374,2376,2379],{"class":30,"line":313},[28,2375,108],{"class":107},[28,2377,2378],{"class":122}," main",[28,2380,478],{"class":111},[28,2382,2383,2385,2387,2389,2392,2394,2397,2399,2402],{"class":30,"line":319},[28,2384,784],{"class":111},[28,2386,787],{"class":122},[28,2388,222],{"class":111},[28,2390,2391],{"class":122},"streamKind",[28,2393,222],{"class":111},[28,2395,2396],{"class":270},"false",[28,2398,65],{"class":111},[28,2400,2401],{"class":270},"true",[28,2403,2404],{"class":111},"))\n",[28,2406,2407,2409,2411,2413,2415,2417,2419,2421,2423],{"class":30,"line":781},[28,2408,784],{"class":111},[28,2410,787],{"class":122},[28,2412,222],{"class":111},[28,2414,2391],{"class":122},[28,2416,222],{"class":111},[28,2418,2401],{"class":270},[28,2420,65],{"class":111},[28,2422,2396],{"class":270},[28,2424,2404],{"class":111},[28,2426,2427,2429,2431,2433,2435,2437,2439,2441,2443],{"class":30,"line":799},[28,2428,784],{"class":111},[28,2430,787],{"class":122},[28,2432,222],{"class":111},[28,2434,2391],{"class":122},[28,2436,222],{"class":111},[28,2438,2401],{"class":270},[28,2440,65],{"class":111},[28,2442,2401],{"class":270},[28,2444,2404],{"class":111},[28,2446,2447],{"class":30,"line":1018},[28,2448,58],{"class":111},[2216,2450,2451],{"v-slot:hint":23},[15,2452,2453],{},"Смотри, какая сторона отправляет много сообщений: client, server или обе.",[2455,2456,2460,2467,2612],"code-task",{"expected":2457,"id":2458,"xp":2459},"bad\\nok\\nbad","rpc-streaming-ct1","20",[15,2461,2462,2463,2466],{},"Реализуй ",[25,2464,2465],{},"RetryStreamReview",": автоматический retry streaming RPC безопасен только если операция идемпотентна и известно, что сообщения не применялись частично.",[2216,2468,2469],{"v-slot:template":23},[18,2470,2472],{"className":98,"code":2471,"language":100,"meta":23,"style":23},"package main\n\nimport \"fmt\"\n\nfunc RetryStreamReview(idempotent bool, partialApplied bool) string {\n    return \"bad\"\n}\n\nfunc main() {\n    fmt.Println(RetryStreamReview(false, false))\n    fmt.Println(RetryStreamReview(true, false))\n    fmt.Println(RetryStreamReview(true, true))\n}\n",[25,2473,2474,2480,2484,2494,2498,2525,2532,2536,2540,2548,2568,2588,2608],{"__ignoreMap":23},[28,2475,2476,2478],{"class":30,"line":31},[28,2477,2262],{"class":107},[28,2479,2265],{"class":122},[28,2481,2482],{"class":30,"line":37},[28,2483,295],{"emptyLinePlaceholder":294},[28,2485,2486,2488,2490,2492],{"class":30,"line":43},[28,2487,2274],{"class":107},[28,2489,2277],{"class":678},[28,2491,2280],{"class":122},[28,2493,2283],{"class":678},[28,2495,2496],{"class":30,"line":49},[28,2497,295],{"emptyLinePlaceholder":294},[28,2499,2500,2502,2505,2507,2510,2512,2514,2517,2519,2521,2523],{"class":30,"line":55},[28,2501,108],{"class":107},[28,2503,2504],{"class":122}," RetryStreamReview",[28,2506,222],{"class":111},[28,2508,2509],{"class":115},"idempotent",[28,2511,2307],{"class":107},[28,2513,65],{"class":111},[28,2515,2516],{"class":115},"partialApplied",[28,2518,2307],{"class":107},[28,2520,126],{"class":111},[28,2522,2312],{"class":107},[28,2524,177],{"class":111},[28,2526,2527,2529],{"class":30,"line":193},[28,2528,182],{"class":107},[28,2530,2531],{"class":678}," \"bad\"\n",[28,2533,2534],{"class":30,"line":205},[28,2535,58],{"class":111},[28,2537,2538],{"class":30,"line":216},[28,2539,295],{"emptyLinePlaceholder":294},[28,2541,2542,2544,2546],{"class":30,"line":242},[28,2543,108],{"class":107},[28,2545,2378],{"class":122},[28,2547,478],{"class":111},[28,2549,2550,2552,2554,2556,2558,2560,2562,2564,2566],{"class":30,"line":276},[28,2551,784],{"class":111},[28,2553,787],{"class":122},[28,2555,222],{"class":111},[28,2557,2465],{"class":122},[28,2559,222],{"class":111},[28,2561,2396],{"class":270},[28,2563,65],{"class":111},[28,2565,2396],{"class":270},[28,2567,2404],{"class":111},[28,2569,2570,2572,2574,2576,2578,2580,2582,2584,2586],{"class":30,"line":285},[28,2571,784],{"class":111},[28,2573,787],{"class":122},[28,2575,222],{"class":111},[28,2577,2465],{"class":122},[28,2579,222],{"class":111},[28,2581,2401],{"class":270},[28,2583,65],{"class":111},[28,2585,2396],{"class":270},[28,2587,2404],{"class":111},[28,2589,2590,2592,2594,2596,2598,2600,2602,2604,2606],{"class":30,"line":291},[28,2591,784],{"class":111},[28,2593,787],{"class":122},[28,2595,222],{"class":111},[28,2597,2465],{"class":122},[28,2599,222],{"class":111},[28,2601,2401],{"class":270},[28,2603,65],{"class":111},[28,2605,2401],{"class":270},[28,2607,2404],{"class":111},[28,2609,2610],{"class":30,"line":298},[28,2611,58],{"class":111},[2216,2613,2614],{"v-slot:hints":23},[1677,2615,2616,2619,2622],{},[1680,2617,2618],{},"Неидемпотентный stream ретраить опасно",[1680,2620,2621],{},"Частично применённые сообщения могут дать дубли",[1680,2623,2624,2625,2628,2629],{},"Только ",[25,2626,2627],{},"idempotent && !partialApplied"," — ",[25,2630,2631],{},"\"ok\"",[2633,2634,2635],"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 .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":23,"searchDepth":37,"depth":37,"links":2637},[2638,2639,2640,2641,2642,2643,2644,2645,2646,2647],{"id":79,"depth":37,"text":80},{"id":811,"depth":37,"text":812},{"id":1223,"depth":37,"text":1224},{"id":1671,"depth":37,"text":1672},{"id":1769,"depth":37,"text":1770},{"id":1981,"depth":37,"text":1982},{"id":2020,"depth":37,"text":2021},{"id":2109,"depth":37,"text":2110},{"id":2150,"depth":37,"text":2151},{"id":2202,"depth":37,"text":2203},1781022067021]