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