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