[{"data":1,"prerenderedAt":2899},["ShallowReactive",2],{"content-\u002F05-web\u002F02-echo":3},{"id":4,"title":5,"body":6,"description":17,"difficulty":2885,"extension":2886,"meta":2887,"module":2888,"navigation":182,"next":2889,"order":179,"path":2890,"prev":419,"seo":2891,"slug":1800,"stem":2892,"tags":2893,"__hash__":2898},"content\u002F05-web\u002F02-echo\u002Findex.md","Основы бэкенда, Echo",{"type":7,"value":8,"toc":2866},"minimark",[9,14,18,21,26,29,40,43,49,54,57,63,70,76,82,85,87,91,97,120,126,128,132,135,158,162,364,378,470,472,476,489,842,844,848,1053,1055,1059,1068,1333,1336,1535,1537,1541,1544,1768,1772,1975,1988,2048,2050,2054,2061,2300,2303,2382,2384,2388,2645,2647,2651,2654,2752,2764,2766,2770,2779,2804,2815,2827,2845,2853,2855,2862],[10,11,13],"h1",{"id":12},"echo-и-основы-backend-разработки-на-go","Echo и основы backend-разработки на Go",[15,16,17],"p",{},"Прежде чем перейти к Echo, разберём как работает backend на базовом уровне. Это то, что часто подразумевается само собой, но нигде явно не объясняется — особенно людям, которые пришли в backend без опыта фронтенда или системного программирования.",[19,20],"hr",{},[22,23,25],"h2",{"id":24},"как-работает-backend-от-запроса-до-базы-данных","Как работает backend: от запроса до базы данных",[15,27,28],{},"Представьте простое действие: пользователь нажимает кнопку \"Создать задачу\" в Todo-приложении. Вот что происходит:",[30,31,36],"pre",{"className":32,"code":34,"language":35},[33],"language-text","Браузер\u002FКлиент                    Ваш Go-сервер                    База данных\n     │                                  │                                │\n     │  POST \u002Ftodos                     │                                │\n     │  Content-Type: application\u002Fjson  │                                │\n     │  {\"title\": \"Купить молоко\"}      │                                │\n     │ ───────────────────────────────► │                                │\n     │                                  │  Декодируем JSON               │\n     │                                  │  Валидируем данные             │\n     │                                  │  INSERT INTO todos...          │\n     │                                  │ ──────────────────────────────►│\n     │                                  │                                │\n     │                                  │  id=42, created_at=...         │\n     │                                  │ ◄──────────────────────────────│\n     │                                  │  Формируем JSON-ответ          │\n     │  HTTP 201 Created                │                                │\n     │  {\"id\": 42, \"title\": \"...\"}      │                                │\n     │ ◄─────────────────────────────── │                                │\n","text",[37,38,34],"code",{"__ignoreMap":39},"",[15,41,42],{},"Каждый HTTP-запрос проходит один и тот же путь внутри сервера:",[30,44,47],{"className":45,"code":46,"language":35},[33],"Входящий запрос\n      │\n      ▼\n Middleware        ← логирование, CORS, аутентификация\n      │\n      ▼\n  Роутер           ← определяет какой обработчик вызвать\n      │\n      ▼\n Обработчик        ← бизнес-логика запроса\n      │\n      ├── Декодирование JSON из тела запроса\n      ├── Валидация входных данных\n      ├── Вызов сервисного слоя\n      │       └── Запрос к базе данных\n      └── Кодирование ответа в JSON\n",[37,48,46],{"__ignoreMap":39},[50,51,53],"h3",{"id":52},"слои-приложения","Слои приложения",[15,55,56],{},"Хорошо структурированный backend делится на слои. Каждый слой отвечает за своё:",[30,58,61],{"className":59,"code":60,"language":35},[33],"┌─────────────────────────────────────────┐\n│           Handler (Transport)           │  HTTP: парсинг запроса, ответ\n├─────────────────────────────────────────┤\n│              Service (Logic)            │  Бизнес-логика\n├─────────────────────────────────────────┤\n│           Repository (Storage)          │  Работа с базой данных\n├─────────────────────────────────────────┤\n│              Database                   │  PostgreSQL, MySQL...\n└─────────────────────────────────────────┘\n",[37,62,60],{"__ignoreMap":39},[15,64,65,69],{},[66,67,68],"strong",{},"Handler"," — знает про HTTP. Читает запрос, вызывает сервис, пишет ответ. Не знает про базу данных.",[15,71,72,75],{},[66,73,74],{},"Service"," — знает про бизнес-логику. Валидирует, считает, принимает решения. Не знает про HTTP и базу данных.",[15,77,78,81],{},[66,79,80],{},"Repository"," — знает про базу данных. Выполняет SQL-запросы. Не знает про HTTP и бизнес-логику.",[15,83,84],{},"Такое разделение делает код тестируемым: каждый слой можно проверить независимо через интерфейсы и моки.",[19,86],{},[22,88,90],{"id":89},"почему-фреймворк-а-не-чистый-nethttp","Почему фреймворк, а не чистый net\u002Fhttp",[15,92,93,96],{},[37,94,95],{},"net\u002Fhttp"," — отличная база, но в реальных проектах постоянно нужно одно и то же:",[98,99,100,108,111,114,117],"ul",{},[101,102,103,104,107],"li",{},"Парсинг path-параметров (",[37,105,106],{},"\u002Fusers\u002F:id",")",[101,109,110],{},"Валидация входных данных",[101,112,113],{},"Группировка роутов с общим префиксом и middleware",[101,115,116],{},"Удобная работа с JSON",[101,118,119],{},"Структурированные ошибки",[15,121,122,123,125],{},"Писать это самому каждый раз — потеря времени. Фреймворки предоставляют готовые решения, оставаясь при этом тонкой обёрткой над ",[37,124,95],{},".",[19,127],{},[22,129,131],{"id":130},"echo-обзор","Echo — обзор",[15,133,134],{},"Echo — минималистичный высокопроизводительный веб-фреймворк. Его философия близка к философии Go: явное лучше неявного, минимум магии, максимум производительности.",[30,136,140],{"className":137,"code":138,"language":139,"meta":39,"style":39},"language-bash shiki shiki-themes github-dark","go get github.com\u002Flabstack\u002Fecho\u002Fv4\n","bash",[37,141,142],{"__ignoreMap":39},[143,144,147,151,155],"span",{"class":145,"line":146},"line",1,[143,148,150],{"class":149},"svObZ","go",[143,152,154],{"class":153},"sU2Wk"," get",[143,156,157],{"class":153}," github.com\u002Flabstack\u002Fecho\u002Fv4\n",[50,159,161],{"id":160},"минимальное-приложение","Минимальное приложение",[30,163,166],{"className":164,"code":165,"language":150,"meta":39,"style":39},"language-go shiki shiki-themes github-dark","package main\n\nimport (\n    \"net\u002Fhttp\"\n    \"github.com\u002Flabstack\u002Fecho\u002Fv4\"\n)\n\nfunc main() {\n    e := echo.New()\n\n    e.GET(\"\u002F\", func(c echo.Context) error {\n        return c.String(http.StatusOK, \"Hello, World!\")\n    })\n\n    e.Logger.Fatal(e.Start(\":8080\"))\n}\n",[37,167,168,177,184,194,205,215,221,226,238,256,261,304,324,330,335,358],{"__ignoreMap":39},[143,169,170,174],{"class":145,"line":146},[143,171,173],{"class":172},"snl16","package",[143,175,176],{"class":149}," main\n",[143,178,180],{"class":145,"line":179},2,[143,181,183],{"emptyLinePlaceholder":182},true,"\n",[143,185,187,190],{"class":145,"line":186},3,[143,188,189],{"class":172},"import",[143,191,193],{"class":192},"s95oV"," (\n",[143,195,197,200,202],{"class":145,"line":196},4,[143,198,199],{"class":153},"    \"",[143,201,95],{"class":149},[143,203,204],{"class":153},"\"\n",[143,206,208,210,213],{"class":145,"line":207},5,[143,209,199],{"class":153},[143,211,212],{"class":149},"github.com\u002Flabstack\u002Fecho\u002Fv4",[143,214,204],{"class":153},[143,216,218],{"class":145,"line":217},6,[143,219,220],{"class":192},")\n",[143,222,224],{"class":145,"line":223},7,[143,225,183],{"emptyLinePlaceholder":182},[143,227,229,232,235],{"class":145,"line":228},8,[143,230,231],{"class":172},"func",[143,233,234],{"class":149}," main",[143,236,237],{"class":192},"() {\n",[143,239,241,244,247,250,253],{"class":145,"line":240},9,[143,242,243],{"class":192},"    e ",[143,245,246],{"class":172},":=",[143,248,249],{"class":192}," echo.",[143,251,252],{"class":149},"New",[143,254,255],{"class":192},"()\n",[143,257,259],{"class":145,"line":258},10,[143,260,183],{"emptyLinePlaceholder":182},[143,262,264,267,270,273,276,279,281,283,287,290,292,295,298,301],{"class":145,"line":263},11,[143,265,266],{"class":192},"    e.",[143,268,269],{"class":149},"GET",[143,271,272],{"class":192},"(",[143,274,275],{"class":153},"\"\u002F\"",[143,277,278],{"class":192},", ",[143,280,231],{"class":172},[143,282,272],{"class":192},[143,284,286],{"class":285},"s9osk","c",[143,288,289],{"class":149}," echo",[143,291,125],{"class":192},[143,293,294],{"class":149},"Context",[143,296,297],{"class":192},") ",[143,299,300],{"class":172},"error",[143,302,303],{"class":192}," {\n",[143,305,307,310,313,316,319,322],{"class":145,"line":306},12,[143,308,309],{"class":172},"        return",[143,311,312],{"class":192}," c.",[143,314,315],{"class":149},"String",[143,317,318],{"class":192},"(http.StatusOK, ",[143,320,321],{"class":153},"\"Hello, World!\"",[143,323,220],{"class":192},[143,325,327],{"class":145,"line":326},13,[143,328,329],{"class":192},"    })\n",[143,331,333],{"class":145,"line":332},14,[143,334,183],{"emptyLinePlaceholder":182},[143,336,338,341,344,347,350,352,355],{"class":145,"line":337},15,[143,339,340],{"class":192},"    e.Logger.",[143,342,343],{"class":149},"Fatal",[143,345,346],{"class":192},"(e.",[143,348,349],{"class":149},"Start",[143,351,272],{"class":192},[143,353,354],{"class":153},"\":8080\"",[143,356,357],{"class":192},"))\n",[143,359,361],{"class":145,"line":360},16,[143,362,363],{"class":192},"}\n",[15,365,366,367,369,370,373,374,377],{},"Главное отличие от ",[37,368,95],{}," — вместо двух аргументов ",[37,371,372],{},"(w, r)"," используется один ",[37,375,376],{},"echo.Context",", который объединяет запрос и ответ:",[30,379,381],{"className":164,"code":380,"language":150,"meta":39,"style":39},"\u002F\u002F net\u002Fhttp\nfunc handler(w http.ResponseWriter, r *http.Request) { ... }\n\n\u002F\u002F Echo\nfunc handler(c echo.Context) error { ... }\n",[37,382,383,389,434,438,443],{"__ignoreMap":39},[143,384,385],{"class":145,"line":146},[143,386,388],{"class":387},"sAwPA","\u002F\u002F net\u002Fhttp\n",[143,390,391,393,396,398,401,404,406,409,411,414,417,420,422,425,428,431],{"class":145,"line":179},[143,392,231],{"class":172},[143,394,395],{"class":149}," handler",[143,397,272],{"class":192},[143,399,400],{"class":285},"w",[143,402,403],{"class":149}," http",[143,405,125],{"class":192},[143,407,408],{"class":149},"ResponseWriter",[143,410,278],{"class":192},[143,412,413],{"class":285},"r",[143,415,416],{"class":172}," *",[143,418,419],{"class":149},"http",[143,421,125],{"class":192},[143,423,424],{"class":149},"Request",[143,426,427],{"class":192},") { ",[143,429,430],{"class":172},"...",[143,432,433],{"class":192}," }\n",[143,435,436],{"class":145,"line":186},[143,437,183],{"emptyLinePlaceholder":182},[143,439,440],{"class":145,"line":196},[143,441,442],{"class":387},"\u002F\u002F Echo\n",[143,444,445,447,449,451,453,455,457,459,461,463,466,468],{"class":145,"line":207},[143,446,231],{"class":172},[143,448,395],{"class":149},[143,450,272],{"class":192},[143,452,286],{"class":285},[143,454,289],{"class":149},[143,456,125],{"class":192},[143,458,294],{"class":149},[143,460,297],{"class":192},[143,462,300],{"class":172},[143,464,465],{"class":192}," { ",[143,467,430],{"class":172},[143,469,433],{"class":192},[19,471],{},[22,473,475],{"id":474},"echocontext-сердце-фреймворка","echo.Context — сердце фреймворка",[15,477,478,480,481,484,485,488],{},[37,479,376],{}," — это обёртка над стандартными ",[37,482,483],{},"http.ResponseWriter"," и ",[37,486,487],{},"*http.Request",", дополненная удобными методами:",[30,490,492],{"className":164,"code":491,"language":150,"meta":39,"style":39},"func handler(c echo.Context) error {\n    \u002F\u002F ─── Запрос ───────────────────────────────────────────\n\n    \u002F\u002F Path параметры (\u002Fusers\u002F:id)\n    id := c.Param(\"id\")\n\n    \u002F\u002F Query параметры (?page=1&limit=10)\n    page := c.QueryParam(\"page\")\n    limit := c.QueryParamOrDefault(\"limit\", \"10\")\n\n    \u002F\u002F Заголовки\n    token := c.Request().Header.Get(\"Authorization\")\n\n    \u002F\u002F Декодирование JSON тела запроса\n    var req CreateTodoRequest\n    if err := c.Bind(&req); err != nil {\n        return err\n    }\n\n    \u002F\u002F ─── Ответ ────────────────────────────────────────────\n\n    \u002F\u002F JSON ответ\n    return c.JSON(http.StatusOK, map[string]string{\"id\": id})\n\n    \u002F\u002F Строка\n    return c.String(http.StatusOK, \"hello\")\n\n    \u002F\u002F Статус без тела\n    return c.NoContent(http.StatusNoContent)\n\n    \u002F\u002F Редирект\n    return c.Redirect(http.StatusMovedPermanently, \"\u002Fnew-path\")\n}\n",[37,493,494,516,521,525,530,549,553,558,577,601,605,610,634,638,643,654,686,694,700,705,711,716,722,757,762,768,784,789,795,808,813,819,837],{"__ignoreMap":39},[143,495,496,498,500,502,504,506,508,510,512,514],{"class":145,"line":146},[143,497,231],{"class":172},[143,499,395],{"class":149},[143,501,272],{"class":192},[143,503,286],{"class":285},[143,505,289],{"class":149},[143,507,125],{"class":192},[143,509,294],{"class":149},[143,511,297],{"class":192},[143,513,300],{"class":172},[143,515,303],{"class":192},[143,517,518],{"class":145,"line":179},[143,519,520],{"class":387},"    \u002F\u002F ─── Запрос ───────────────────────────────────────────\n",[143,522,523],{"class":145,"line":186},[143,524,183],{"emptyLinePlaceholder":182},[143,526,527],{"class":145,"line":196},[143,528,529],{"class":387},"    \u002F\u002F Path параметры (\u002Fusers\u002F:id)\n",[143,531,532,535,537,539,542,544,547],{"class":145,"line":207},[143,533,534],{"class":192},"    id ",[143,536,246],{"class":172},[143,538,312],{"class":192},[143,540,541],{"class":149},"Param",[143,543,272],{"class":192},[143,545,546],{"class":153},"\"id\"",[143,548,220],{"class":192},[143,550,551],{"class":145,"line":217},[143,552,183],{"emptyLinePlaceholder":182},[143,554,555],{"class":145,"line":223},[143,556,557],{"class":387},"    \u002F\u002F Query параметры (?page=1&limit=10)\n",[143,559,560,563,565,567,570,572,575],{"class":145,"line":228},[143,561,562],{"class":192},"    page ",[143,564,246],{"class":172},[143,566,312],{"class":192},[143,568,569],{"class":149},"QueryParam",[143,571,272],{"class":192},[143,573,574],{"class":153},"\"page\"",[143,576,220],{"class":192},[143,578,579,582,584,586,589,591,594,596,599],{"class":145,"line":240},[143,580,581],{"class":192},"    limit ",[143,583,246],{"class":172},[143,585,312],{"class":192},[143,587,588],{"class":149},"QueryParamOrDefault",[143,590,272],{"class":192},[143,592,593],{"class":153},"\"limit\"",[143,595,278],{"class":192},[143,597,598],{"class":153},"\"10\"",[143,600,220],{"class":192},[143,602,603],{"class":145,"line":258},[143,604,183],{"emptyLinePlaceholder":182},[143,606,607],{"class":145,"line":263},[143,608,609],{"class":387},"    \u002F\u002F Заголовки\n",[143,611,612,615,617,619,621,624,627,629,632],{"class":145,"line":306},[143,613,614],{"class":192},"    token ",[143,616,246],{"class":172},[143,618,312],{"class":192},[143,620,424],{"class":149},[143,622,623],{"class":192},"().Header.",[143,625,626],{"class":149},"Get",[143,628,272],{"class":192},[143,630,631],{"class":153},"\"Authorization\"",[143,633,220],{"class":192},[143,635,636],{"class":145,"line":326},[143,637,183],{"emptyLinePlaceholder":182},[143,639,640],{"class":145,"line":332},[143,641,642],{"class":387},"    \u002F\u002F Декодирование JSON тела запроса\n",[143,644,645,648,651],{"class":145,"line":337},[143,646,647],{"class":172},"    var",[143,649,650],{"class":192}," req ",[143,652,653],{"class":149},"CreateTodoRequest\n",[143,655,656,659,662,664,666,669,671,674,677,680,684],{"class":145,"line":360},[143,657,658],{"class":172},"    if",[143,660,661],{"class":192}," err ",[143,663,246],{"class":172},[143,665,312],{"class":192},[143,667,668],{"class":149},"Bind",[143,670,272],{"class":192},[143,672,673],{"class":172},"&",[143,675,676],{"class":192},"req); err ",[143,678,679],{"class":172},"!=",[143,681,683],{"class":682},"sDLfK"," nil",[143,685,303],{"class":192},[143,687,689,691],{"class":145,"line":688},17,[143,690,309],{"class":172},[143,692,693],{"class":192}," err\n",[143,695,697],{"class":145,"line":696},18,[143,698,699],{"class":192},"    }\n",[143,701,703],{"class":145,"line":702},19,[143,704,183],{"emptyLinePlaceholder":182},[143,706,708],{"class":145,"line":707},20,[143,709,710],{"class":387},"    \u002F\u002F ─── Ответ ────────────────────────────────────────────\n",[143,712,714],{"class":145,"line":713},21,[143,715,183],{"emptyLinePlaceholder":182},[143,717,719],{"class":145,"line":718},22,[143,720,721],{"class":387},"    \u002F\u002F JSON ответ\n",[143,723,725,728,730,733,735,738,741,744,747,749,752,754],{"class":145,"line":724},23,[143,726,727],{"class":172},"    return",[143,729,312],{"class":192},[143,731,732],{"class":149},"JSON",[143,734,318],{"class":192},[143,736,737],{"class":172},"map",[143,739,740],{"class":192},"[",[143,742,743],{"class":172},"string",[143,745,746],{"class":192},"]",[143,748,743],{"class":172},[143,750,751],{"class":192},"{",[143,753,546],{"class":153},[143,755,756],{"class":192},": id})\n",[143,758,760],{"class":145,"line":759},24,[143,761,183],{"emptyLinePlaceholder":182},[143,763,765],{"class":145,"line":764},25,[143,766,767],{"class":387},"    \u002F\u002F Строка\n",[143,769,771,773,775,777,779,782],{"class":145,"line":770},26,[143,772,727],{"class":172},[143,774,312],{"class":192},[143,776,315],{"class":149},[143,778,318],{"class":192},[143,780,781],{"class":153},"\"hello\"",[143,783,220],{"class":192},[143,785,787],{"class":145,"line":786},27,[143,788,183],{"emptyLinePlaceholder":182},[143,790,792],{"class":145,"line":791},28,[143,793,794],{"class":387},"    \u002F\u002F Статус без тела\n",[143,796,798,800,802,805],{"class":145,"line":797},29,[143,799,727],{"class":172},[143,801,312],{"class":192},[143,803,804],{"class":149},"NoContent",[143,806,807],{"class":192},"(http.StatusNoContent)\n",[143,809,811],{"class":145,"line":810},30,[143,812,183],{"emptyLinePlaceholder":182},[143,814,816],{"class":145,"line":815},31,[143,817,818],{"class":387},"    \u002F\u002F Редирект\n",[143,820,822,824,826,829,832,835],{"class":145,"line":821},32,[143,823,727],{"class":172},[143,825,312],{"class":192},[143,827,828],{"class":149},"Redirect",[143,830,831],{"class":192},"(http.StatusMovedPermanently, ",[143,833,834],{"class":153},"\"\u002Fnew-path\"",[143,836,220],{"class":192},[143,838,840],{"class":145,"line":839},33,[143,841,363],{"class":192},[19,843],{},[22,845,847],{"id":846},"роутинг","Роутинг",[30,849,851],{"className":164,"code":850,"language":150,"meta":39,"style":39},"e := echo.New()\n\n\u002F\u002F Базовые методы\ne.GET(\"\u002Fusers\", listUsers)\ne.POST(\"\u002Fusers\", createUser)\ne.GET(\"\u002Fusers\u002F:id\", getUser)\ne.PUT(\"\u002Fusers\u002F:id\", updateUser)\ne.DELETE(\"\u002Fusers\u002F:id\", deleteUser)\n\n\u002F\u002F Группировка роутов\napi := e.Group(\"\u002Fapi\u002Fv1\")\napi.GET(\"\u002Fusers\", listUsers)\napi.POST(\"\u002Fusers\", createUser)\n\n\u002F\u002F Группа с middleware (например, аутентификация только для этих роутов)\nprotected := api.Group(\"\u002Fadmin\")\nprotected.Use(authMiddleware)\nprotected.GET(\"\u002Fstats\", getStats)\n",[37,852,853,866,870,875,890,904,918,932,946,950,955,975,988,1000,1004,1009,1028,1039],{"__ignoreMap":39},[143,854,855,858,860,862,864],{"class":145,"line":146},[143,856,857],{"class":192},"e ",[143,859,246],{"class":172},[143,861,249],{"class":192},[143,863,252],{"class":149},[143,865,255],{"class":192},[143,867,868],{"class":145,"line":179},[143,869,183],{"emptyLinePlaceholder":182},[143,871,872],{"class":145,"line":186},[143,873,874],{"class":387},"\u002F\u002F Базовые методы\n",[143,876,877,880,882,884,887],{"class":145,"line":196},[143,878,879],{"class":192},"e.",[143,881,269],{"class":149},[143,883,272],{"class":192},[143,885,886],{"class":153},"\"\u002Fusers\"",[143,888,889],{"class":192},", listUsers)\n",[143,891,892,894,897,899,901],{"class":145,"line":207},[143,893,879],{"class":192},[143,895,896],{"class":149},"POST",[143,898,272],{"class":192},[143,900,886],{"class":153},[143,902,903],{"class":192},", createUser)\n",[143,905,906,908,910,912,915],{"class":145,"line":217},[143,907,879],{"class":192},[143,909,269],{"class":149},[143,911,272],{"class":192},[143,913,914],{"class":153},"\"\u002Fusers\u002F:id\"",[143,916,917],{"class":192},", getUser)\n",[143,919,920,922,925,927,929],{"class":145,"line":223},[143,921,879],{"class":192},[143,923,924],{"class":149},"PUT",[143,926,272],{"class":192},[143,928,914],{"class":153},[143,930,931],{"class":192},", updateUser)\n",[143,933,934,936,939,941,943],{"class":145,"line":228},[143,935,879],{"class":192},[143,937,938],{"class":149},"DELETE",[143,940,272],{"class":192},[143,942,914],{"class":153},[143,944,945],{"class":192},", deleteUser)\n",[143,947,948],{"class":145,"line":240},[143,949,183],{"emptyLinePlaceholder":182},[143,951,952],{"class":145,"line":258},[143,953,954],{"class":387},"\u002F\u002F Группировка роутов\n",[143,956,957,960,962,965,968,970,973],{"class":145,"line":263},[143,958,959],{"class":192},"api ",[143,961,246],{"class":172},[143,963,964],{"class":192}," e.",[143,966,967],{"class":149},"Group",[143,969,272],{"class":192},[143,971,972],{"class":153},"\"\u002Fapi\u002Fv1\"",[143,974,220],{"class":192},[143,976,977,980,982,984,986],{"class":145,"line":306},[143,978,979],{"class":192},"api.",[143,981,269],{"class":149},[143,983,272],{"class":192},[143,985,886],{"class":153},[143,987,889],{"class":192},[143,989,990,992,994,996,998],{"class":145,"line":326},[143,991,979],{"class":192},[143,993,896],{"class":149},[143,995,272],{"class":192},[143,997,886],{"class":153},[143,999,903],{"class":192},[143,1001,1002],{"class":145,"line":332},[143,1003,183],{"emptyLinePlaceholder":182},[143,1005,1006],{"class":145,"line":337},[143,1007,1008],{"class":387},"\u002F\u002F Группа с middleware (например, аутентификация только для этих роутов)\n",[143,1010,1011,1014,1016,1019,1021,1023,1026],{"class":145,"line":360},[143,1012,1013],{"class":192},"protected ",[143,1015,246],{"class":172},[143,1017,1018],{"class":192}," api.",[143,1020,967],{"class":149},[143,1022,272],{"class":192},[143,1024,1025],{"class":153},"\"\u002Fadmin\"",[143,1027,220],{"class":192},[143,1029,1030,1033,1036],{"class":145,"line":688},[143,1031,1032],{"class":192},"protected.",[143,1034,1035],{"class":149},"Use",[143,1037,1038],{"class":192},"(authMiddleware)\n",[143,1040,1041,1043,1045,1047,1050],{"class":145,"line":696},[143,1042,1032],{"class":192},[143,1044,269],{"class":149},[143,1046,272],{"class":192},[143,1048,1049],{"class":153},"\"\u002Fstats\"",[143,1051,1052],{"class":192},", getStats)\n",[19,1054],{},[22,1056,1058],{"id":1057},"bind-и-валидация","Bind и валидация",[15,1060,1061,1063,1064,1067],{},[37,1062,668],{}," автоматически декодирует данные из запроса в структуру — JSON, form-data, query параметры — в зависимости от ",[37,1065,1066],{},"Content-Type",":",[30,1069,1071],{"className":164,"code":1070,"language":150,"meta":39,"style":39},"type CreateTodoRequest struct {\n    Title    string `json:\"title\"    validate:\"required,min=1,max=200\"`\n    Priority int    `json:\"priority\" validate:\"min=1,max=3\"`\n}\n\nfunc createTodo(c echo.Context) error {\n    var req CreateTodoRequest\n\n    \u002F\u002F Bind декодирует JSON → структуру\n    if err := c.Bind(&req); err != nil {\n        return echo.NewHTTPError(http.StatusBadRequest, \"invalid request body\")\n    }\n\n    \u002F\u002F Validate запускает валидацию по тегам\n    if err := c.Validate(&req); err != nil {\n        return err \u002F\u002F Echo вернёт 400 с описанием ошибок\n    }\n\n    \u002F\u002F Дальше работаем с валидными данными\n    todo, err := todoService.Create(c.Request().Context(), req)\n    if err != nil {\n        return echo.NewHTTPError(http.StatusInternalServerError, \"failed to create todo\")\n    }\n\n    return c.JSON(http.StatusCreated, todo)\n}\n",[37,1072,1073,1086,1096,1107,1111,1115,1138,1146,1150,1155,1179,1196,1200,1204,1209,1234,1243,1247,1251,1256,1282,1294,1310,1314,1318,1329],{"__ignoreMap":39},[143,1074,1075,1078,1081,1084],{"class":145,"line":146},[143,1076,1077],{"class":172},"type",[143,1079,1080],{"class":149}," CreateTodoRequest",[143,1082,1083],{"class":172}," struct",[143,1085,303],{"class":192},[143,1087,1088,1091,1093],{"class":145,"line":179},[143,1089,1090],{"class":192},"    Title    ",[143,1092,743],{"class":172},[143,1094,1095],{"class":153}," `json:\"title\"    validate:\"required,min=1,max=200\"`\n",[143,1097,1098,1101,1104],{"class":145,"line":186},[143,1099,1100],{"class":192},"    Priority ",[143,1102,1103],{"class":172},"int",[143,1105,1106],{"class":153},"    `json:\"priority\" validate:\"min=1,max=3\"`\n",[143,1108,1109],{"class":145,"line":196},[143,1110,363],{"class":192},[143,1112,1113],{"class":145,"line":207},[143,1114,183],{"emptyLinePlaceholder":182},[143,1116,1117,1119,1122,1124,1126,1128,1130,1132,1134,1136],{"class":145,"line":217},[143,1118,231],{"class":172},[143,1120,1121],{"class":149}," createTodo",[143,1123,272],{"class":192},[143,1125,286],{"class":285},[143,1127,289],{"class":149},[143,1129,125],{"class":192},[143,1131,294],{"class":149},[143,1133,297],{"class":192},[143,1135,300],{"class":172},[143,1137,303],{"class":192},[143,1139,1140,1142,1144],{"class":145,"line":223},[143,1141,647],{"class":172},[143,1143,650],{"class":192},[143,1145,653],{"class":149},[143,1147,1148],{"class":145,"line":228},[143,1149,183],{"emptyLinePlaceholder":182},[143,1151,1152],{"class":145,"line":240},[143,1153,1154],{"class":387},"    \u002F\u002F Bind декодирует JSON → структуру\n",[143,1156,1157,1159,1161,1163,1165,1167,1169,1171,1173,1175,1177],{"class":145,"line":258},[143,1158,658],{"class":172},[143,1160,661],{"class":192},[143,1162,246],{"class":172},[143,1164,312],{"class":192},[143,1166,668],{"class":149},[143,1168,272],{"class":192},[143,1170,673],{"class":172},[143,1172,676],{"class":192},[143,1174,679],{"class":172},[143,1176,683],{"class":682},[143,1178,303],{"class":192},[143,1180,1181,1183,1185,1188,1191,1194],{"class":145,"line":263},[143,1182,309],{"class":172},[143,1184,249],{"class":192},[143,1186,1187],{"class":149},"NewHTTPError",[143,1189,1190],{"class":192},"(http.StatusBadRequest, ",[143,1192,1193],{"class":153},"\"invalid request body\"",[143,1195,220],{"class":192},[143,1197,1198],{"class":145,"line":306},[143,1199,699],{"class":192},[143,1201,1202],{"class":145,"line":326},[143,1203,183],{"emptyLinePlaceholder":182},[143,1205,1206],{"class":145,"line":332},[143,1207,1208],{"class":387},"    \u002F\u002F Validate запускает валидацию по тегам\n",[143,1210,1211,1213,1215,1217,1219,1222,1224,1226,1228,1230,1232],{"class":145,"line":337},[143,1212,658],{"class":172},[143,1214,661],{"class":192},[143,1216,246],{"class":172},[143,1218,312],{"class":192},[143,1220,1221],{"class":149},"Validate",[143,1223,272],{"class":192},[143,1225,673],{"class":172},[143,1227,676],{"class":192},[143,1229,679],{"class":172},[143,1231,683],{"class":682},[143,1233,303],{"class":192},[143,1235,1236,1238,1240],{"class":145,"line":360},[143,1237,309],{"class":172},[143,1239,661],{"class":192},[143,1241,1242],{"class":387},"\u002F\u002F Echo вернёт 400 с описанием ошибок\n",[143,1244,1245],{"class":145,"line":688},[143,1246,699],{"class":192},[143,1248,1249],{"class":145,"line":696},[143,1250,183],{"emptyLinePlaceholder":182},[143,1252,1253],{"class":145,"line":702},[143,1254,1255],{"class":387},"    \u002F\u002F Дальше работаем с валидными данными\n",[143,1257,1258,1261,1263,1266,1269,1272,1274,1277,1279],{"class":145,"line":707},[143,1259,1260],{"class":192},"    todo, err ",[143,1262,246],{"class":172},[143,1264,1265],{"class":192}," todoService.",[143,1267,1268],{"class":149},"Create",[143,1270,1271],{"class":192},"(c.",[143,1273,424],{"class":149},[143,1275,1276],{"class":192},"().",[143,1278,294],{"class":149},[143,1280,1281],{"class":192},"(), req)\n",[143,1283,1284,1286,1288,1290,1292],{"class":145,"line":713},[143,1285,658],{"class":172},[143,1287,661],{"class":192},[143,1289,679],{"class":172},[143,1291,683],{"class":682},[143,1293,303],{"class":192},[143,1295,1296,1298,1300,1302,1305,1308],{"class":145,"line":718},[143,1297,309],{"class":172},[143,1299,249],{"class":192},[143,1301,1187],{"class":149},[143,1303,1304],{"class":192},"(http.StatusInternalServerError, ",[143,1306,1307],{"class":153},"\"failed to create todo\"",[143,1309,220],{"class":192},[143,1311,1312],{"class":145,"line":724},[143,1313,699],{"class":192},[143,1315,1316],{"class":145,"line":759},[143,1317,183],{"emptyLinePlaceholder":182},[143,1319,1320,1322,1324,1326],{"class":145,"line":764},[143,1321,727],{"class":172},[143,1323,312],{"class":192},[143,1325,732],{"class":149},[143,1327,1328],{"class":192},"(http.StatusCreated, todo)\n",[143,1330,1331],{"class":145,"line":770},[143,1332,363],{"class":192},[15,1334,1335],{},"Валидатор нужно зарегистрировать при инициализации — Echo не навязывает конкретную библиотеку:",[30,1337,1339],{"className":164,"code":1338,"language":150,"meta":39,"style":39},"import \"github.com\u002Fgo-playground\u002Fvalidator\u002Fv10\"\n\ntype CustomValidator struct {\n    validator *validator.Validate\n}\n\nfunc (cv *CustomValidator) Validate(i interface{}) error {\n    if err := cv.validator.Struct(i); err != nil {\n        return echo.NewHTTPError(http.StatusBadRequest, err.Error())\n    }\n    return nil\n}\n\nfunc main() {\n    e := echo.New()\n    e.Validator = &CustomValidator{validator: validator.New()}\n    \u002F\u002F ...\n}\n",[37,1340,1341,1353,1357,1368,1384,1388,1392,1426,1449,1466,1470,1477,1481,1485,1493,1505,1526,1531],{"__ignoreMap":39},[143,1342,1343,1345,1348,1351],{"class":145,"line":146},[143,1344,189],{"class":172},[143,1346,1347],{"class":153}," \"",[143,1349,1350],{"class":149},"github.com\u002Fgo-playground\u002Fvalidator\u002Fv10",[143,1352,204],{"class":153},[143,1354,1355],{"class":145,"line":179},[143,1356,183],{"emptyLinePlaceholder":182},[143,1358,1359,1361,1364,1366],{"class":145,"line":186},[143,1360,1077],{"class":172},[143,1362,1363],{"class":149}," CustomValidator",[143,1365,1083],{"class":172},[143,1367,303],{"class":192},[143,1369,1370,1373,1376,1379,1381],{"class":145,"line":196},[143,1371,1372],{"class":192},"    validator ",[143,1374,1375],{"class":172},"*",[143,1377,1378],{"class":149},"validator",[143,1380,125],{"class":192},[143,1382,1383],{"class":149},"Validate\n",[143,1385,1386],{"class":145,"line":207},[143,1387,363],{"class":192},[143,1389,1390],{"class":145,"line":217},[143,1391,183],{"emptyLinePlaceholder":182},[143,1393,1394,1396,1399,1402,1404,1407,1409,1411,1413,1416,1419,1422,1424],{"class":145,"line":223},[143,1395,231],{"class":172},[143,1397,1398],{"class":192}," (",[143,1400,1401],{"class":285},"cv ",[143,1403,1375],{"class":172},[143,1405,1406],{"class":149},"CustomValidator",[143,1408,297],{"class":192},[143,1410,1221],{"class":149},[143,1412,272],{"class":192},[143,1414,1415],{"class":285},"i",[143,1417,1418],{"class":172}," interface",[143,1420,1421],{"class":192},"{}) ",[143,1423,300],{"class":172},[143,1425,303],{"class":192},[143,1427,1428,1430,1432,1434,1437,1440,1443,1445,1447],{"class":145,"line":228},[143,1429,658],{"class":172},[143,1431,661],{"class":192},[143,1433,246],{"class":172},[143,1435,1436],{"class":192}," cv.validator.",[143,1438,1439],{"class":149},"Struct",[143,1441,1442],{"class":192},"(i); err ",[143,1444,679],{"class":172},[143,1446,683],{"class":682},[143,1448,303],{"class":192},[143,1450,1451,1453,1455,1457,1460,1463],{"class":145,"line":240},[143,1452,309],{"class":172},[143,1454,249],{"class":192},[143,1456,1187],{"class":149},[143,1458,1459],{"class":192},"(http.StatusBadRequest, err.",[143,1461,1462],{"class":149},"Error",[143,1464,1465],{"class":192},"())\n",[143,1467,1468],{"class":145,"line":258},[143,1469,699],{"class":192},[143,1471,1472,1474],{"class":145,"line":263},[143,1473,727],{"class":172},[143,1475,1476],{"class":682}," nil\n",[143,1478,1479],{"class":145,"line":306},[143,1480,363],{"class":192},[143,1482,1483],{"class":145,"line":326},[143,1484,183],{"emptyLinePlaceholder":182},[143,1486,1487,1489,1491],{"class":145,"line":332},[143,1488,231],{"class":172},[143,1490,234],{"class":149},[143,1492,237],{"class":192},[143,1494,1495,1497,1499,1501,1503],{"class":145,"line":337},[143,1496,243],{"class":192},[143,1498,246],{"class":172},[143,1500,249],{"class":192},[143,1502,252],{"class":149},[143,1504,255],{"class":192},[143,1506,1507,1510,1513,1516,1518,1521,1523],{"class":145,"line":360},[143,1508,1509],{"class":192},"    e.Validator ",[143,1511,1512],{"class":172},"=",[143,1514,1515],{"class":172}," &",[143,1517,1406],{"class":149},[143,1519,1520],{"class":192},"{validator: validator.",[143,1522,252],{"class":149},[143,1524,1525],{"class":192},"()}\n",[143,1527,1528],{"class":145,"line":688},[143,1529,1530],{"class":387},"    \u002F\u002F ...\n",[143,1532,1533],{"class":145,"line":696},[143,1534,363],{"class":192},[19,1536],{},[22,1538,1540],{"id":1539},"middleware-в-echo","Middleware в Echo",[15,1542,1543],{},"Echo поставляется с набором готовых middleware:",[30,1545,1547],{"className":164,"code":1546,"language":150,"meta":39,"style":39},"import \"github.com\u002Flabstack\u002Fecho\u002Fv4\u002Fmiddleware\"\n\ne := echo.New()\n\n\u002F\u002F Логирование всех запросов\ne.Use(middleware.Logger())\n\n\u002F\u002F Восстановление после паники\ne.Use(middleware.Recover())\n\n\u002F\u002F CORS\ne.Use(middleware.CORSWithConfig(middleware.CORSConfig{\n    AllowOrigins: []string{\"https:\u002F\u002Fexample.com\"},\n    AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},\n}))\n\n\u002F\u002F Rate limiting\ne.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))\n\n\u002F\u002F JWT аутентификация\ne.Use(middleware.JWTWithConfig(middleware.JWTConfig{\n    SigningKey: []byte(\"secret\"),\n}))\n",[37,1548,1549,1560,1564,1576,1580,1585,1599,1603,1608,1621,1625,1630,1654,1669,1679,1684,1688,1693,1717,1721,1726,1748,1764],{"__ignoreMap":39},[143,1550,1551,1553,1555,1558],{"class":145,"line":146},[143,1552,189],{"class":172},[143,1554,1347],{"class":153},[143,1556,1557],{"class":149},"github.com\u002Flabstack\u002Fecho\u002Fv4\u002Fmiddleware",[143,1559,204],{"class":153},[143,1561,1562],{"class":145,"line":179},[143,1563,183],{"emptyLinePlaceholder":182},[143,1565,1566,1568,1570,1572,1574],{"class":145,"line":186},[143,1567,857],{"class":192},[143,1569,246],{"class":172},[143,1571,249],{"class":192},[143,1573,252],{"class":149},[143,1575,255],{"class":192},[143,1577,1578],{"class":145,"line":196},[143,1579,183],{"emptyLinePlaceholder":182},[143,1581,1582],{"class":145,"line":207},[143,1583,1584],{"class":387},"\u002F\u002F Логирование всех запросов\n",[143,1586,1587,1589,1591,1594,1597],{"class":145,"line":217},[143,1588,879],{"class":192},[143,1590,1035],{"class":149},[143,1592,1593],{"class":192},"(middleware.",[143,1595,1596],{"class":149},"Logger",[143,1598,1465],{"class":192},[143,1600,1601],{"class":145,"line":223},[143,1602,183],{"emptyLinePlaceholder":182},[143,1604,1605],{"class":145,"line":228},[143,1606,1607],{"class":387},"\u002F\u002F Восстановление после паники\n",[143,1609,1610,1612,1614,1616,1619],{"class":145,"line":240},[143,1611,879],{"class":192},[143,1613,1035],{"class":149},[143,1615,1593],{"class":192},[143,1617,1618],{"class":149},"Recover",[143,1620,1465],{"class":192},[143,1622,1623],{"class":145,"line":258},[143,1624,183],{"emptyLinePlaceholder":182},[143,1626,1627],{"class":145,"line":263},[143,1628,1629],{"class":387},"\u002F\u002F CORS\n",[143,1631,1632,1634,1636,1638,1641,1643,1646,1648,1651],{"class":145,"line":306},[143,1633,879],{"class":192},[143,1635,1035],{"class":149},[143,1637,1593],{"class":192},[143,1639,1640],{"class":149},"CORSWithConfig",[143,1642,272],{"class":192},[143,1644,1645],{"class":149},"middleware",[143,1647,125],{"class":192},[143,1649,1650],{"class":149},"CORSConfig",[143,1652,1653],{"class":192},"{\n",[143,1655,1656,1659,1661,1663,1666],{"class":145,"line":326},[143,1657,1658],{"class":192},"    AllowOrigins: []",[143,1660,743],{"class":172},[143,1662,751],{"class":192},[143,1664,1665],{"class":153},"\"https:\u002F\u002Fexample.com\"",[143,1667,1668],{"class":192},"},\n",[143,1670,1671,1674,1676],{"class":145,"line":332},[143,1672,1673],{"class":192},"    AllowMethods: []",[143,1675,743],{"class":172},[143,1677,1678],{"class":192},"{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},\n",[143,1680,1681],{"class":145,"line":337},[143,1682,1683],{"class":192},"}))\n",[143,1685,1686],{"class":145,"line":360},[143,1687,183],{"emptyLinePlaceholder":182},[143,1689,1690],{"class":145,"line":688},[143,1691,1692],{"class":387},"\u002F\u002F Rate limiting\n",[143,1694,1695,1697,1699,1701,1704,1706,1709,1711,1714],{"class":145,"line":696},[143,1696,879],{"class":192},[143,1698,1035],{"class":149},[143,1700,1593],{"class":192},[143,1702,1703],{"class":149},"RateLimiter",[143,1705,1593],{"class":192},[143,1707,1708],{"class":149},"NewRateLimiterMemoryStore",[143,1710,272],{"class":192},[143,1712,1713],{"class":682},"20",[143,1715,1716],{"class":192},")))\n",[143,1718,1719],{"class":145,"line":702},[143,1720,183],{"emptyLinePlaceholder":182},[143,1722,1723],{"class":145,"line":707},[143,1724,1725],{"class":387},"\u002F\u002F JWT аутентификация\n",[143,1727,1728,1730,1732,1734,1737,1739,1741,1743,1746],{"class":145,"line":713},[143,1729,879],{"class":192},[143,1731,1035],{"class":149},[143,1733,1593],{"class":192},[143,1735,1736],{"class":149},"JWTWithConfig",[143,1738,272],{"class":192},[143,1740,1645],{"class":149},[143,1742,125],{"class":192},[143,1744,1745],{"class":149},"JWTConfig",[143,1747,1653],{"class":192},[143,1749,1750,1753,1756,1758,1761],{"class":145,"line":718},[143,1751,1752],{"class":192},"    SigningKey: []",[143,1754,1755],{"class":172},"byte",[143,1757,272],{"class":192},[143,1759,1760],{"class":153},"\"secret\"",[143,1762,1763],{"class":192},"),\n",[143,1765,1766],{"class":145,"line":724},[143,1767,1683],{"class":192},[50,1769,1771],{"id":1770},"кастомный-middleware","Кастомный middleware",[30,1773,1775],{"className":164,"code":1774,"language":150,"meta":39,"style":39},"func requestIDMiddleware(next echo.HandlerFunc) echo.HandlerFunc {\n    return func(c echo.Context) error {\n        \u002F\u002F До обработчика\n        requestID := uuid.New().String()\n        c.Set(\"requestID\", requestID)\n        c.Response().Header().Set(\"X-Request-ID\", requestID)\n\n        \u002F\u002F Вызываем следующий обработчик\n        err := next(c)\n\n        \u002F\u002F После обработчика\n        log.Printf(\"request %s completed\", requestID)\n\n        return err\n    }\n}\n\ne.Use(requestIDMiddleware)\n",[37,1776,1777,1807,1830,1835,1853,1869,1892,1896,1901,1914,1918,1923,1944,1948,1954,1958,1962,1966],{"__ignoreMap":39},[143,1778,1779,1781,1784,1786,1789,1791,1793,1796,1798,1801,1803,1805],{"class":145,"line":146},[143,1780,231],{"class":172},[143,1782,1783],{"class":149}," requestIDMiddleware",[143,1785,272],{"class":192},[143,1787,1788],{"class":285},"next",[143,1790,289],{"class":149},[143,1792,125],{"class":192},[143,1794,1795],{"class":149},"HandlerFunc",[143,1797,297],{"class":192},[143,1799,1800],{"class":149},"echo",[143,1802,125],{"class":192},[143,1804,1795],{"class":149},[143,1806,303],{"class":192},[143,1808,1809,1811,1814,1816,1818,1820,1822,1824,1826,1828],{"class":145,"line":179},[143,1810,727],{"class":172},[143,1812,1813],{"class":172}," func",[143,1815,272],{"class":192},[143,1817,286],{"class":285},[143,1819,289],{"class":149},[143,1821,125],{"class":192},[143,1823,294],{"class":149},[143,1825,297],{"class":192},[143,1827,300],{"class":172},[143,1829,303],{"class":192},[143,1831,1832],{"class":145,"line":186},[143,1833,1834],{"class":387},"        \u002F\u002F До обработчика\n",[143,1836,1837,1840,1842,1845,1847,1849,1851],{"class":145,"line":196},[143,1838,1839],{"class":192},"        requestID ",[143,1841,246],{"class":172},[143,1843,1844],{"class":192}," uuid.",[143,1846,252],{"class":149},[143,1848,1276],{"class":192},[143,1850,315],{"class":149},[143,1852,255],{"class":192},[143,1854,1855,1858,1861,1863,1866],{"class":145,"line":207},[143,1856,1857],{"class":192},"        c.",[143,1859,1860],{"class":149},"Set",[143,1862,272],{"class":192},[143,1864,1865],{"class":153},"\"requestID\"",[143,1867,1868],{"class":192},", requestID)\n",[143,1870,1871,1873,1876,1878,1881,1883,1885,1887,1890],{"class":145,"line":217},[143,1872,1857],{"class":192},[143,1874,1875],{"class":149},"Response",[143,1877,1276],{"class":192},[143,1879,1880],{"class":149},"Header",[143,1882,1276],{"class":192},[143,1884,1860],{"class":149},[143,1886,272],{"class":192},[143,1888,1889],{"class":153},"\"X-Request-ID\"",[143,1891,1868],{"class":192},[143,1893,1894],{"class":145,"line":223},[143,1895,183],{"emptyLinePlaceholder":182},[143,1897,1898],{"class":145,"line":228},[143,1899,1900],{"class":387},"        \u002F\u002F Вызываем следующий обработчик\n",[143,1902,1903,1906,1908,1911],{"class":145,"line":240},[143,1904,1905],{"class":192},"        err ",[143,1907,246],{"class":172},[143,1909,1910],{"class":149}," next",[143,1912,1913],{"class":192},"(c)\n",[143,1915,1916],{"class":145,"line":258},[143,1917,183],{"emptyLinePlaceholder":182},[143,1919,1920],{"class":145,"line":263},[143,1921,1922],{"class":387},"        \u002F\u002F После обработчика\n",[143,1924,1925,1928,1931,1933,1936,1939,1942],{"class":145,"line":306},[143,1926,1927],{"class":192},"        log.",[143,1929,1930],{"class":149},"Printf",[143,1932,272],{"class":192},[143,1934,1935],{"class":153},"\"request ",[143,1937,1938],{"class":682},"%s",[143,1940,1941],{"class":153}," completed\"",[143,1943,1868],{"class":192},[143,1945,1946],{"class":145,"line":326},[143,1947,183],{"emptyLinePlaceholder":182},[143,1949,1950,1952],{"class":145,"line":332},[143,1951,309],{"class":172},[143,1953,693],{"class":192},[143,1955,1956],{"class":145,"line":337},[143,1957,699],{"class":192},[143,1959,1960],{"class":145,"line":360},[143,1961,363],{"class":192},[143,1963,1964],{"class":145,"line":688},[143,1965,183],{"emptyLinePlaceholder":182},[143,1967,1968,1970,1972],{"class":145,"line":696},[143,1969,879],{"class":192},[143,1971,1035],{"class":149},[143,1973,1974],{"class":192},"(requestIDMiddleware)\n",[15,1976,1977,484,1980,1983,1984,1987],{},[37,1978,1979],{},"c.Set",[37,1981,1982],{},"c.Get"," — способ передавать данные между middleware и обработчиками внутри одного запроса. Аналог ",[37,1985,1986],{},"context.WithValue",", но специфичный для Echo:",[30,1989,1991],{"className":164,"code":1990,"language":150,"meta":39,"style":39},"\u002F\u002F В middleware\nc.Set(\"userID\", 42)\n\n\u002F\u002F В обработчике\nuserID := c.Get(\"userID\").(int)\n",[37,1992,1993,1998,2017,2021,2026],{"__ignoreMap":39},[143,1994,1995],{"class":145,"line":146},[143,1996,1997],{"class":387},"\u002F\u002F В middleware\n",[143,1999,2000,2003,2005,2007,2010,2012,2015],{"class":145,"line":179},[143,2001,2002],{"class":192},"c.",[143,2004,1860],{"class":149},[143,2006,272],{"class":192},[143,2008,2009],{"class":153},"\"userID\"",[143,2011,278],{"class":192},[143,2013,2014],{"class":682},"42",[143,2016,220],{"class":192},[143,2018,2019],{"class":145,"line":186},[143,2020,183],{"emptyLinePlaceholder":182},[143,2022,2023],{"class":145,"line":196},[143,2024,2025],{"class":387},"\u002F\u002F В обработчике\n",[143,2027,2028,2031,2033,2035,2037,2039,2041,2044,2046],{"class":145,"line":207},[143,2029,2030],{"class":192},"userID ",[143,2032,246],{"class":172},[143,2034,312],{"class":192},[143,2036,626],{"class":149},[143,2038,272],{"class":192},[143,2040,2009],{"class":153},[143,2042,2043],{"class":192},").(",[143,2045,1103],{"class":172},[143,2047,220],{"class":192},[19,2049],{},[22,2051,2053],{"id":2052},"обработка-ошибок","Обработка ошибок",[15,2055,2056,2057,2060],{},"Echo централизует обработку ошибок через ",[37,2058,2059],{},"HTTPErrorHandler",". Все ошибки возвращённые из обработчиков попадают сюда:",[30,2062,2064],{"className":164,"code":2063,"language":150,"meta":39,"style":39},"type ErrorResponse struct {\n    Code    int    `json:\"code\"`\n    Message string `json:\"message\"`\n}\n\nfunc customErrorHandler(err error, c echo.Context) {\n    var httpError *echo.HTTPError\n    if errors.As(err, &httpError) {\n        c.JSON(httpError.Code, ErrorResponse{\n            Code:    httpError.Code,\n            Message: fmt.Sprintf(\"%v\", httpError.Message),\\n        })\n        return\n    }\n\n    \u002F\u002F Неизвестная ошибка — 500\n    c.JSON(http.StatusInternalServerError, ErrorResponse{\n        Code:    http.StatusInternalServerError,\n        Message: \"internal server error\",\n    })\n}\n\nfunc main() {\n    e := echo.New()\n    e.HTTPErrorHandler = customErrorHandler\n}\n",[37,2065,2066,2077,2087,2097,2101,2105,2133,2149,2167,2181,2186,2207,2212,2216,2220,2225,2238,2243,2254,2258,2262,2266,2274,2286,2296],{"__ignoreMap":39},[143,2067,2068,2070,2073,2075],{"class":145,"line":146},[143,2069,1077],{"class":172},[143,2071,2072],{"class":149}," ErrorResponse",[143,2074,1083],{"class":172},[143,2076,303],{"class":192},[143,2078,2079,2082,2084],{"class":145,"line":179},[143,2080,2081],{"class":192},"    Code    ",[143,2083,1103],{"class":172},[143,2085,2086],{"class":153},"    `json:\"code\"`\n",[143,2088,2089,2092,2094],{"class":145,"line":186},[143,2090,2091],{"class":192},"    Message ",[143,2093,743],{"class":172},[143,2095,2096],{"class":153}," `json:\"message\"`\n",[143,2098,2099],{"class":145,"line":196},[143,2100,363],{"class":192},[143,2102,2103],{"class":145,"line":207},[143,2104,183],{"emptyLinePlaceholder":182},[143,2106,2107,2109,2112,2114,2117,2120,2122,2124,2126,2128,2130],{"class":145,"line":217},[143,2108,231],{"class":172},[143,2110,2111],{"class":149}," customErrorHandler",[143,2113,272],{"class":192},[143,2115,2116],{"class":285},"err",[143,2118,2119],{"class":172}," error",[143,2121,278],{"class":192},[143,2123,286],{"class":285},[143,2125,289],{"class":149},[143,2127,125],{"class":192},[143,2129,294],{"class":149},[143,2131,2132],{"class":192},") {\n",[143,2134,2135,2137,2140,2142,2144,2146],{"class":145,"line":223},[143,2136,647],{"class":172},[143,2138,2139],{"class":192}," httpError ",[143,2141,1375],{"class":172},[143,2143,1800],{"class":149},[143,2145,125],{"class":192},[143,2147,2148],{"class":149},"HTTPError\n",[143,2150,2151,2153,2156,2159,2162,2164],{"class":145,"line":228},[143,2152,658],{"class":172},[143,2154,2155],{"class":192}," errors.",[143,2157,2158],{"class":149},"As",[143,2160,2161],{"class":192},"(err, ",[143,2163,673],{"class":172},[143,2165,2166],{"class":192},"httpError) {\n",[143,2168,2169,2171,2173,2176,2179],{"class":145,"line":240},[143,2170,1857],{"class":192},[143,2172,732],{"class":149},[143,2174,2175],{"class":192},"(httpError.Code, ",[143,2177,2178],{"class":149},"ErrorResponse",[143,2180,1653],{"class":192},[143,2182,2183],{"class":145,"line":258},[143,2184,2185],{"class":192},"            Code:    httpError.Code,\n",[143,2187,2188,2191,2194,2196,2199,2202,2204],{"class":145,"line":263},[143,2189,2190],{"class":192},"            Message: fmt.",[143,2192,2193],{"class":149},"Sprintf",[143,2195,272],{"class":192},[143,2197,2198],{"class":153},"\"",[143,2200,2201],{"class":682},"%v",[143,2203,2198],{"class":153},[143,2205,2206],{"class":192},", httpError.Message),\\n        })\n",[143,2208,2209],{"class":145,"line":306},[143,2210,2211],{"class":172},"        return\n",[143,2213,2214],{"class":145,"line":326},[143,2215,699],{"class":192},[143,2217,2218],{"class":145,"line":332},[143,2219,183],{"emptyLinePlaceholder":182},[143,2221,2222],{"class":145,"line":337},[143,2223,2224],{"class":387},"    \u002F\u002F Неизвестная ошибка — 500\n",[143,2226,2227,2230,2232,2234,2236],{"class":145,"line":360},[143,2228,2229],{"class":192},"    c.",[143,2231,732],{"class":149},[143,2233,1304],{"class":192},[143,2235,2178],{"class":149},[143,2237,1653],{"class":192},[143,2239,2240],{"class":145,"line":688},[143,2241,2242],{"class":192},"        Code:    http.StatusInternalServerError,\n",[143,2244,2245,2248,2251],{"class":145,"line":696},[143,2246,2247],{"class":192},"        Message: ",[143,2249,2250],{"class":153},"\"internal server error\"",[143,2252,2253],{"class":192},",\n",[143,2255,2256],{"class":145,"line":702},[143,2257,329],{"class":192},[143,2259,2260],{"class":145,"line":707},[143,2261,363],{"class":192},[143,2263,2264],{"class":145,"line":713},[143,2265,183],{"emptyLinePlaceholder":182},[143,2267,2268,2270,2272],{"class":145,"line":718},[143,2269,231],{"class":172},[143,2271,234],{"class":149},[143,2273,237],{"class":192},[143,2275,2276,2278,2280,2282,2284],{"class":145,"line":724},[143,2277,243],{"class":192},[143,2279,246],{"class":172},[143,2281,249],{"class":192},[143,2283,252],{"class":149},[143,2285,255],{"class":192},[143,2287,2288,2291,2293],{"class":145,"line":759},[143,2289,2290],{"class":192},"    e.HTTPErrorHandler ",[143,2292,1512],{"class":172},[143,2294,2295],{"class":192}," customErrorHandler\n",[143,2297,2298],{"class":145,"line":764},[143,2299,363],{"class":192},[15,2301,2302],{},"Из обработчика можно возвращать:",[30,2304,2306],{"className":164,"code":2305,"language":150,"meta":39,"style":39},"\u002F\u002F Стандартная HTTP ошибка\nreturn echo.NewHTTPError(http.StatusNotFound, \"todo not found\")\n\n\u002F\u002F Любая ошибка Go — попадёт в HTTPErrorHandler как 500\nreturn fmt.Errorf(\"database error: %w\", err)\n\n\u002F\u002F nil — успешный ответ\nreturn c.JSON(http.StatusOK, result)\n",[37,2307,2308,2313,2330,2334,2339,2362,2366,2371],{"__ignoreMap":39},[143,2309,2310],{"class":145,"line":146},[143,2311,2312],{"class":387},"\u002F\u002F Стандартная HTTP ошибка\n",[143,2314,2315,2318,2320,2322,2325,2328],{"class":145,"line":179},[143,2316,2317],{"class":172},"return",[143,2319,249],{"class":192},[143,2321,1187],{"class":149},[143,2323,2324],{"class":192},"(http.StatusNotFound, ",[143,2326,2327],{"class":153},"\"todo not found\"",[143,2329,220],{"class":192},[143,2331,2332],{"class":145,"line":186},[143,2333,183],{"emptyLinePlaceholder":182},[143,2335,2336],{"class":145,"line":196},[143,2337,2338],{"class":387},"\u002F\u002F Любая ошибка Go — попадёт в HTTPErrorHandler как 500\n",[143,2340,2341,2343,2346,2349,2351,2354,2357,2359],{"class":145,"line":207},[143,2342,2317],{"class":172},[143,2344,2345],{"class":192}," fmt.",[143,2347,2348],{"class":149},"Errorf",[143,2350,272],{"class":192},[143,2352,2353],{"class":153},"\"database error: ",[143,2355,2356],{"class":682},"%w",[143,2358,2198],{"class":153},[143,2360,2361],{"class":192},", err)\n",[143,2363,2364],{"class":145,"line":217},[143,2365,183],{"emptyLinePlaceholder":182},[143,2367,2368],{"class":145,"line":223},[143,2369,2370],{"class":387},"\u002F\u002F nil — успешный ответ\n",[143,2372,2373,2375,2377,2379],{"class":145,"line":228},[143,2374,2317],{"class":172},[143,2376,312],{"class":192},[143,2378,732],{"class":149},[143,2380,2381],{"class":192},"(http.StatusOK, result)\n",[19,2383],{},[22,2385,2387],{"id":2386},"graceful-shutdown-в-echo","Graceful Shutdown в Echo",[30,2389,2391],{"className":164,"code":2390,"language":150,"meta":39,"style":39},"func main() {\n    e := echo.New()\n    e.HideBanner = true\n\n    \u002F\u002F Роуты и middleware...\n\n    \u002F\u002F Запуск сервера в горутине\n    go func() {\n        if err := e.Start(\":8080\"); err != http.ErrServerClosed {\n            e.Logger.Fatal(err)\n        }\n    }()\n\n    \u002F\u002F Ожидание сигнала завершения\n    quit := make(chan os.Signal, 1)\n    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)\n    \u003C-quit\n\n    \u002F\u002F Graceful shutdown с таймаутом 10 секунд\n    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\n    defer cancel()\n\n    if err := e.Shutdown(ctx); err != nil {\n        e.Logger.Fatal(err)\n    }\n}\n",[37,2392,2393,2401,2413,2423,2427,2432,2436,2441,2450,2475,2485,2490,2495,2499,2504,2534,2545,2553,2557,2562,2592,2602,2606,2628,2637,2641],{"__ignoreMap":39},[143,2394,2395,2397,2399],{"class":145,"line":146},[143,2396,231],{"class":172},[143,2398,234],{"class":149},[143,2400,237],{"class":192},[143,2402,2403,2405,2407,2409,2411],{"class":145,"line":179},[143,2404,243],{"class":192},[143,2406,246],{"class":172},[143,2408,249],{"class":192},[143,2410,252],{"class":149},[143,2412,255],{"class":192},[143,2414,2415,2418,2420],{"class":145,"line":186},[143,2416,2417],{"class":192},"    e.HideBanner ",[143,2419,1512],{"class":172},[143,2421,2422],{"class":682}," true\n",[143,2424,2425],{"class":145,"line":196},[143,2426,183],{"emptyLinePlaceholder":182},[143,2428,2429],{"class":145,"line":207},[143,2430,2431],{"class":387},"    \u002F\u002F Роуты и middleware...\n",[143,2433,2434],{"class":145,"line":217},[143,2435,183],{"emptyLinePlaceholder":182},[143,2437,2438],{"class":145,"line":223},[143,2439,2440],{"class":387},"    \u002F\u002F Запуск сервера в горутине\n",[143,2442,2443,2446,2448],{"class":145,"line":228},[143,2444,2445],{"class":172},"    go",[143,2447,1813],{"class":172},[143,2449,237],{"class":192},[143,2451,2452,2455,2457,2459,2461,2463,2465,2467,2470,2472],{"class":145,"line":240},[143,2453,2454],{"class":172},"        if",[143,2456,661],{"class":192},[143,2458,246],{"class":172},[143,2460,964],{"class":192},[143,2462,349],{"class":149},[143,2464,272],{"class":192},[143,2466,354],{"class":153},[143,2468,2469],{"class":192},"); err ",[143,2471,679],{"class":172},[143,2473,2474],{"class":192}," http.ErrServerClosed {\n",[143,2476,2477,2480,2482],{"class":145,"line":258},[143,2478,2479],{"class":192},"            e.Logger.",[143,2481,343],{"class":149},[143,2483,2484],{"class":192},"(err)\n",[143,2486,2487],{"class":145,"line":263},[143,2488,2489],{"class":192},"        }\n",[143,2491,2492],{"class":145,"line":306},[143,2493,2494],{"class":192},"    }()\n",[143,2496,2497],{"class":145,"line":326},[143,2498,183],{"emptyLinePlaceholder":182},[143,2500,2501],{"class":145,"line":332},[143,2502,2503],{"class":387},"    \u002F\u002F Ожидание сигнала завершения\n",[143,2505,2506,2509,2511,2514,2516,2519,2522,2524,2527,2529,2532],{"class":145,"line":337},[143,2507,2508],{"class":192},"    quit ",[143,2510,246],{"class":172},[143,2512,2513],{"class":149}," make",[143,2515,272],{"class":192},[143,2517,2518],{"class":172},"chan",[143,2520,2521],{"class":149}," os",[143,2523,125],{"class":192},[143,2525,2526],{"class":149},"Signal",[143,2528,278],{"class":192},[143,2530,2531],{"class":682},"1",[143,2533,220],{"class":192},[143,2535,2536,2539,2542],{"class":145,"line":360},[143,2537,2538],{"class":192},"    signal.",[143,2540,2541],{"class":149},"Notify",[143,2543,2544],{"class":192},"(quit, syscall.SIGINT, syscall.SIGTERM)\n",[143,2546,2547,2550],{"class":145,"line":688},[143,2548,2549],{"class":172},"    \u003C-",[143,2551,2552],{"class":192},"quit\n",[143,2554,2555],{"class":145,"line":696},[143,2556,183],{"emptyLinePlaceholder":182},[143,2558,2559],{"class":145,"line":702},[143,2560,2561],{"class":387},"    \u002F\u002F Graceful shutdown с таймаутом 10 секунд\n",[143,2563,2564,2567,2569,2572,2575,2578,2581,2584,2587,2589],{"class":145,"line":707},[143,2565,2566],{"class":192},"    ctx, cancel ",[143,2568,246],{"class":172},[143,2570,2571],{"class":192}," context.",[143,2573,2574],{"class":149},"WithTimeout",[143,2576,2577],{"class":192},"(context.",[143,2579,2580],{"class":149},"Background",[143,2582,2583],{"class":192},"(), ",[143,2585,2586],{"class":682},"10",[143,2588,1375],{"class":172},[143,2590,2591],{"class":192},"time.Second)\n",[143,2593,2594,2597,2600],{"class":145,"line":713},[143,2595,2596],{"class":172},"    defer",[143,2598,2599],{"class":149}," cancel",[143,2601,255],{"class":192},[143,2603,2604],{"class":145,"line":718},[143,2605,183],{"emptyLinePlaceholder":182},[143,2607,2608,2610,2612,2614,2616,2619,2622,2624,2626],{"class":145,"line":724},[143,2609,658],{"class":172},[143,2611,661],{"class":192},[143,2613,246],{"class":172},[143,2615,964],{"class":192},[143,2617,2618],{"class":149},"Shutdown",[143,2620,2621],{"class":192},"(ctx); err ",[143,2623,679],{"class":172},[143,2625,683],{"class":682},[143,2627,303],{"class":192},[143,2629,2630,2633,2635],{"class":145,"line":759},[143,2631,2632],{"class":192},"        e.Logger.",[143,2634,343],{"class":149},[143,2636,2484],{"class":192},[143,2638,2639],{"class":145,"line":764},[143,2640,699],{"class":192},[143,2642,2643],{"class":145,"line":770},[143,2644,363],{"class":192},[19,2646],{},[22,2648,2650],{"id":2649},"echo-vs-gin-коротко","Echo vs Gin — коротко",[15,2652,2653],{},"Оба фреймворка решают одну задачу и очень похожи. Ключевые различия:",[2655,2656,2657,2672],"table",{},[2658,2659,2660],"thead",{},[2661,2662,2663,2666,2669],"tr",{},[2664,2665],"th",{},[2664,2667,2668],{},"Echo",[2664,2670,2671],{},"Gin",[2673,2674,2675,2691,2708,2719,2730,2741],"tbody",{},[2661,2676,2677,2681,2686],{},[2678,2679,2680],"td",{},"Контекст",[2678,2682,2683,2685],{},[37,2684,376],{}," — единый объект",[2678,2687,2688,2685],{},[37,2689,2690],{},"*gin.Context",[2661,2692,2693,2696,2702],{},[2678,2694,2695],{},"Возврат ошибки",[2678,2697,2698,2701],{},[37,2699,2700],{},"return error"," из обработчика",[2678,2703,2704,2705],{},"нет, всё через ",[37,2706,2707],{},"c.JSON",[2661,2709,2710,2713,2716],{},[2678,2711,2712],{},"Встроенный middleware",[2678,2714,2715],{},"богатый набор",[2678,2717,2718],{},"базовый набор",[2661,2720,2721,2724,2727],{},[2678,2722,2723],{},"Валидация",[2678,2725,2726],{},"через интерфейс, любая библиотека",[2678,2728,2729],{},"встроенная через binding",[2661,2731,2732,2735,2738],{},[2678,2733,2734],{},"Производительность",[2678,2736,2737],{},"чуть быстрее в бенчмарках",[2678,2739,2740],{},"сопоставимо",[2661,2742,2743,2746,2749],{},[2678,2744,2745],{},"Популярность",[2678,2747,2748],{},"меньше",[2678,2750,2751],{},"больше звёзд на GitHub",[15,2753,2754,2755,2757,2758,484,2760,2763],{},"Echo удобнее тем, что обработчик возвращает ",[37,2756,300],{}," — это идиоматично для Go и упрощает централизованную обработку ошибок. В Gin нужно явно вызывать ",[37,2759,2707],{},[37,2761,2762],{},"c.Abort"," в каждом обработчике, что многословнее.",[19,2765],{},[22,2767,2769],{"id":2768},"вопросы-на-собеседовании","Вопросы на собеседовании",[15,2771,2772,2775,2778],{},[66,2773,2774],{},"Q: Из каких слоёв обычно состоит backend-приложение на Go?",[2776,2777],"br",{},"\nA: Handler (transport) — знает про HTTP, парсит запросы и формирует ответы. Service — содержит бизнес-логику, не знает про HTTP и БД. Repository — работает с базой данных, только SQL и маппинг. Такое разделение делает каждый слой тестируемым независимо.",[15,2780,2781,2784,2786,2787,2789,2790,2792,2793,2795,2796,2792,2798,2800,2801,2803],{},[66,2782,2783],{},"Q: Что такое echo.Context и чем он удобнее пары (w, r)?",[2776,2785],{},"\nA: Единый объект объединяющий запрос и ответ с удобными методами: ",[37,2788,668],{}," для декодирования, ",[37,2791,541],{},"\u002F",[37,2794,569],{}," для параметров, ",[37,2797,732],{},[37,2799,315],{}," для ответа. Вместо двух аргументов — один, и возврат ",[37,2802,300],{}," вместо явной записи в ResponseWriter.",[15,2805,2806,2809,2811,2812,2814],{},[66,2807,2808],{},"Q: Как работает Bind в Echo?",[2776,2810],{},"\nA: Автоматически определяет формат данных по заголовку ",[37,2813,1066],{}," и декодирует тело запроса в переданную структуру. Поддерживает JSON, form-data, query параметры и path параметры через struct теги.",[15,2816,2817,2820,2822,2823,2826],{},[66,2818,2819],{},"Q: Как централизовать обработку ошибок в Echo?",[2776,2821],{},"\nA: Через ",[37,2824,2825],{},"e.HTTPErrorHandler"," — функция, которая принимает ошибку и контекст. Все ошибки, возвращённые из обработчиков, попадают сюда. Позволяет единообразно форматировать ошибки, логировать их и возвращать клиенту.",[15,2828,2829,2832,2834,2835,2838,2839,2841,2842,125],{},[66,2830,2831],{},"Q: Чем отличается c.Set\u002Fc.Get от context.WithValue?",[2776,2833],{},"\nA: ",[37,2836,2837],{},"c.Set\u002Fc.Get"," — хранилище пар ключ-значение внутри Echo-контекста, специфичное для фреймворка. ",[37,2840,1986],{}," — стандартный механизм Go, работает везде включая сторонние библиотеки. Для передачи данных в библиотеки (например, в database\u002Fsql через ctx) нужен стандартный context, доступный через ",[37,2843,2844],{},"c.Request().Context()",[15,2846,2847,2850,2852],{},[66,2848,2849],{},"Q: Зачем нужен Graceful Shutdown? Что без него произойдёт?",[2776,2851],{},"\nA: Без него при остановке процесса активные запросы получат обрыв соединения — клиент увидит ошибку сети вместо нормального ответа. Graceful Shutdown перестаёт принимать новые соединения и ждёт завершения текущих запросов перед остановкой.",[19,2854],{},[15,2856,2857,2858,2861],{},"Следующий шаг — ",[66,2859,2860],{},"итоговый проект: Todo API",". Соберём всё вместе: Echo, слоистая архитектура, работа с in-memory хранилищем (без реальной БД чтобы не усложнять), контекст, graceful shutdown и базовая валидация. Переходим?",[2863,2864,2865],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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 .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}",{"title":39,"searchDepth":179,"depth":179,"links":2867},[2868,2871,2872,2875,2876,2877,2878,2881,2882,2883,2884],{"id":24,"depth":179,"text":25,"children":2869},[2870],{"id":52,"depth":186,"text":53},{"id":89,"depth":179,"text":90},{"id":130,"depth":179,"text":131,"children":2873},[2874],{"id":160,"depth":186,"text":161},{"id":474,"depth":179,"text":475},{"id":846,"depth":179,"text":847},{"id":1057,"depth":179,"text":1058},{"id":1539,"depth":179,"text":1540,"children":2879},[2880],{"id":1770,"depth":186,"text":1771},{"id":2052,"depth":179,"text":2053},{"id":2386,"depth":179,"text":2387},{"id":2649,"depth":179,"text":2650},{"id":2768,"depth":179,"text":2769},"intermediate","md",{},"web","rest-api","\u002F05-web\u002F02-echo",{"title":5,"description":17},"05-web\u002F02-echo\u002Findex",[1800,2894,2895,1645,2896,2897],"фреймворк","маршрутизация","валидация","собеседование","2fy0VYKwk3LbVToaegx-NH02N9tCpfvHP78gQUYmP-Y",1776280769713]