[{"data":1,"prerenderedAt":6786},["ShallowReactive",2],{"content:\u002F06-architecture\u002F09-patterns":3},{"title":4,"description":5,"path":6,"body":7},"Паттерны в Go — шпаргалка","","\u002F06-architecture\u002F09-patterns",{"type":8,"value":9,"toc":6740},"minimark",[10,15,19,27,30,34,43,46,255,258,265,269,276,753,756,761,765,772,1210,1214,1220,1349,1352,1362,1367,1372,1379,1381,1385,1389,1392,1719,1722,1726,1998,2001,2138,2141,2143,2147,2151,2154,2456,2459,2463,2723,2726,2730,2733,2984,2991,2993,2997,3001,3004,3147,3150,3159,3163,3166,3514,3517,3615,3626,3628,3632,3636,3806,3809,3813,4139,4145,4148,4163,4166,4170,4173,4440,4443,4447,4452,4555,4558,4591,4595,4598,4606,4785,4788,4790,4794,4797,4895,4898,5250,5253,5256,5452,5455,5457,5461,5465,5708,5718,5720,5724,5731,5827,5835,5839,5909,5916,5920,5960,5967,5973,5977,5980,5984,6014,6021,6108,6110,6114,6223,6226,6228,6232,6323,6327,6369,6371,6375,6404,6586,6736],[11,12,14],"h2",{"id":13},"подход","Подход",[16,17,18],"p",{},"Классическая Банда Четырёх писалась под Java\u002FC++ с наследованием и приватными конструкторами. В Go многое упрощается: интерфейсы неявные, функции — первоклассные, есть каналы. Поэтому часть паттернов превращается в идиомы (Factory, Strategy), часть — в каноничные примеры использования каналов (Pipeline, Worker Pool), а часть — становится не нужна (Singleton почти всегда, классический Visitor).",[16,20,21,22,26],{},"Главное: ",[23,24,25],"strong",{},"паттерн — не цель."," Цель — простой, понятный код. Если без паттерна понятнее — обходитесь без него.",[28,29],"hr",{},[11,31,33],{"id":32},"creational","Creational",[35,36,38,39],"h3",{"id":37},"factory-newxxx","Factory — ",[40,41,42],"code",{},"NewXxx()",[16,44,45],{},"Проблема: конструирование требует валидации, генерации ID, инициализации связей. Голая структура нарушает инварианты.",[47,48,52],"pre",{"className":49,"code":50,"language":51,"meta":5,"style":5},"language-go shiki shiki-themes github-dark","func NewOrder(customerID CustomerID, items []Item) (*Order, error) {\n    if customerID == \"\" {\n        return nil, ErrEmptyCustomer\n    }\n    if len(items) == 0 {\n        return nil, ErrEmptyOrder\n    }\n    return &Order{\n        id:        OrderID(uuid.NewString()),\n        customer:  customerID,\n        items:     items,\n        status:    Draft,\n        createdAt: time.Now(),\n    }, nil\n}\n","go",[40,53,54,107,126,139,145,163,173,178,192,210,216,222,228,240,249],{"__ignoreMap":5},[55,56,59,63,67,71,75,78,81,84,87,90,93,96,99,101,104],"span",{"class":57,"line":58},"line",1,[55,60,62],{"class":61},"snl16","func",[55,64,66],{"class":65},"svObZ"," NewOrder",[55,68,70],{"class":69},"s95oV","(",[55,72,74],{"class":73},"s9osk","customerID",[55,76,77],{"class":65}," CustomerID",[55,79,80],{"class":69},", ",[55,82,83],{"class":73},"items",[55,85,86],{"class":69}," []",[55,88,89],{"class":65},"Item",[55,91,92],{"class":69},") (",[55,94,95],{"class":61},"*",[55,97,98],{"class":65},"Order",[55,100,80],{"class":69},[55,102,103],{"class":61},"error",[55,105,106],{"class":69},") {\n",[55,108,110,113,116,119,123],{"class":57,"line":109},2,[55,111,112],{"class":61},"    if",[55,114,115],{"class":69}," customerID ",[55,117,118],{"class":61},"==",[55,120,122],{"class":121},"sU2Wk"," \"\"",[55,124,125],{"class":69}," {\n",[55,127,129,132,136],{"class":57,"line":128},3,[55,130,131],{"class":61},"        return",[55,133,135],{"class":134},"sDLfK"," nil",[55,137,138],{"class":69},", ErrEmptyCustomer\n",[55,140,142],{"class":57,"line":141},4,[55,143,144],{"class":69},"    }\n",[55,146,148,150,153,156,158,161],{"class":57,"line":147},5,[55,149,112],{"class":61},[55,151,152],{"class":65}," len",[55,154,155],{"class":69},"(items) ",[55,157,118],{"class":61},[55,159,160],{"class":134}," 0",[55,162,125],{"class":69},[55,164,166,168,170],{"class":57,"line":165},6,[55,167,131],{"class":61},[55,169,135],{"class":134},[55,171,172],{"class":69},", ErrEmptyOrder\n",[55,174,176],{"class":57,"line":175},7,[55,177,144],{"class":69},[55,179,181,184,187,189],{"class":57,"line":180},8,[55,182,183],{"class":61},"    return",[55,185,186],{"class":61}," &",[55,188,98],{"class":65},[55,190,191],{"class":69},"{\n",[55,193,195,198,201,204,207],{"class":57,"line":194},9,[55,196,197],{"class":69},"        id:        ",[55,199,200],{"class":65},"OrderID",[55,202,203],{"class":69},"(uuid.",[55,205,206],{"class":65},"NewString",[55,208,209],{"class":69},"()),\n",[55,211,213],{"class":57,"line":212},10,[55,214,215],{"class":69},"        customer:  customerID,\n",[55,217,219],{"class":57,"line":218},11,[55,220,221],{"class":69},"        items:     items,\n",[55,223,225],{"class":57,"line":224},12,[55,226,227],{"class":69},"        status:    Draft,\n",[55,229,231,234,237],{"class":57,"line":230},13,[55,232,233],{"class":69},"        createdAt: time.",[55,235,236],{"class":65},"Now",[55,238,239],{"class":69},"(),\n",[55,241,243,246],{"class":57,"line":242},14,[55,244,245],{"class":69},"    }, ",[55,247,248],{"class":134},"nil\n",[55,250,252],{"class":57,"line":251},15,[55,253,254],{"class":69},"}\n",[16,256,257],{},"Когда применять: всегда, если конструирование нетривиально. В Go не паттерн, а идиома.",[16,259,260,261,264],{},"Когда ",[23,262,263],{},"не"," нужно: для DTO\u002FValue Object из 2 полей — литерал понятнее.",[35,266,268],{"id":267},"functional-options-конфигурация-без-боли","Functional Options — конфигурация без боли",[16,270,271,272,275],{},"Проблема: у объекта 10 опциональных параметров. Конструктор ",[40,273,274],{},"New(a, b, c, d, e, f, g, h, i, j)"," нечитаемый. Перегрузок в Go нет.",[47,277,279],{"className":49,"code":278,"language":51,"meta":5,"style":5},"type Server struct {\n    addr        string\n    timeout     time.Duration\n    tlsConfig   *tls.Config\n    middleware  []Middleware\n    logger      Logger\n}\n\ntype Option func(*Server)\n\nfunc WithTimeout(t time.Duration) Option {\n    return func(s *Server) { s.timeout = t }\n}\n\nfunc WithTLS(cfg *tls.Config) Option {\n    return func(s *Server) { s.tlsConfig = cfg }\n}\n\nfunc WithLogger(l Logger) Option {\n    return func(s *Server) { s.logger = l }\n}\n\nfunc NewServer(addr string, opts ...Option) *Server {\n    s := &Server{\n        addr:    addr,\n        timeout: 30 * time.Second, \u002F\u002F дефолты\n        logger:  noopLogger{},\n    }\n    for _, opt := range opts {\n        opt(s)\n    }\n    return s\n}\n\nserver := NewServer(\"0.0.0.0:8080\",\n    WithTimeout(5*time.Second),\n    WithTLS(tlsCfg),\n    WithLogger(myLogger),\n)\n",[40,280,281,294,302,316,331,339,347,351,357,377,381,409,434,438,442,469,492,497,502,524,547,552,557,591,606,612,630,642,647,664,673,678,686,691,696,714,730,739,748],{"__ignoreMap":5},[55,282,283,286,289,292],{"class":57,"line":58},[55,284,285],{"class":61},"type",[55,287,288],{"class":65}," Server",[55,290,291],{"class":61}," struct",[55,293,125],{"class":69},[55,295,296,299],{"class":57,"line":109},[55,297,298],{"class":69},"    addr        ",[55,300,301],{"class":61},"string\n",[55,303,304,307,310,313],{"class":57,"line":128},[55,305,306],{"class":69},"    timeout     ",[55,308,309],{"class":65},"time",[55,311,312],{"class":69},".",[55,314,315],{"class":65},"Duration\n",[55,317,318,321,323,326,328],{"class":57,"line":141},[55,319,320],{"class":69},"    tlsConfig   ",[55,322,95],{"class":61},[55,324,325],{"class":65},"tls",[55,327,312],{"class":69},[55,329,330],{"class":65},"Config\n",[55,332,333,336],{"class":57,"line":147},[55,334,335],{"class":69},"    middleware  []",[55,337,338],{"class":65},"Middleware\n",[55,340,341,344],{"class":57,"line":165},[55,342,343],{"class":69},"    logger      ",[55,345,346],{"class":65},"Logger\n",[55,348,349],{"class":57,"line":175},[55,350,254],{"class":69},[55,352,353],{"class":57,"line":180},[55,354,356],{"emptyLinePlaceholder":355},true,"\n",[55,358,359,361,364,367,369,371,374],{"class":57,"line":194},[55,360,285],{"class":61},[55,362,363],{"class":65}," Option",[55,365,366],{"class":61}," func",[55,368,70],{"class":69},[55,370,95],{"class":61},[55,372,373],{"class":65},"Server",[55,375,376],{"class":69},")\n",[55,378,379],{"class":57,"line":212},[55,380,356],{"emptyLinePlaceholder":355},[55,382,383,385,388,390,393,396,398,401,404,407],{"class":57,"line":218},[55,384,62],{"class":61},[55,386,387],{"class":65}," WithTimeout",[55,389,70],{"class":69},[55,391,392],{"class":73},"t",[55,394,395],{"class":65}," time",[55,397,312],{"class":69},[55,399,400],{"class":65},"Duration",[55,402,403],{"class":69},") ",[55,405,406],{"class":65},"Option",[55,408,125],{"class":69},[55,410,411,413,415,417,420,423,425,428,431],{"class":57,"line":224},[55,412,183],{"class":61},[55,414,366],{"class":61},[55,416,70],{"class":69},[55,418,419],{"class":73},"s",[55,421,422],{"class":61}," *",[55,424,373],{"class":65},[55,426,427],{"class":69},") { s.timeout ",[55,429,430],{"class":61},"=",[55,432,433],{"class":69}," t }\n",[55,435,436],{"class":57,"line":230},[55,437,254],{"class":69},[55,439,440],{"class":57,"line":242},[55,441,356],{"emptyLinePlaceholder":355},[55,443,444,446,449,451,454,456,458,460,463,465,467],{"class":57,"line":251},[55,445,62],{"class":61},[55,447,448],{"class":65}," WithTLS",[55,450,70],{"class":69},[55,452,453],{"class":73},"cfg",[55,455,422],{"class":61},[55,457,325],{"class":65},[55,459,312],{"class":69},[55,461,462],{"class":65},"Config",[55,464,403],{"class":69},[55,466,406],{"class":65},[55,468,125],{"class":69},[55,470,472,474,476,478,480,482,484,487,489],{"class":57,"line":471},16,[55,473,183],{"class":61},[55,475,366],{"class":61},[55,477,70],{"class":69},[55,479,419],{"class":73},[55,481,422],{"class":61},[55,483,373],{"class":65},[55,485,486],{"class":69},") { s.tlsConfig ",[55,488,430],{"class":61},[55,490,491],{"class":69}," cfg }\n",[55,493,495],{"class":57,"line":494},17,[55,496,254],{"class":69},[55,498,500],{"class":57,"line":499},18,[55,501,356],{"emptyLinePlaceholder":355},[55,503,505,507,510,512,515,518,520,522],{"class":57,"line":504},19,[55,506,62],{"class":61},[55,508,509],{"class":65}," WithLogger",[55,511,70],{"class":69},[55,513,514],{"class":73},"l",[55,516,517],{"class":65}," Logger",[55,519,403],{"class":69},[55,521,406],{"class":65},[55,523,125],{"class":69},[55,525,527,529,531,533,535,537,539,542,544],{"class":57,"line":526},20,[55,528,183],{"class":61},[55,530,366],{"class":61},[55,532,70],{"class":69},[55,534,419],{"class":73},[55,536,422],{"class":61},[55,538,373],{"class":65},[55,540,541],{"class":69},") { s.logger ",[55,543,430],{"class":61},[55,545,546],{"class":69}," l }\n",[55,548,550],{"class":57,"line":549},21,[55,551,254],{"class":69},[55,553,555],{"class":57,"line":554},22,[55,556,356],{"emptyLinePlaceholder":355},[55,558,560,562,565,567,570,573,575,578,581,583,585,587,589],{"class":57,"line":559},23,[55,561,62],{"class":61},[55,563,564],{"class":65}," NewServer",[55,566,70],{"class":69},[55,568,569],{"class":73},"addr",[55,571,572],{"class":61}," string",[55,574,80],{"class":69},[55,576,577],{"class":73},"opts",[55,579,580],{"class":61}," ...",[55,582,406],{"class":65},[55,584,403],{"class":69},[55,586,95],{"class":61},[55,588,373],{"class":65},[55,590,125],{"class":69},[55,592,594,597,600,602,604],{"class":57,"line":593},24,[55,595,596],{"class":69},"    s ",[55,598,599],{"class":61},":=",[55,601,186],{"class":61},[55,603,373],{"class":65},[55,605,191],{"class":69},[55,607,609],{"class":57,"line":608},25,[55,610,611],{"class":69},"        addr:    addr,\n",[55,613,615,618,621,623,626],{"class":57,"line":614},26,[55,616,617],{"class":69},"        timeout: ",[55,619,620],{"class":134},"30",[55,622,422],{"class":61},[55,624,625],{"class":69}," time.Second, ",[55,627,629],{"class":628},"sAwPA","\u002F\u002F дефолты\n",[55,631,633,636,639],{"class":57,"line":632},27,[55,634,635],{"class":69},"        logger:  ",[55,637,638],{"class":65},"noopLogger",[55,640,641],{"class":69},"{},\n",[55,643,645],{"class":57,"line":644},28,[55,646,144],{"class":69},[55,648,650,653,656,658,661],{"class":57,"line":649},29,[55,651,652],{"class":61},"    for",[55,654,655],{"class":69}," _, opt ",[55,657,599],{"class":61},[55,659,660],{"class":61}," range",[55,662,663],{"class":69}," opts {\n",[55,665,667,670],{"class":57,"line":666},30,[55,668,669],{"class":65},"        opt",[55,671,672],{"class":69},"(s)\n",[55,674,676],{"class":57,"line":675},31,[55,677,144],{"class":69},[55,679,681,683],{"class":57,"line":680},32,[55,682,183],{"class":61},[55,684,685],{"class":69}," s\n",[55,687,689],{"class":57,"line":688},33,[55,690,254],{"class":69},[55,692,694],{"class":57,"line":693},34,[55,695,356],{"emptyLinePlaceholder":355},[55,697,699,702,704,706,708,711],{"class":57,"line":698},35,[55,700,701],{"class":69},"server ",[55,703,599],{"class":61},[55,705,564],{"class":65},[55,707,70],{"class":69},[55,709,710],{"class":121},"\"0.0.0.0:8080\"",[55,712,713],{"class":69},",\n",[55,715,717,720,722,725,727],{"class":57,"line":716},36,[55,718,719],{"class":65},"    WithTimeout",[55,721,70],{"class":69},[55,723,724],{"class":134},"5",[55,726,95],{"class":61},[55,728,729],{"class":69},"time.Second),\n",[55,731,733,736],{"class":57,"line":732},37,[55,734,735],{"class":65},"    WithTLS",[55,737,738],{"class":69},"(tlsCfg),\n",[55,740,742,745],{"class":57,"line":741},38,[55,743,744],{"class":65},"    WithLogger",[55,746,747],{"class":69},"(myLogger),\n",[55,749,751],{"class":57,"line":750},39,[55,752,376],{"class":69},[16,754,755],{},"Плюсы: имена опций самодокументируются, добавление новых не ломает старый код, дефолты централизованы.",[16,757,260,758,760],{},[23,759,263],{}," нужно: если опций 1-2 — обычные параметры понятнее.",[35,762,764],{"id":763},"builder-через-functional-options","Builder — через Functional Options",[16,766,767,768,771],{},"В Go классический Builder с цепочкой ",[40,769,770],{},".WithX().WithY().Build()"," редко применяется. Functional Options покрывает большинство задач. Но если объект строится поэтапно с валидацией промежуточных состояний — Builder уместен.",[47,773,775],{"className":49,"code":774,"language":51,"meta":5,"style":5},"type QueryBuilder struct {\n    table  string\n    where  []string\n    args   []any\n    limit  int\n}\n\nfunc Query(table string) *QueryBuilder {\n    return &QueryBuilder{table: table}\n}\n\nfunc (q *QueryBuilder) Where(cond string, args ...any) *QueryBuilder {\n    q.where = append(q.where, cond)\n    q.args = append(q.args, args...)\n    return q\n}\n\nfunc (q *QueryBuilder) Limit(n int) *QueryBuilder {\n    q.limit = n\n    return q\n}\n\nfunc (q *QueryBuilder) Build() (string, []any) {\n    sql := \"SELECT * FROM \" + q.table\n    if len(q.where) > 0 {\n        sql += \" WHERE \" + strings.Join(q.where, \" AND \")\n    }\n    if q.limit > 0 {\n        sql += fmt.Sprintf(\" LIMIT %d\", q.limit)\n    }\n    return sql, q.args\n}\n\nsql, args := Query(\"users\").Where(\"age > ?\", 18).Limit(10).Build()\n",[40,776,777,788,795,802,810,818,822,826,849,860,864,868,912,925,942,949,953,957,990,1000,1006,1010,1014,1044,1060,1076,1103,1107,1120,1146,1150,1157,1161,1165],{"__ignoreMap":5},[55,778,779,781,784,786],{"class":57,"line":58},[55,780,285],{"class":61},[55,782,783],{"class":65}," QueryBuilder",[55,785,291],{"class":61},[55,787,125],{"class":69},[55,789,790,793],{"class":57,"line":109},[55,791,792],{"class":69},"    table  ",[55,794,301],{"class":61},[55,796,797,800],{"class":57,"line":128},[55,798,799],{"class":69},"    where  []",[55,801,301],{"class":61},[55,803,804,807],{"class":57,"line":141},[55,805,806],{"class":69},"    args   []",[55,808,809],{"class":65},"any\n",[55,811,812,815],{"class":57,"line":147},[55,813,814],{"class":69},"    limit  ",[55,816,817],{"class":61},"int\n",[55,819,820],{"class":57,"line":165},[55,821,254],{"class":69},[55,823,824],{"class":57,"line":175},[55,825,356],{"emptyLinePlaceholder":355},[55,827,828,830,833,835,838,840,842,844,847],{"class":57,"line":180},[55,829,62],{"class":61},[55,831,832],{"class":65}," Query",[55,834,70],{"class":69},[55,836,837],{"class":73},"table",[55,839,572],{"class":61},[55,841,403],{"class":69},[55,843,95],{"class":61},[55,845,846],{"class":65},"QueryBuilder",[55,848,125],{"class":69},[55,850,851,853,855,857],{"class":57,"line":194},[55,852,183],{"class":61},[55,854,186],{"class":61},[55,856,846],{"class":65},[55,858,859],{"class":69},"{table: table}\n",[55,861,862],{"class":57,"line":212},[55,863,254],{"class":69},[55,865,866],{"class":57,"line":218},[55,867,356],{"emptyLinePlaceholder":355},[55,869,870,872,875,878,880,882,884,887,889,892,894,896,899,901,904,906,908,910],{"class":57,"line":224},[55,871,62],{"class":61},[55,873,874],{"class":69}," (",[55,876,877],{"class":73},"q ",[55,879,95],{"class":61},[55,881,846],{"class":65},[55,883,403],{"class":69},[55,885,886],{"class":65},"Where",[55,888,70],{"class":69},[55,890,891],{"class":73},"cond",[55,893,572],{"class":61},[55,895,80],{"class":69},[55,897,898],{"class":73},"args",[55,900,580],{"class":61},[55,902,903],{"class":65},"any",[55,905,403],{"class":69},[55,907,95],{"class":61},[55,909,846],{"class":65},[55,911,125],{"class":69},[55,913,914,917,919,922],{"class":57,"line":230},[55,915,916],{"class":69},"    q.where ",[55,918,430],{"class":61},[55,920,921],{"class":65}," append",[55,923,924],{"class":69},"(q.where, cond)\n",[55,926,927,930,932,934,937,940],{"class":57,"line":242},[55,928,929],{"class":69},"    q.args ",[55,931,430],{"class":61},[55,933,921],{"class":65},[55,935,936],{"class":69},"(q.args, args",[55,938,939],{"class":61},"...",[55,941,376],{"class":69},[55,943,944,946],{"class":57,"line":251},[55,945,183],{"class":61},[55,947,948],{"class":69}," q\n",[55,950,951],{"class":57,"line":471},[55,952,254],{"class":69},[55,954,955],{"class":57,"line":494},[55,956,356],{"emptyLinePlaceholder":355},[55,958,959,961,963,965,967,969,971,974,976,979,982,984,986,988],{"class":57,"line":499},[55,960,62],{"class":61},[55,962,874],{"class":69},[55,964,877],{"class":73},[55,966,95],{"class":61},[55,968,846],{"class":65},[55,970,403],{"class":69},[55,972,973],{"class":65},"Limit",[55,975,70],{"class":69},[55,977,978],{"class":73},"n",[55,980,981],{"class":61}," int",[55,983,403],{"class":69},[55,985,95],{"class":61},[55,987,846],{"class":65},[55,989,125],{"class":69},[55,991,992,995,997],{"class":57,"line":504},[55,993,994],{"class":69},"    q.limit ",[55,996,430],{"class":61},[55,998,999],{"class":69}," n\n",[55,1001,1002,1004],{"class":57,"line":526},[55,1003,183],{"class":61},[55,1005,948],{"class":69},[55,1007,1008],{"class":57,"line":549},[55,1009,254],{"class":69},[55,1011,1012],{"class":57,"line":554},[55,1013,356],{"emptyLinePlaceholder":355},[55,1015,1016,1018,1020,1022,1024,1026,1028,1031,1034,1037,1040,1042],{"class":57,"line":559},[55,1017,62],{"class":61},[55,1019,874],{"class":69},[55,1021,877],{"class":73},[55,1023,95],{"class":61},[55,1025,846],{"class":65},[55,1027,403],{"class":69},[55,1029,1030],{"class":65},"Build",[55,1032,1033],{"class":69},"() (",[55,1035,1036],{"class":61},"string",[55,1038,1039],{"class":69},", []",[55,1041,903],{"class":65},[55,1043,106],{"class":69},[55,1045,1046,1049,1051,1054,1057],{"class":57,"line":593},[55,1047,1048],{"class":69},"    sql ",[55,1050,599],{"class":61},[55,1052,1053],{"class":121}," \"SELECT * FROM \"",[55,1055,1056],{"class":61}," +",[55,1058,1059],{"class":69}," q.table\n",[55,1061,1062,1064,1066,1069,1072,1074],{"class":57,"line":608},[55,1063,112],{"class":61},[55,1065,152],{"class":65},[55,1067,1068],{"class":69},"(q.where) ",[55,1070,1071],{"class":61},">",[55,1073,160],{"class":134},[55,1075,125],{"class":69},[55,1077,1078,1081,1084,1087,1089,1092,1095,1098,1101],{"class":57,"line":614},[55,1079,1080],{"class":69},"        sql ",[55,1082,1083],{"class":61},"+=",[55,1085,1086],{"class":121}," \" WHERE \"",[55,1088,1056],{"class":61},[55,1090,1091],{"class":69}," strings.",[55,1093,1094],{"class":65},"Join",[55,1096,1097],{"class":69},"(q.where, ",[55,1099,1100],{"class":121},"\" AND \"",[55,1102,376],{"class":69},[55,1104,1105],{"class":57,"line":632},[55,1106,144],{"class":69},[55,1108,1109,1111,1114,1116,1118],{"class":57,"line":644},[55,1110,112],{"class":61},[55,1112,1113],{"class":69}," q.limit ",[55,1115,1071],{"class":61},[55,1117,160],{"class":134},[55,1119,125],{"class":69},[55,1121,1122,1124,1126,1129,1132,1134,1137,1140,1143],{"class":57,"line":649},[55,1123,1080],{"class":69},[55,1125,1083],{"class":61},[55,1127,1128],{"class":69}," fmt.",[55,1130,1131],{"class":65},"Sprintf",[55,1133,70],{"class":69},[55,1135,1136],{"class":121},"\" LIMIT ",[55,1138,1139],{"class":134},"%d",[55,1141,1142],{"class":121},"\"",[55,1144,1145],{"class":69},", q.limit)\n",[55,1147,1148],{"class":57,"line":666},[55,1149,144],{"class":69},[55,1151,1152,1154],{"class":57,"line":675},[55,1153,183],{"class":61},[55,1155,1156],{"class":69}," sql, q.args\n",[55,1158,1159],{"class":57,"line":680},[55,1160,254],{"class":69},[55,1162,1163],{"class":57,"line":688},[55,1164,356],{"emptyLinePlaceholder":355},[55,1166,1167,1170,1172,1174,1176,1179,1182,1184,1186,1189,1191,1194,1196,1198,1200,1203,1205,1207],{"class":57,"line":693},[55,1168,1169],{"class":69},"sql, args ",[55,1171,599],{"class":61},[55,1173,832],{"class":65},[55,1175,70],{"class":69},[55,1177,1178],{"class":121},"\"users\"",[55,1180,1181],{"class":69},").",[55,1183,886],{"class":65},[55,1185,70],{"class":69},[55,1187,1188],{"class":121},"\"age > ?\"",[55,1190,80],{"class":69},[55,1192,1193],{"class":134},"18",[55,1195,1181],{"class":69},[55,1197,973],{"class":65},[55,1199,70],{"class":69},[55,1201,1202],{"class":134},"10",[55,1204,1181],{"class":69},[55,1206,1030],{"class":65},[55,1208,1209],{"class":69},"()\n",[35,1211,1213],{"id":1212},"singleton-почти-всегда-не-нужен","Singleton — почти всегда не нужен",[16,1215,1216,1217,312],{},"В Go нет приватных конструкторов и единственный «нормальный» Singleton — глобальная переменная + ",[40,1218,1219],{},"sync.Once",[47,1221,1223],{"className":49,"code":1222,"language":51,"meta":5,"style":5},"var (\n    redisClient *redis.Client\n    redisOnce   sync.Once\n)\n\nfunc Redis() *redis.Client {\n    redisOnce.Do(func() {\n        redisClient = redis.NewClient(&redis.Options{Addr: cfg.RedisAddr})\n    })\n    return redisClient\n}\n",[40,1224,1225,1233,1248,1261,1265,1269,1290,1305,1333,1338,1345],{"__ignoreMap":5},[55,1226,1227,1230],{"class":57,"line":58},[55,1228,1229],{"class":61},"var",[55,1231,1232],{"class":69}," (\n",[55,1234,1235,1238,1240,1243,1245],{"class":57,"line":109},[55,1236,1237],{"class":69},"    redisClient ",[55,1239,95],{"class":61},[55,1241,1242],{"class":65},"redis",[55,1244,312],{"class":69},[55,1246,1247],{"class":65},"Client\n",[55,1249,1250,1253,1256,1258],{"class":57,"line":128},[55,1251,1252],{"class":69},"    redisOnce   ",[55,1254,1255],{"class":65},"sync",[55,1257,312],{"class":69},[55,1259,1260],{"class":65},"Once\n",[55,1262,1263],{"class":57,"line":141},[55,1264,376],{"class":69},[55,1266,1267],{"class":57,"line":147},[55,1268,356],{"emptyLinePlaceholder":355},[55,1270,1271,1273,1276,1279,1281,1283,1285,1288],{"class":57,"line":165},[55,1272,62],{"class":61},[55,1274,1275],{"class":65}," Redis",[55,1277,1278],{"class":69},"() ",[55,1280,95],{"class":61},[55,1282,1242],{"class":65},[55,1284,312],{"class":69},[55,1286,1287],{"class":65},"Client",[55,1289,125],{"class":69},[55,1291,1292,1295,1298,1300,1302],{"class":57,"line":175},[55,1293,1294],{"class":69},"    redisOnce.",[55,1296,1297],{"class":65},"Do",[55,1299,70],{"class":69},[55,1301,62],{"class":61},[55,1303,1304],{"class":69},"() {\n",[55,1306,1307,1310,1312,1315,1318,1320,1323,1325,1327,1330],{"class":57,"line":180},[55,1308,1309],{"class":69},"        redisClient ",[55,1311,430],{"class":61},[55,1313,1314],{"class":69}," redis.",[55,1316,1317],{"class":65},"NewClient",[55,1319,70],{"class":69},[55,1321,1322],{"class":61},"&",[55,1324,1242],{"class":65},[55,1326,312],{"class":69},[55,1328,1329],{"class":65},"Options",[55,1331,1332],{"class":69},"{Addr: cfg.RedisAddr})\n",[55,1334,1335],{"class":57,"line":194},[55,1336,1337],{"class":69},"    })\n",[55,1339,1340,1342],{"class":57,"line":212},[55,1341,183],{"class":61},[55,1343,1344],{"class":69}," redisClient\n",[55,1346,1347],{"class":57,"line":218},[55,1348,254],{"class":69},[16,1350,1351],{},"Когда применять:",[1353,1354,1355,1359],"ul",{},[1356,1357,1358],"li",{},"Подключение к БД, Redis, очереди — реально один на процесс.",[1356,1360,1361],{},"Логгер, метрики, конфиг.",[16,1363,260,1364,1366],{},[23,1365,263],{}," применять:",[1353,1368,1369],{},[1356,1370,1371],{},"Везде где можно — DI лучше. Singleton мешает тестам (моки), скрывает зависимости, провоцирует глобальное состояние.",[16,1373,1374,1375,1378],{},"Идиоматично: создавать сервисы в ",[40,1376,1377],{},"main",", передавать как параметры.",[28,1380],{},[11,1382,1384],{"id":1383},"structural","Structural",[35,1386,1388],{"id":1387},"adapter","Adapter",[16,1390,1391],{},"Проблема: внешний API не подходит под наш интерфейс. Нужен переходник.",[47,1393,1395],{"className":49,"code":1394,"language":51,"meta":5,"style":5},"\u002F\u002F Наш интерфейс\ntype Cache interface {\n    Get(ctx context.Context, key string) ([]byte, error)\n    Set(ctx context.Context, key string, value []byte, ttl time.Duration) error\n}\n\n\u002F\u002F Внешняя библиотека с другим API\ntype RedisAdapter struct{ c *redis.Client }\n\nfunc (r *RedisAdapter) Get(ctx context.Context, key string) ([]byte, error) {\n    val, err := r.c.Get(ctx, key).Bytes()\n    if err == redis.Nil {\n        return nil, ErrNotFound\n    }\n    return val, err\n}\n\nfunc (r *RedisAdapter) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error {\n    return r.c.Set(ctx, key, value, ttl).Err()\n}\n",[40,1396,1397,1402,1414,1451,1497,1501,1505,1510,1533,1537,1582,1602,1614,1623,1627,1634,1638,1642,1699,1715],{"__ignoreMap":5},[55,1398,1399],{"class":57,"line":58},[55,1400,1401],{"class":628},"\u002F\u002F Наш интерфейс\n",[55,1403,1404,1406,1409,1412],{"class":57,"line":109},[55,1405,285],{"class":61},[55,1407,1408],{"class":65}," Cache",[55,1410,1411],{"class":61}," interface",[55,1413,125],{"class":69},[55,1415,1416,1419,1421,1424,1427,1429,1432,1434,1437,1439,1442,1445,1447,1449],{"class":57,"line":128},[55,1417,1418],{"class":65},"    Get",[55,1420,70],{"class":69},[55,1422,1423],{"class":73},"ctx",[55,1425,1426],{"class":65}," context",[55,1428,312],{"class":69},[55,1430,1431],{"class":65},"Context",[55,1433,80],{"class":69},[55,1435,1436],{"class":73},"key",[55,1438,572],{"class":61},[55,1440,1441],{"class":69},") ([]",[55,1443,1444],{"class":61},"byte",[55,1446,80],{"class":69},[55,1448,103],{"class":61},[55,1450,376],{"class":69},[55,1452,1453,1456,1458,1460,1462,1464,1466,1468,1470,1472,1474,1477,1479,1481,1483,1486,1488,1490,1492,1494],{"class":57,"line":141},[55,1454,1455],{"class":65},"    Set",[55,1457,70],{"class":69},[55,1459,1423],{"class":73},[55,1461,1426],{"class":65},[55,1463,312],{"class":69},[55,1465,1431],{"class":65},[55,1467,80],{"class":69},[55,1469,1436],{"class":73},[55,1471,572],{"class":61},[55,1473,80],{"class":69},[55,1475,1476],{"class":73},"value",[55,1478,86],{"class":69},[55,1480,1444],{"class":61},[55,1482,80],{"class":69},[55,1484,1485],{"class":73},"ttl",[55,1487,395],{"class":65},[55,1489,312],{"class":69},[55,1491,400],{"class":65},[55,1493,403],{"class":69},[55,1495,1496],{"class":61},"error\n",[55,1498,1499],{"class":57,"line":147},[55,1500,254],{"class":69},[55,1502,1503],{"class":57,"line":165},[55,1504,356],{"emptyLinePlaceholder":355},[55,1506,1507],{"class":57,"line":175},[55,1508,1509],{"class":628},"\u002F\u002F Внешняя библиотека с другим API\n",[55,1511,1512,1514,1517,1519,1522,1524,1526,1528,1530],{"class":57,"line":180},[55,1513,285],{"class":61},[55,1515,1516],{"class":65}," RedisAdapter",[55,1518,291],{"class":61},[55,1520,1521],{"class":69},"{ c ",[55,1523,95],{"class":61},[55,1525,1242],{"class":65},[55,1527,312],{"class":69},[55,1529,1287],{"class":65},[55,1531,1532],{"class":69}," }\n",[55,1534,1535],{"class":57,"line":194},[55,1536,356],{"emptyLinePlaceholder":355},[55,1538,1539,1541,1543,1546,1548,1551,1553,1556,1558,1560,1562,1564,1566,1568,1570,1572,1574,1576,1578,1580],{"class":57,"line":212},[55,1540,62],{"class":61},[55,1542,874],{"class":69},[55,1544,1545],{"class":73},"r ",[55,1547,95],{"class":61},[55,1549,1550],{"class":65},"RedisAdapter",[55,1552,403],{"class":69},[55,1554,1555],{"class":65},"Get",[55,1557,70],{"class":69},[55,1559,1423],{"class":73},[55,1561,1426],{"class":65},[55,1563,312],{"class":69},[55,1565,1431],{"class":65},[55,1567,80],{"class":69},[55,1569,1436],{"class":73},[55,1571,572],{"class":61},[55,1573,1441],{"class":69},[55,1575,1444],{"class":61},[55,1577,80],{"class":69},[55,1579,103],{"class":61},[55,1581,106],{"class":69},[55,1583,1584,1587,1589,1592,1594,1597,1600],{"class":57,"line":218},[55,1585,1586],{"class":69},"    val, err ",[55,1588,599],{"class":61},[55,1590,1591],{"class":69}," r.c.",[55,1593,1555],{"class":65},[55,1595,1596],{"class":69},"(ctx, key).",[55,1598,1599],{"class":65},"Bytes",[55,1601,1209],{"class":69},[55,1603,1604,1606,1609,1611],{"class":57,"line":224},[55,1605,112],{"class":61},[55,1607,1608],{"class":69}," err ",[55,1610,118],{"class":61},[55,1612,1613],{"class":69}," redis.Nil {\n",[55,1615,1616,1618,1620],{"class":57,"line":230},[55,1617,131],{"class":61},[55,1619,135],{"class":134},[55,1621,1622],{"class":69},", ErrNotFound\n",[55,1624,1625],{"class":57,"line":242},[55,1626,144],{"class":69},[55,1628,1629,1631],{"class":57,"line":251},[55,1630,183],{"class":61},[55,1632,1633],{"class":69}," val, err\n",[55,1635,1636],{"class":57,"line":471},[55,1637,254],{"class":69},[55,1639,1640],{"class":57,"line":494},[55,1641,356],{"emptyLinePlaceholder":355},[55,1643,1644,1646,1648,1650,1652,1654,1656,1659,1661,1663,1665,1667,1669,1671,1673,1675,1677,1679,1681,1683,1685,1687,1689,1691,1693,1695,1697],{"class":57,"line":499},[55,1645,62],{"class":61},[55,1647,874],{"class":69},[55,1649,1545],{"class":73},[55,1651,95],{"class":61},[55,1653,1550],{"class":65},[55,1655,403],{"class":69},[55,1657,1658],{"class":65},"Set",[55,1660,70],{"class":69},[55,1662,1423],{"class":73},[55,1664,1426],{"class":65},[55,1666,312],{"class":69},[55,1668,1431],{"class":65},[55,1670,80],{"class":69},[55,1672,1436],{"class":73},[55,1674,572],{"class":61},[55,1676,80],{"class":69},[55,1678,1476],{"class":73},[55,1680,86],{"class":69},[55,1682,1444],{"class":61},[55,1684,80],{"class":69},[55,1686,1485],{"class":73},[55,1688,395],{"class":65},[55,1690,312],{"class":69},[55,1692,400],{"class":65},[55,1694,403],{"class":69},[55,1696,103],{"class":61},[55,1698,125],{"class":69},[55,1700,1701,1703,1705,1707,1710,1713],{"class":57,"line":504},[55,1702,183],{"class":61},[55,1704,1591],{"class":69},[55,1706,1658],{"class":65},[55,1708,1709],{"class":69},"(ctx, key, value, ttl).",[55,1711,1712],{"class":65},"Err",[55,1714,1209],{"class":69},[55,1716,1717],{"class":57,"line":526},[55,1718,254],{"class":69},[16,1720,1721],{},"Разница с портами\u002Fадаптерами в Hexagonal: там Adapter — архитектурный концепт (ввод\u002Fвывод системы), здесь — точечный паттерн для одного интерфейса. Идея та же, масштаб разный.",[35,1723,1725],{"id":1724},"decorator-middleware-обёртка-поведения","Decorator \u002F Middleware — обёртка поведения",[47,1727,1729],{"className":49,"code":1728,"language":51,"meta":5,"style":5},"type Handler interface {\n    Handle(ctx context.Context, req Request) (Response, error)\n}\n\n\u002F\u002F Декоратор добавляет логирование\ntype LoggingDecorator struct {\n    next Handler\n    log  Logger\n}\n\nfunc (d *LoggingDecorator) Handle(ctx context.Context, req Request) (Response, error) {\n    d.log.Info(\"request\", \"type\", req.Type)\n    start := time.Now()\n    resp, err := d.next.Handle(ctx, req)\n    d.log.Info(\"response\", \"took\", time.Since(start), \"err\", err)\n    return resp, err\n}\n\n\u002F\u002F Стек: Logging → Metrics → Auth → Handler\nh := &LoggingDecorator{next: &MetricsDecorator{next: &AuthDecorator{next: realHandler}}}\n",[40,1730,1731,1742,1776,1780,1784,1789,1800,1808,1815,1819,1823,1868,1889,1903,1918,1949,1956,1960,1964,1969],{"__ignoreMap":5},[55,1732,1733,1735,1738,1740],{"class":57,"line":58},[55,1734,285],{"class":61},[55,1736,1737],{"class":65}," Handler",[55,1739,1411],{"class":61},[55,1741,125],{"class":69},[55,1743,1744,1747,1749,1751,1753,1755,1757,1759,1762,1765,1767,1770,1772,1774],{"class":57,"line":109},[55,1745,1746],{"class":65},"    Handle",[55,1748,70],{"class":69},[55,1750,1423],{"class":73},[55,1752,1426],{"class":65},[55,1754,312],{"class":69},[55,1756,1431],{"class":65},[55,1758,80],{"class":69},[55,1760,1761],{"class":73},"req",[55,1763,1764],{"class":65}," Request",[55,1766,92],{"class":69},[55,1768,1769],{"class":65},"Response",[55,1771,80],{"class":69},[55,1773,103],{"class":61},[55,1775,376],{"class":69},[55,1777,1778],{"class":57,"line":128},[55,1779,254],{"class":69},[55,1781,1782],{"class":57,"line":141},[55,1783,356],{"emptyLinePlaceholder":355},[55,1785,1786],{"class":57,"line":147},[55,1787,1788],{"class":628},"\u002F\u002F Декоратор добавляет логирование\n",[55,1790,1791,1793,1796,1798],{"class":57,"line":165},[55,1792,285],{"class":61},[55,1794,1795],{"class":65}," LoggingDecorator",[55,1797,291],{"class":61},[55,1799,125],{"class":69},[55,1801,1802,1805],{"class":57,"line":175},[55,1803,1804],{"class":69},"    next ",[55,1806,1807],{"class":65},"Handler\n",[55,1809,1810,1813],{"class":57,"line":180},[55,1811,1812],{"class":69},"    log  ",[55,1814,346],{"class":65},[55,1816,1817],{"class":57,"line":194},[55,1818,254],{"class":69},[55,1820,1821],{"class":57,"line":212},[55,1822,356],{"emptyLinePlaceholder":355},[55,1824,1825,1827,1829,1832,1834,1837,1839,1842,1844,1846,1848,1850,1852,1854,1856,1858,1860,1862,1864,1866],{"class":57,"line":218},[55,1826,62],{"class":61},[55,1828,874],{"class":69},[55,1830,1831],{"class":73},"d ",[55,1833,95],{"class":61},[55,1835,1836],{"class":65},"LoggingDecorator",[55,1838,403],{"class":69},[55,1840,1841],{"class":65},"Handle",[55,1843,70],{"class":69},[55,1845,1423],{"class":73},[55,1847,1426],{"class":65},[55,1849,312],{"class":69},[55,1851,1431],{"class":65},[55,1853,80],{"class":69},[55,1855,1761],{"class":73},[55,1857,1764],{"class":65},[55,1859,92],{"class":69},[55,1861,1769],{"class":65},[55,1863,80],{"class":69},[55,1865,103],{"class":61},[55,1867,106],{"class":69},[55,1869,1870,1873,1876,1878,1881,1883,1886],{"class":57,"line":224},[55,1871,1872],{"class":69},"    d.log.",[55,1874,1875],{"class":65},"Info",[55,1877,70],{"class":69},[55,1879,1880],{"class":121},"\"request\"",[55,1882,80],{"class":69},[55,1884,1885],{"class":121},"\"type\"",[55,1887,1888],{"class":69},", req.Type)\n",[55,1890,1891,1894,1896,1899,1901],{"class":57,"line":230},[55,1892,1893],{"class":69},"    start ",[55,1895,599],{"class":61},[55,1897,1898],{"class":69}," time.",[55,1900,236],{"class":65},[55,1902,1209],{"class":69},[55,1904,1905,1908,1910,1913,1915],{"class":57,"line":242},[55,1906,1907],{"class":69},"    resp, err ",[55,1909,599],{"class":61},[55,1911,1912],{"class":69}," d.next.",[55,1914,1841],{"class":65},[55,1916,1917],{"class":69},"(ctx, req)\n",[55,1919,1920,1922,1924,1926,1929,1931,1934,1937,1940,1943,1946],{"class":57,"line":251},[55,1921,1872],{"class":69},[55,1923,1875],{"class":65},[55,1925,70],{"class":69},[55,1927,1928],{"class":121},"\"response\"",[55,1930,80],{"class":69},[55,1932,1933],{"class":121},"\"took\"",[55,1935,1936],{"class":69},", time.",[55,1938,1939],{"class":65},"Since",[55,1941,1942],{"class":69},"(start), ",[55,1944,1945],{"class":121},"\"err\"",[55,1947,1948],{"class":69},", err)\n",[55,1950,1951,1953],{"class":57,"line":471},[55,1952,183],{"class":61},[55,1954,1955],{"class":69}," resp, err\n",[55,1957,1958],{"class":57,"line":494},[55,1959,254],{"class":69},[55,1961,1962],{"class":57,"line":499},[55,1963,356],{"emptyLinePlaceholder":355},[55,1965,1966],{"class":57,"line":504},[55,1967,1968],{"class":628},"\u002F\u002F Стек: Logging → Metrics → Auth → Handler\n",[55,1970,1971,1974,1976,1978,1980,1983,1985,1988,1990,1992,1995],{"class":57,"line":526},[55,1972,1973],{"class":69},"h ",[55,1975,599],{"class":61},[55,1977,186],{"class":61},[55,1979,1836],{"class":65},[55,1981,1982],{"class":69},"{next: ",[55,1984,1322],{"class":61},[55,1986,1987],{"class":65},"MetricsDecorator",[55,1989,1982],{"class":69},[55,1991,1322],{"class":61},[55,1993,1994],{"class":65},"AuthDecorator",[55,1996,1997],{"class":69},"{next: realHandler}}}\n",[16,1999,2000],{},"В HTTP — каноничный middleware:",[47,2002,2004],{"className":49,"code":2003,"language":51,"meta":5,"style":5},"func Logging(next http.Handler) http.Handler {\n    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        log.Println(r.Method, r.URL.Path)\n        next.ServeHTTP(w, r)\n    })\n}\n\nrouter.Handle(\"\u002Fapi\", Logging(Auth(myHandler)))\n",[40,2005,2006,2037,2079,2090,2101,2105,2109,2113],{"__ignoreMap":5},[55,2007,2008,2010,2013,2015,2018,2021,2023,2026,2028,2031,2033,2035],{"class":57,"line":58},[55,2009,62],{"class":61},[55,2011,2012],{"class":65}," Logging",[55,2014,70],{"class":69},[55,2016,2017],{"class":73},"next",[55,2019,2020],{"class":65}," http",[55,2022,312],{"class":69},[55,2024,2025],{"class":65},"Handler",[55,2027,403],{"class":69},[55,2029,2030],{"class":65},"http",[55,2032,312],{"class":69},[55,2034,2025],{"class":65},[55,2036,125],{"class":69},[55,2038,2039,2041,2044,2047,2049,2051,2053,2056,2058,2060,2063,2065,2068,2070,2072,2074,2077],{"class":57,"line":109},[55,2040,183],{"class":61},[55,2042,2043],{"class":69}," http.",[55,2045,2046],{"class":65},"HandlerFunc",[55,2048,70],{"class":69},[55,2050,62],{"class":61},[55,2052,70],{"class":69},[55,2054,2055],{"class":73},"w",[55,2057,2020],{"class":65},[55,2059,312],{"class":69},[55,2061,2062],{"class":65},"ResponseWriter",[55,2064,80],{"class":69},[55,2066,2067],{"class":73},"r",[55,2069,422],{"class":61},[55,2071,2030],{"class":65},[55,2073,312],{"class":69},[55,2075,2076],{"class":65},"Request",[55,2078,106],{"class":69},[55,2080,2081,2084,2087],{"class":57,"line":128},[55,2082,2083],{"class":69},"        log.",[55,2085,2086],{"class":65},"Println",[55,2088,2089],{"class":69},"(r.Method, r.URL.Path)\n",[55,2091,2092,2095,2098],{"class":57,"line":141},[55,2093,2094],{"class":69},"        next.",[55,2096,2097],{"class":65},"ServeHTTP",[55,2099,2100],{"class":69},"(w, r)\n",[55,2102,2103],{"class":57,"line":147},[55,2104,1337],{"class":69},[55,2106,2107],{"class":57,"line":165},[55,2108,254],{"class":69},[55,2110,2111],{"class":57,"line":175},[55,2112,356],{"emptyLinePlaceholder":355},[55,2114,2115,2118,2120,2122,2125,2127,2130,2132,2135],{"class":57,"line":180},[55,2116,2117],{"class":69},"router.",[55,2119,1841],{"class":65},[55,2121,70],{"class":69},[55,2123,2124],{"class":121},"\"\u002Fapi\"",[55,2126,80],{"class":69},[55,2128,2129],{"class":65},"Logging",[55,2131,70],{"class":69},[55,2133,2134],{"class":65},"Auth",[55,2136,2137],{"class":69},"(myHandler)))\n",[16,2139,2140],{},"Каждая обёртка делает одну вещь и передаёт дальше.",[28,2142],{},[11,2144,2146],{"id":2145},"behavioral","Behavioral",[35,2148,2150],{"id":2149},"strategy","Strategy",[16,2152,2153],{},"Проблема: алгоритм выбирается в runtime, а не зашит в код.",[47,2155,2157],{"className":49,"code":2156,"language":51,"meta":5,"style":5},"type Notifier interface {\n    Send(ctx context.Context, msg string) error\n}\n\ntype EmailNotifier struct{ \u002F* SMTP *\u002F }\nfunc (e *EmailNotifier) Send(ctx context.Context, msg string) error { \u002F* ... *\u002F }\n\ntype SlackNotifier struct{ \u002F* webhook *\u002F }\nfunc (s *SlackNotifier) Send(ctx context.Context, msg string) error { \u002F* ... *\u002F }\n\ntype TelegramNotifier struct{ \u002F* bot api *\u002F }\nfunc (t *TelegramNotifier) Send(ctx context.Context, msg string) error { \u002F* ... *\u002F }\n\n\u002F\u002F Вызывающий код не знает реализации\nfunc NotifyUser(ctx context.Context, n Notifier, msg string) error {\n    return n.Send(ctx, msg)\n}\n",[40,2158,2159,2170,2196,2200,2204,2221,2268,2272,2288,2332,2336,2352,2396,2400,2405,2440,2452],{"__ignoreMap":5},[55,2160,2161,2163,2166,2168],{"class":57,"line":58},[55,2162,285],{"class":61},[55,2164,2165],{"class":65}," Notifier",[55,2167,1411],{"class":61},[55,2169,125],{"class":69},[55,2171,2172,2175,2177,2179,2181,2183,2185,2187,2190,2192,2194],{"class":57,"line":109},[55,2173,2174],{"class":65},"    Send",[55,2176,70],{"class":69},[55,2178,1423],{"class":73},[55,2180,1426],{"class":65},[55,2182,312],{"class":69},[55,2184,1431],{"class":65},[55,2186,80],{"class":69},[55,2188,2189],{"class":73},"msg",[55,2191,572],{"class":61},[55,2193,403],{"class":69},[55,2195,1496],{"class":61},[55,2197,2198],{"class":57,"line":128},[55,2199,254],{"class":69},[55,2201,2202],{"class":57,"line":141},[55,2203,356],{"emptyLinePlaceholder":355},[55,2205,2206,2208,2211,2213,2216,2219],{"class":57,"line":147},[55,2207,285],{"class":61},[55,2209,2210],{"class":65}," EmailNotifier",[55,2212,291],{"class":61},[55,2214,2215],{"class":69},"{ ",[55,2217,2218],{"class":628},"\u002F* SMTP *\u002F",[55,2220,1532],{"class":69},[55,2222,2223,2225,2227,2230,2232,2235,2237,2240,2242,2244,2246,2248,2250,2252,2254,2256,2258,2260,2263,2266],{"class":57,"line":165},[55,2224,62],{"class":61},[55,2226,874],{"class":69},[55,2228,2229],{"class":73},"e ",[55,2231,95],{"class":61},[55,2233,2234],{"class":65},"EmailNotifier",[55,2236,403],{"class":69},[55,2238,2239],{"class":65},"Send",[55,2241,70],{"class":69},[55,2243,1423],{"class":73},[55,2245,1426],{"class":65},[55,2247,312],{"class":69},[55,2249,1431],{"class":65},[55,2251,80],{"class":69},[55,2253,2189],{"class":73},[55,2255,572],{"class":61},[55,2257,403],{"class":69},[55,2259,103],{"class":61},[55,2261,2262],{"class":69}," { ",[55,2264,2265],{"class":628},"\u002F* ... *\u002F",[55,2267,1532],{"class":69},[55,2269,2270],{"class":57,"line":175},[55,2271,356],{"emptyLinePlaceholder":355},[55,2273,2274,2276,2279,2281,2283,2286],{"class":57,"line":180},[55,2275,285],{"class":61},[55,2277,2278],{"class":65}," SlackNotifier",[55,2280,291],{"class":61},[55,2282,2215],{"class":69},[55,2284,2285],{"class":628},"\u002F* webhook *\u002F",[55,2287,1532],{"class":69},[55,2289,2290,2292,2294,2297,2299,2302,2304,2306,2308,2310,2312,2314,2316,2318,2320,2322,2324,2326,2328,2330],{"class":57,"line":194},[55,2291,62],{"class":61},[55,2293,874],{"class":69},[55,2295,2296],{"class":73},"s ",[55,2298,95],{"class":61},[55,2300,2301],{"class":65},"SlackNotifier",[55,2303,403],{"class":69},[55,2305,2239],{"class":65},[55,2307,70],{"class":69},[55,2309,1423],{"class":73},[55,2311,1426],{"class":65},[55,2313,312],{"class":69},[55,2315,1431],{"class":65},[55,2317,80],{"class":69},[55,2319,2189],{"class":73},[55,2321,572],{"class":61},[55,2323,403],{"class":69},[55,2325,103],{"class":61},[55,2327,2262],{"class":69},[55,2329,2265],{"class":628},[55,2331,1532],{"class":69},[55,2333,2334],{"class":57,"line":212},[55,2335,356],{"emptyLinePlaceholder":355},[55,2337,2338,2340,2343,2345,2347,2350],{"class":57,"line":218},[55,2339,285],{"class":61},[55,2341,2342],{"class":65}," TelegramNotifier",[55,2344,291],{"class":61},[55,2346,2215],{"class":69},[55,2348,2349],{"class":628},"\u002F* bot api *\u002F",[55,2351,1532],{"class":69},[55,2353,2354,2356,2358,2361,2363,2366,2368,2370,2372,2374,2376,2378,2380,2382,2384,2386,2388,2390,2392,2394],{"class":57,"line":224},[55,2355,62],{"class":61},[55,2357,874],{"class":69},[55,2359,2360],{"class":73},"t ",[55,2362,95],{"class":61},[55,2364,2365],{"class":65},"TelegramNotifier",[55,2367,403],{"class":69},[55,2369,2239],{"class":65},[55,2371,70],{"class":69},[55,2373,1423],{"class":73},[55,2375,1426],{"class":65},[55,2377,312],{"class":69},[55,2379,1431],{"class":65},[55,2381,80],{"class":69},[55,2383,2189],{"class":73},[55,2385,572],{"class":61},[55,2387,403],{"class":69},[55,2389,103],{"class":61},[55,2391,2262],{"class":69},[55,2393,2265],{"class":628},[55,2395,1532],{"class":69},[55,2397,2398],{"class":57,"line":230},[55,2399,356],{"emptyLinePlaceholder":355},[55,2401,2402],{"class":57,"line":242},[55,2403,2404],{"class":628},"\u002F\u002F Вызывающий код не знает реализации\n",[55,2406,2407,2409,2412,2414,2416,2418,2420,2422,2424,2426,2428,2430,2432,2434,2436,2438],{"class":57,"line":251},[55,2408,62],{"class":61},[55,2410,2411],{"class":65}," NotifyUser",[55,2413,70],{"class":69},[55,2415,1423],{"class":73},[55,2417,1426],{"class":65},[55,2419,312],{"class":69},[55,2421,1431],{"class":65},[55,2423,80],{"class":69},[55,2425,978],{"class":73},[55,2427,2165],{"class":65},[55,2429,80],{"class":69},[55,2431,2189],{"class":73},[55,2433,572],{"class":61},[55,2435,403],{"class":69},[55,2437,103],{"class":61},[55,2439,125],{"class":69},[55,2441,2442,2444,2447,2449],{"class":57,"line":471},[55,2443,183],{"class":61},[55,2445,2446],{"class":69}," n.",[55,2448,2239],{"class":65},[55,2450,2451],{"class":69},"(ctx, msg)\n",[55,2453,2454],{"class":57,"line":494},[55,2455,254],{"class":69},[16,2457,2458],{},"В Go — это просто интерфейс с реализациями. По сути — порты\u002Fадаптеры из Hexagonal в миниатюре.",[35,2460,2462],{"id":2461},"observer-pub-sub-через-каналы","Observer \u002F Pub-Sub через каналы",[47,2464,2466],{"className":49,"code":2465,"language":51,"meta":5,"style":5},"type EventBus struct {\n    mu          sync.RWMutex\n    subscribers map[EventType][]chan\u003C- Event\n}\n\nfunc (b *EventBus) Subscribe(t EventType, ch chan\u003C- Event) {\n    b.mu.Lock()\n    defer b.mu.Unlock()\n    b.subscribers[t] = append(b.subscribers[t], ch)\n}\n\nfunc (b *EventBus) Publish(e Event) {\n    b.mu.RLock()\n    subs := b.subscribers[e.Type()]\n    b.mu.RUnlock()\n    for _, ch := range subs {\n        select {\n        case ch \u003C- e:\n        default: \u002F\u002F не блокируемся, если подписчик медленный\n        }\n    }\n}\n",[40,2467,2468,2479,2491,2514,2518,2522,2561,2571,2584,2596,2600,2604,2630,2639,2655,2664,2678,2685,2699,2710,2715,2719],{"__ignoreMap":5},[55,2469,2470,2472,2475,2477],{"class":57,"line":58},[55,2471,285],{"class":61},[55,2473,2474],{"class":65}," EventBus",[55,2476,291],{"class":61},[55,2478,125],{"class":69},[55,2480,2481,2484,2486,2488],{"class":57,"line":109},[55,2482,2483],{"class":69},"    mu          ",[55,2485,1255],{"class":65},[55,2487,312],{"class":69},[55,2489,2490],{"class":65},"RWMutex\n",[55,2492,2493,2496,2499,2502,2505,2508,2511],{"class":57,"line":128},[55,2494,2495],{"class":69},"    subscribers ",[55,2497,2498],{"class":61},"map",[55,2500,2501],{"class":69},"[",[55,2503,2504],{"class":65},"EventType",[55,2506,2507],{"class":69},"][]",[55,2509,2510],{"class":61},"chan\u003C-",[55,2512,2513],{"class":65}," Event\n",[55,2515,2516],{"class":57,"line":141},[55,2517,254],{"class":69},[55,2519,2520],{"class":57,"line":147},[55,2521,356],{"emptyLinePlaceholder":355},[55,2523,2524,2526,2528,2531,2533,2536,2538,2541,2543,2545,2548,2550,2553,2556,2559],{"class":57,"line":165},[55,2525,62],{"class":61},[55,2527,874],{"class":69},[55,2529,2530],{"class":73},"b ",[55,2532,95],{"class":61},[55,2534,2535],{"class":65},"EventBus",[55,2537,403],{"class":69},[55,2539,2540],{"class":65},"Subscribe",[55,2542,70],{"class":69},[55,2544,392],{"class":73},[55,2546,2547],{"class":65}," EventType",[55,2549,80],{"class":69},[55,2551,2552],{"class":73},"ch",[55,2554,2555],{"class":61}," chan\u003C-",[55,2557,2558],{"class":65}," Event",[55,2560,106],{"class":69},[55,2562,2563,2566,2569],{"class":57,"line":175},[55,2564,2565],{"class":69},"    b.mu.",[55,2567,2568],{"class":65},"Lock",[55,2570,1209],{"class":69},[55,2572,2573,2576,2579,2582],{"class":57,"line":180},[55,2574,2575],{"class":61},"    defer",[55,2577,2578],{"class":69}," b.mu.",[55,2580,2581],{"class":65},"Unlock",[55,2583,1209],{"class":69},[55,2585,2586,2589,2591,2593],{"class":57,"line":194},[55,2587,2588],{"class":69},"    b.subscribers[t] ",[55,2590,430],{"class":61},[55,2592,921],{"class":65},[55,2594,2595],{"class":69},"(b.subscribers[t], ch)\n",[55,2597,2598],{"class":57,"line":212},[55,2599,254],{"class":69},[55,2601,2602],{"class":57,"line":218},[55,2603,356],{"emptyLinePlaceholder":355},[55,2605,2606,2608,2610,2612,2614,2616,2618,2621,2623,2626,2628],{"class":57,"line":224},[55,2607,62],{"class":61},[55,2609,874],{"class":69},[55,2611,2530],{"class":73},[55,2613,95],{"class":61},[55,2615,2535],{"class":65},[55,2617,403],{"class":69},[55,2619,2620],{"class":65},"Publish",[55,2622,70],{"class":69},[55,2624,2625],{"class":73},"e",[55,2627,2558],{"class":65},[55,2629,106],{"class":69},[55,2631,2632,2634,2637],{"class":57,"line":230},[55,2633,2565],{"class":69},[55,2635,2636],{"class":65},"RLock",[55,2638,1209],{"class":69},[55,2640,2641,2644,2646,2649,2652],{"class":57,"line":242},[55,2642,2643],{"class":69},"    subs ",[55,2645,599],{"class":61},[55,2647,2648],{"class":69}," b.subscribers[e.",[55,2650,2651],{"class":65},"Type",[55,2653,2654],{"class":69},"()]\n",[55,2656,2657,2659,2662],{"class":57,"line":251},[55,2658,2565],{"class":69},[55,2660,2661],{"class":65},"RUnlock",[55,2663,1209],{"class":69},[55,2665,2666,2668,2671,2673,2675],{"class":57,"line":471},[55,2667,652],{"class":61},[55,2669,2670],{"class":69}," _, ch ",[55,2672,599],{"class":61},[55,2674,660],{"class":61},[55,2676,2677],{"class":69}," subs {\n",[55,2679,2680,2683],{"class":57,"line":494},[55,2681,2682],{"class":61},"        select",[55,2684,125],{"class":69},[55,2686,2687,2690,2693,2696],{"class":57,"line":499},[55,2688,2689],{"class":61},"        case",[55,2691,2692],{"class":69}," ch ",[55,2694,2695],{"class":61},"\u003C-",[55,2697,2698],{"class":69}," e:\n",[55,2700,2701,2704,2707],{"class":57,"line":504},[55,2702,2703],{"class":61},"        default",[55,2705,2706],{"class":69},": ",[55,2708,2709],{"class":628},"\u002F\u002F не блокируемся, если подписчик медленный\n",[55,2711,2712],{"class":57,"line":526},[55,2713,2714],{"class":69},"        }\n",[55,2716,2717],{"class":57,"line":549},[55,2718,144],{"class":69},[55,2720,2721],{"class":57,"line":554},[55,2722,254],{"class":69},[16,2724,2725],{},"Domain Events между агрегатами — тот же Observer.",[35,2727,2729],{"id":2728},"command-объект-команда","Command — объект-команда",[16,2731,2732],{},"Use Case описывается командой (DTO с входными данными) + хендлером.",[47,2734,2736],{"className":49,"code":2735,"language":51,"meta":5,"style":5},"type PlaceOrderCommand struct {\n    OrderID    OrderID\n    CustomerID CustomerID\n    Items      []OrderItem\n}\n\ntype PlaceOrderHandler struct {\n    orders OrderRepository\n    events EventPublisher\n}\n\nfunc (h *PlaceOrderHandler) Handle(ctx context.Context, cmd PlaceOrderCommand) error {\n    order, err := NewOrder(cmd.CustomerID, cmd.Items)\n    if err != nil {\n        return err\n    }\n    if err := order.Place(); err != nil {\n        return err\n    }\n    if err := h.orders.Save(ctx, order); err != nil {\n        return err\n    }\n    return h.events.Publish(ctx, order.PullEvents()...)\n}\n",[40,2737,2738,2749,2757,2765,2773,2777,2781,2792,2800,2808,2812,2816,2856,2868,2881,2888,2892,2915,2921,2925,2948,2954,2958,2980],{"__ignoreMap":5},[55,2739,2740,2742,2745,2747],{"class":57,"line":58},[55,2741,285],{"class":61},[55,2743,2744],{"class":65}," PlaceOrderCommand",[55,2746,291],{"class":61},[55,2748,125],{"class":69},[55,2750,2751,2754],{"class":57,"line":109},[55,2752,2753],{"class":69},"    OrderID    ",[55,2755,2756],{"class":65},"OrderID\n",[55,2758,2759,2762],{"class":57,"line":128},[55,2760,2761],{"class":69},"    CustomerID ",[55,2763,2764],{"class":65},"CustomerID\n",[55,2766,2767,2770],{"class":57,"line":141},[55,2768,2769],{"class":69},"    Items      []",[55,2771,2772],{"class":65},"OrderItem\n",[55,2774,2775],{"class":57,"line":147},[55,2776,254],{"class":69},[55,2778,2779],{"class":57,"line":165},[55,2780,356],{"emptyLinePlaceholder":355},[55,2782,2783,2785,2788,2790],{"class":57,"line":175},[55,2784,285],{"class":61},[55,2786,2787],{"class":65}," PlaceOrderHandler",[55,2789,291],{"class":61},[55,2791,125],{"class":69},[55,2793,2794,2797],{"class":57,"line":180},[55,2795,2796],{"class":69},"    orders ",[55,2798,2799],{"class":65},"OrderRepository\n",[55,2801,2802,2805],{"class":57,"line":194},[55,2803,2804],{"class":69},"    events ",[55,2806,2807],{"class":65},"EventPublisher\n",[55,2809,2810],{"class":57,"line":212},[55,2811,254],{"class":69},[55,2813,2814],{"class":57,"line":218},[55,2815,356],{"emptyLinePlaceholder":355},[55,2817,2818,2820,2822,2824,2826,2829,2831,2833,2835,2837,2839,2841,2843,2845,2848,2850,2852,2854],{"class":57,"line":224},[55,2819,62],{"class":61},[55,2821,874],{"class":69},[55,2823,1973],{"class":73},[55,2825,95],{"class":61},[55,2827,2828],{"class":65},"PlaceOrderHandler",[55,2830,403],{"class":69},[55,2832,1841],{"class":65},[55,2834,70],{"class":69},[55,2836,1423],{"class":73},[55,2838,1426],{"class":65},[55,2840,312],{"class":69},[55,2842,1431],{"class":65},[55,2844,80],{"class":69},[55,2846,2847],{"class":73},"cmd",[55,2849,2744],{"class":65},[55,2851,403],{"class":69},[55,2853,103],{"class":61},[55,2855,125],{"class":69},[55,2857,2858,2861,2863,2865],{"class":57,"line":230},[55,2859,2860],{"class":69},"    order, err ",[55,2862,599],{"class":61},[55,2864,66],{"class":65},[55,2866,2867],{"class":69},"(cmd.CustomerID, cmd.Items)\n",[55,2869,2870,2872,2874,2877,2879],{"class":57,"line":242},[55,2871,112],{"class":61},[55,2873,1608],{"class":69},[55,2875,2876],{"class":61},"!=",[55,2878,135],{"class":134},[55,2880,125],{"class":69},[55,2882,2883,2885],{"class":57,"line":251},[55,2884,131],{"class":61},[55,2886,2887],{"class":69}," err\n",[55,2889,2890],{"class":57,"line":471},[55,2891,144],{"class":69},[55,2893,2894,2896,2898,2900,2903,2906,2909,2911,2913],{"class":57,"line":494},[55,2895,112],{"class":61},[55,2897,1608],{"class":69},[55,2899,599],{"class":61},[55,2901,2902],{"class":69}," order.",[55,2904,2905],{"class":65},"Place",[55,2907,2908],{"class":69},"(); err ",[55,2910,2876],{"class":61},[55,2912,135],{"class":134},[55,2914,125],{"class":69},[55,2916,2917,2919],{"class":57,"line":499},[55,2918,131],{"class":61},[55,2920,2887],{"class":69},[55,2922,2923],{"class":57,"line":504},[55,2924,144],{"class":69},[55,2926,2927,2929,2931,2933,2936,2939,2942,2944,2946],{"class":57,"line":526},[55,2928,112],{"class":61},[55,2930,1608],{"class":69},[55,2932,599],{"class":61},[55,2934,2935],{"class":69}," h.orders.",[55,2937,2938],{"class":65},"Save",[55,2940,2941],{"class":69},"(ctx, order); err ",[55,2943,2876],{"class":61},[55,2945,135],{"class":134},[55,2947,125],{"class":69},[55,2949,2950,2952],{"class":57,"line":549},[55,2951,131],{"class":61},[55,2953,2887],{"class":69},[55,2955,2956],{"class":57,"line":554},[55,2957,144],{"class":69},[55,2959,2960,2962,2965,2967,2970,2973,2976,2978],{"class":57,"line":559},[55,2961,183],{"class":61},[55,2963,2964],{"class":69}," h.events.",[55,2966,2620],{"class":65},[55,2968,2969],{"class":69},"(ctx, order.",[55,2971,2972],{"class":65},"PullEvents",[55,2974,2975],{"class":69},"()",[55,2977,939],{"class":61},[55,2979,376],{"class":69},[55,2981,2982],{"class":57,"line":593},[55,2983,254],{"class":69},[16,2985,2986,2987,2990],{},"Плюсы: единый интерфейс ",[40,2988,2989],{},"Handle(ctx, cmd)",", легко обвешать middleware (логи, метрики, transaction), естественная сериализация для очередей, симметрия с Query (CQRS).",[28,2992],{},[11,2994,2996],{"id":2995},"persistence","Persistence",[35,2998,3000],{"id":2999},"repository","Repository",[16,3002,3003],{},"Коллекция доменных объектов. Прячет инфраструктуру.",[47,3005,3007],{"className":49,"code":3006,"language":51,"meta":5,"style":5},"type OrderRepository interface {\n    Get(ctx context.Context, id OrderID) (*Order, error)\n    Save(ctx context.Context, order *Order) error\n    FindByCustomer(ctx context.Context, id CustomerID) ([]*Order, error)\n}\n\ntype pgOrderRepository struct{ db *sql.DB }\n",[40,3008,3009,3020,3054,3082,3115,3119,3123],{"__ignoreMap":5},[55,3010,3011,3013,3016,3018],{"class":57,"line":58},[55,3012,285],{"class":61},[55,3014,3015],{"class":65}," OrderRepository",[55,3017,1411],{"class":61},[55,3019,125],{"class":69},[55,3021,3022,3024,3026,3028,3030,3032,3034,3036,3039,3042,3044,3046,3048,3050,3052],{"class":57,"line":109},[55,3023,1418],{"class":65},[55,3025,70],{"class":69},[55,3027,1423],{"class":73},[55,3029,1426],{"class":65},[55,3031,312],{"class":69},[55,3033,1431],{"class":65},[55,3035,80],{"class":69},[55,3037,3038],{"class":73},"id",[55,3040,3041],{"class":65}," OrderID",[55,3043,92],{"class":69},[55,3045,95],{"class":61},[55,3047,98],{"class":65},[55,3049,80],{"class":69},[55,3051,103],{"class":61},[55,3053,376],{"class":69},[55,3055,3056,3059,3061,3063,3065,3067,3069,3071,3074,3076,3078,3080],{"class":57,"line":128},[55,3057,3058],{"class":65},"    Save",[55,3060,70],{"class":69},[55,3062,1423],{"class":73},[55,3064,1426],{"class":65},[55,3066,312],{"class":69},[55,3068,1431],{"class":65},[55,3070,80],{"class":69},[55,3072,3073],{"class":73},"order",[55,3075,422],{"class":61},[55,3077,98],{"class":65},[55,3079,403],{"class":69},[55,3081,1496],{"class":61},[55,3083,3084,3087,3089,3091,3093,3095,3097,3099,3101,3103,3105,3107,3109,3111,3113],{"class":57,"line":141},[55,3085,3086],{"class":65},"    FindByCustomer",[55,3088,70],{"class":69},[55,3090,1423],{"class":73},[55,3092,1426],{"class":65},[55,3094,312],{"class":69},[55,3096,1431],{"class":65},[55,3098,80],{"class":69},[55,3100,3038],{"class":73},[55,3102,77],{"class":65},[55,3104,1441],{"class":69},[55,3106,95],{"class":61},[55,3108,98],{"class":65},[55,3110,80],{"class":69},[55,3112,103],{"class":61},[55,3114,376],{"class":69},[55,3116,3117],{"class":57,"line":147},[55,3118,254],{"class":69},[55,3120,3121],{"class":57,"line":165},[55,3122,356],{"emptyLinePlaceholder":355},[55,3124,3125,3127,3130,3132,3135,3137,3140,3142,3145],{"class":57,"line":175},[55,3126,285],{"class":61},[55,3128,3129],{"class":65}," pgOrderRepository",[55,3131,291],{"class":61},[55,3133,3134],{"class":69},"{ db ",[55,3136,95],{"class":61},[55,3138,3139],{"class":65},"sql",[55,3141,312],{"class":69},[55,3143,3144],{"class":65},"DB",[55,3146,1532],{"class":69},[16,3148,3149],{},"В DDD — один репозиторий на агрегат. Подробнее в теме про агрегаты.",[16,3151,260,3152,3154,3155,3158],{},[23,3153,263],{}," нужно: если у вас тонкая обёртка ",[40,3156,3157],{},"Repository.Save = db.Insert"," без агрегата — это лишний слой. Используйте напрямую sqlx\u002Fsqlc, не плодите интерфейсы ради паттерна.",[35,3160,3162],{"id":3161},"unit-of-work-транзакция-на-несколько-операций","Unit of Work — транзакция на несколько операций",[16,3164,3165],{},"Проблема: Use Case изменяет несколько объектов (агрегатов или внутри одного агрегата), всё должно сохраниться атомарно.",[47,3167,3169],{"className":49,"code":3168,"language":51,"meta":5,"style":5},"type UnitOfWork interface {\n    WithTx(ctx context.Context, fn func(ctx context.Context) error) error\n}\n\ntype pgUnitOfWork struct{ db *sql.DB }\n\nfunc (u *pgUnitOfWork) WithTx(ctx context.Context, fn func(ctx context.Context) error) error {\n    tx, err := u.db.BeginTx(ctx, nil)\n    if err != nil {\n        return err\n    }\n    ctx = context.WithValue(ctx, txKey{}, tx)\n\n    if err := fn(ctx); err != nil {\n        _ = tx.Rollback()\n        return err\n    }\n    return tx.Commit()\n}\n\n\u002F\u002F Репозитории читают tx из контекста\nfunc (r *pgOrderRepo) Save(ctx context.Context, o *Order) error {\n    db := dbOrTx(ctx, r.db) \u002F\u002F если есть tx — используем его, иначе db\n    \u002F\u002F ...\n}\n",[40,3170,3171,3182,3222,3226,3230,3251,3255,3310,3331,3343,3349,3353,3374,3378,3398,3413,3419,3423,3434,3438,3442,3447,3489,3505,3510],{"__ignoreMap":5},[55,3172,3173,3175,3178,3180],{"class":57,"line":58},[55,3174,285],{"class":61},[55,3176,3177],{"class":65}," UnitOfWork",[55,3179,1411],{"class":61},[55,3181,125],{"class":69},[55,3183,3184,3187,3189,3191,3193,3195,3197,3199,3202,3204,3206,3208,3210,3212,3214,3216,3218,3220],{"class":57,"line":109},[55,3185,3186],{"class":65},"    WithTx",[55,3188,70],{"class":69},[55,3190,1423],{"class":73},[55,3192,1426],{"class":65},[55,3194,312],{"class":69},[55,3196,1431],{"class":65},[55,3198,80],{"class":69},[55,3200,3201],{"class":73},"fn",[55,3203,366],{"class":61},[55,3205,70],{"class":69},[55,3207,1423],{"class":73},[55,3209,1426],{"class":65},[55,3211,312],{"class":69},[55,3213,1431],{"class":65},[55,3215,403],{"class":69},[55,3217,103],{"class":61},[55,3219,403],{"class":69},[55,3221,1496],{"class":61},[55,3223,3224],{"class":57,"line":128},[55,3225,254],{"class":69},[55,3227,3228],{"class":57,"line":141},[55,3229,356],{"emptyLinePlaceholder":355},[55,3231,3232,3234,3237,3239,3241,3243,3245,3247,3249],{"class":57,"line":147},[55,3233,285],{"class":61},[55,3235,3236],{"class":65}," pgUnitOfWork",[55,3238,291],{"class":61},[55,3240,3134],{"class":69},[55,3242,95],{"class":61},[55,3244,3139],{"class":65},[55,3246,312],{"class":69},[55,3248,3144],{"class":65},[55,3250,1532],{"class":69},[55,3252,3253],{"class":57,"line":165},[55,3254,356],{"emptyLinePlaceholder":355},[55,3256,3257,3259,3261,3264,3266,3269,3271,3274,3276,3278,3280,3282,3284,3286,3288,3290,3292,3294,3296,3298,3300,3302,3304,3306,3308],{"class":57,"line":175},[55,3258,62],{"class":61},[55,3260,874],{"class":69},[55,3262,3263],{"class":73},"u ",[55,3265,95],{"class":61},[55,3267,3268],{"class":65},"pgUnitOfWork",[55,3270,403],{"class":69},[55,3272,3273],{"class":65},"WithTx",[55,3275,70],{"class":69},[55,3277,1423],{"class":73},[55,3279,1426],{"class":65},[55,3281,312],{"class":69},[55,3283,1431],{"class":65},[55,3285,80],{"class":69},[55,3287,3201],{"class":73},[55,3289,366],{"class":61},[55,3291,70],{"class":69},[55,3293,1423],{"class":73},[55,3295,1426],{"class":65},[55,3297,312],{"class":69},[55,3299,1431],{"class":65},[55,3301,403],{"class":69},[55,3303,103],{"class":61},[55,3305,403],{"class":69},[55,3307,103],{"class":61},[55,3309,125],{"class":69},[55,3311,3312,3315,3317,3320,3323,3326,3329],{"class":57,"line":180},[55,3313,3314],{"class":69},"    tx, err ",[55,3316,599],{"class":61},[55,3318,3319],{"class":69}," u.db.",[55,3321,3322],{"class":65},"BeginTx",[55,3324,3325],{"class":69},"(ctx, ",[55,3327,3328],{"class":134},"nil",[55,3330,376],{"class":69},[55,3332,3333,3335,3337,3339,3341],{"class":57,"line":194},[55,3334,112],{"class":61},[55,3336,1608],{"class":69},[55,3338,2876],{"class":61},[55,3340,135],{"class":134},[55,3342,125],{"class":69},[55,3344,3345,3347],{"class":57,"line":212},[55,3346,131],{"class":61},[55,3348,2887],{"class":69},[55,3350,3351],{"class":57,"line":218},[55,3352,144],{"class":69},[55,3354,3355,3358,3360,3363,3366,3368,3371],{"class":57,"line":224},[55,3356,3357],{"class":69},"    ctx ",[55,3359,430],{"class":61},[55,3361,3362],{"class":69}," context.",[55,3364,3365],{"class":65},"WithValue",[55,3367,3325],{"class":69},[55,3369,3370],{"class":65},"txKey",[55,3372,3373],{"class":69},"{}, tx)\n",[55,3375,3376],{"class":57,"line":230},[55,3377,356],{"emptyLinePlaceholder":355},[55,3379,3380,3382,3384,3386,3389,3392,3394,3396],{"class":57,"line":242},[55,3381,112],{"class":61},[55,3383,1608],{"class":69},[55,3385,599],{"class":61},[55,3387,3388],{"class":65}," fn",[55,3390,3391],{"class":69},"(ctx); err ",[55,3393,2876],{"class":61},[55,3395,135],{"class":134},[55,3397,125],{"class":69},[55,3399,3400,3403,3405,3408,3411],{"class":57,"line":251},[55,3401,3402],{"class":69},"        _ ",[55,3404,430],{"class":61},[55,3406,3407],{"class":69}," tx.",[55,3409,3410],{"class":65},"Rollback",[55,3412,1209],{"class":69},[55,3414,3415,3417],{"class":57,"line":471},[55,3416,131],{"class":61},[55,3418,2887],{"class":69},[55,3420,3421],{"class":57,"line":494},[55,3422,144],{"class":69},[55,3424,3425,3427,3429,3432],{"class":57,"line":499},[55,3426,183],{"class":61},[55,3428,3407],{"class":69},[55,3430,3431],{"class":65},"Commit",[55,3433,1209],{"class":69},[55,3435,3436],{"class":57,"line":504},[55,3437,254],{"class":69},[55,3439,3440],{"class":57,"line":526},[55,3441,356],{"emptyLinePlaceholder":355},[55,3443,3444],{"class":57,"line":549},[55,3445,3446],{"class":628},"\u002F\u002F Репозитории читают tx из контекста\n",[55,3448,3449,3451,3453,3455,3457,3460,3462,3464,3466,3468,3470,3472,3474,3476,3479,3481,3483,3485,3487],{"class":57,"line":554},[55,3450,62],{"class":61},[55,3452,874],{"class":69},[55,3454,1545],{"class":73},[55,3456,95],{"class":61},[55,3458,3459],{"class":65},"pgOrderRepo",[55,3461,403],{"class":69},[55,3463,2938],{"class":65},[55,3465,70],{"class":69},[55,3467,1423],{"class":73},[55,3469,1426],{"class":65},[55,3471,312],{"class":69},[55,3473,1431],{"class":65},[55,3475,80],{"class":69},[55,3477,3478],{"class":73},"o",[55,3480,422],{"class":61},[55,3482,98],{"class":65},[55,3484,403],{"class":69},[55,3486,103],{"class":61},[55,3488,125],{"class":69},[55,3490,3491,3494,3496,3499,3502],{"class":57,"line":559},[55,3492,3493],{"class":69},"    db ",[55,3495,599],{"class":61},[55,3497,3498],{"class":65}," dbOrTx",[55,3500,3501],{"class":69},"(ctx, r.db) ",[55,3503,3504],{"class":628},"\u002F\u002F если есть tx — используем его, иначе db\n",[55,3506,3507],{"class":57,"line":593},[55,3508,3509],{"class":628},"    \u002F\u002F ...\n",[55,3511,3512],{"class":57,"line":608},[55,3513,254],{"class":69},[16,3515,3516],{},"Use Case:",[47,3518,3520],{"className":49,"code":3519,"language":51,"meta":5,"style":5},"uow.WithTx(ctx, func(ctx context.Context) error {\n    if err := orderRepo.Save(ctx, order); err != nil { return err }\n    if err := outboxRepo.Append(ctx, events); err != nil { return err }\n    return nil\n})\n",[40,3521,3522,3549,3576,3603,3610],{"__ignoreMap":5},[55,3523,3524,3527,3529,3531,3533,3535,3537,3539,3541,3543,3545,3547],{"class":57,"line":58},[55,3525,3526],{"class":69},"uow.",[55,3528,3273],{"class":65},[55,3530,3325],{"class":69},[55,3532,62],{"class":61},[55,3534,70],{"class":69},[55,3536,1423],{"class":73},[55,3538,1426],{"class":65},[55,3540,312],{"class":69},[55,3542,1431],{"class":65},[55,3544,403],{"class":69},[55,3546,103],{"class":61},[55,3548,125],{"class":69},[55,3550,3551,3553,3555,3557,3560,3562,3564,3566,3568,3570,3573],{"class":57,"line":109},[55,3552,112],{"class":61},[55,3554,1608],{"class":69},[55,3556,599],{"class":61},[55,3558,3559],{"class":69}," orderRepo.",[55,3561,2938],{"class":65},[55,3563,2941],{"class":69},[55,3565,2876],{"class":61},[55,3567,135],{"class":134},[55,3569,2262],{"class":69},[55,3571,3572],{"class":61},"return",[55,3574,3575],{"class":69}," err }\n",[55,3577,3578,3580,3582,3584,3587,3590,3593,3595,3597,3599,3601],{"class":57,"line":128},[55,3579,112],{"class":61},[55,3581,1608],{"class":69},[55,3583,599],{"class":61},[55,3585,3586],{"class":69}," outboxRepo.",[55,3588,3589],{"class":65},"Append",[55,3591,3592],{"class":69},"(ctx, events); err ",[55,3594,2876],{"class":61},[55,3596,135],{"class":134},[55,3598,2262],{"class":69},[55,3600,3572],{"class":61},[55,3602,3575],{"class":69},[55,3604,3605,3607],{"class":57,"line":141},[55,3606,183],{"class":61},[55,3608,3609],{"class":134}," nil\n",[55,3611,3612],{"class":57,"line":147},[55,3613,3614],{"class":69},"})\n",[16,3616,3617,3618,3621,3622,3625],{},"Альтернатива в Go — явно передавать ",[40,3619,3620],{},"*sql.Tx"," или использовать паттерн ",[40,3623,3624],{},"Tx Manager"," (как pgx + сервис). Главное: транзакция управляется в одном месте, репозитории её не открывают.",[28,3627],{},[11,3629,3631],{"id":3630},"concurrency-patterns-в-go","Concurrency Patterns в Go",[35,3633,3635],{"id":3634},"generator-функция-возвращает-канал","Generator — функция возвращает канал",[47,3637,3639],{"className":49,"code":3638,"language":51,"meta":5,"style":5},"func Range(start, end int) \u003C-chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for i := start; i \u003C end; i++ {\n            out \u003C- i\n        }\n    }()\n    return out\n}\n\nfor v := range Range(1, 100) {\n    fmt.Println(v)\n}\n",[40,3640,3641,3669,3688,3697,3708,3732,3742,3746,3751,3758,3762,3766,3792,3802],{"__ignoreMap":5},[55,3642,3643,3645,3648,3650,3653,3655,3658,3660,3662,3665,3667],{"class":57,"line":58},[55,3644,62],{"class":61},[55,3646,3647],{"class":65}," Range",[55,3649,70],{"class":69},[55,3651,3652],{"class":73},"start",[55,3654,80],{"class":69},[55,3656,3657],{"class":73},"end",[55,3659,981],{"class":61},[55,3661,403],{"class":69},[55,3663,3664],{"class":61},"\u003C-chan",[55,3666,981],{"class":61},[55,3668,125],{"class":69},[55,3670,3671,3674,3676,3679,3681,3684,3686],{"class":57,"line":109},[55,3672,3673],{"class":69},"    out ",[55,3675,599],{"class":61},[55,3677,3678],{"class":65}," make",[55,3680,70],{"class":69},[55,3682,3683],{"class":61},"chan",[55,3685,981],{"class":61},[55,3687,376],{"class":69},[55,3689,3690,3693,3695],{"class":57,"line":128},[55,3691,3692],{"class":61},"    go",[55,3694,366],{"class":61},[55,3696,1304],{"class":69},[55,3698,3699,3702,3705],{"class":57,"line":141},[55,3700,3701],{"class":61},"        defer",[55,3703,3704],{"class":65}," close",[55,3706,3707],{"class":69},"(out)\n",[55,3709,3710,3713,3716,3718,3721,3724,3727,3730],{"class":57,"line":147},[55,3711,3712],{"class":61},"        for",[55,3714,3715],{"class":69}," i ",[55,3717,599],{"class":61},[55,3719,3720],{"class":69}," start; i ",[55,3722,3723],{"class":61},"\u003C",[55,3725,3726],{"class":69}," end; i",[55,3728,3729],{"class":61},"++",[55,3731,125],{"class":69},[55,3733,3734,3737,3739],{"class":57,"line":165},[55,3735,3736],{"class":69},"            out ",[55,3738,2695],{"class":61},[55,3740,3741],{"class":69}," i\n",[55,3743,3744],{"class":57,"line":175},[55,3745,2714],{"class":69},[55,3747,3748],{"class":57,"line":180},[55,3749,3750],{"class":69},"    }()\n",[55,3752,3753,3755],{"class":57,"line":194},[55,3754,183],{"class":61},[55,3756,3757],{"class":69}," out\n",[55,3759,3760],{"class":57,"line":212},[55,3761,254],{"class":69},[55,3763,3764],{"class":57,"line":218},[55,3765,356],{"emptyLinePlaceholder":355},[55,3767,3768,3771,3774,3776,3778,3780,3782,3785,3787,3790],{"class":57,"line":224},[55,3769,3770],{"class":61},"for",[55,3772,3773],{"class":69}," v ",[55,3775,599],{"class":61},[55,3777,660],{"class":61},[55,3779,3647],{"class":65},[55,3781,70],{"class":69},[55,3783,3784],{"class":134},"1",[55,3786,80],{"class":69},[55,3788,3789],{"class":134},"100",[55,3791,106],{"class":69},[55,3793,3794,3797,3799],{"class":57,"line":230},[55,3795,3796],{"class":69},"    fmt.",[55,3798,2086],{"class":65},[55,3800,3801],{"class":69},"(v)\n",[55,3803,3804],{"class":57,"line":242},[55,3805,254],{"class":69},[16,3807,3808],{},"Когда применять: ленивая генерация, источник данных без блокировки потребителя.",[35,3810,3812],{"id":3811},"pipeline-stage-based-обработка","Pipeline — stage-based обработка",[47,3814,3816],{"className":49,"code":3815,"language":51,"meta":5,"style":5},"func gen(nums ...int) \u003C-chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for _, n := range nums {\n            out \u003C- n\n        }\n    }()\n    return out\n}\n\nfunc square(in \u003C-chan int) \u003C-chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for n := range in {\n            out \u003C- n * n\n        }\n    }()\n    return out\n}\n\nfunc sum(in \u003C-chan int) int {\n    total := 0\n    for n := range in {\n        total += n\n    }\n    return total\n}\n\nresult := sum(square(gen(1, 2, 3, 4, 5)))\n",[40,3817,3818,3841,3857,3865,3873,3887,3895,3899,3903,3909,3913,3917,3942,3958,3966,3974,3988,4000,4004,4008,4014,4018,4022,4044,4054,4066,4075,4079,4086,4090,4094],{"__ignoreMap":5},[55,3819,3820,3822,3825,3827,3830,3833,3835,3837,3839],{"class":57,"line":58},[55,3821,62],{"class":61},[55,3823,3824],{"class":65}," gen",[55,3826,70],{"class":69},[55,3828,3829],{"class":73},"nums",[55,3831,3832],{"class":61}," ...int",[55,3834,403],{"class":69},[55,3836,3664],{"class":61},[55,3838,981],{"class":61},[55,3840,125],{"class":69},[55,3842,3843,3845,3847,3849,3851,3853,3855],{"class":57,"line":109},[55,3844,3673],{"class":69},[55,3846,599],{"class":61},[55,3848,3678],{"class":65},[55,3850,70],{"class":69},[55,3852,3683],{"class":61},[55,3854,981],{"class":61},[55,3856,376],{"class":69},[55,3858,3859,3861,3863],{"class":57,"line":128},[55,3860,3692],{"class":61},[55,3862,366],{"class":61},[55,3864,1304],{"class":69},[55,3866,3867,3869,3871],{"class":57,"line":141},[55,3868,3701],{"class":61},[55,3870,3704],{"class":65},[55,3872,3707],{"class":69},[55,3874,3875,3877,3880,3882,3884],{"class":57,"line":147},[55,3876,3712],{"class":61},[55,3878,3879],{"class":69}," _, n ",[55,3881,599],{"class":61},[55,3883,660],{"class":61},[55,3885,3886],{"class":69}," nums {\n",[55,3888,3889,3891,3893],{"class":57,"line":165},[55,3890,3736],{"class":69},[55,3892,2695],{"class":61},[55,3894,999],{"class":69},[55,3896,3897],{"class":57,"line":175},[55,3898,2714],{"class":69},[55,3900,3901],{"class":57,"line":180},[55,3902,3750],{"class":69},[55,3904,3905,3907],{"class":57,"line":194},[55,3906,183],{"class":61},[55,3908,3757],{"class":69},[55,3910,3911],{"class":57,"line":212},[55,3912,254],{"class":69},[55,3914,3915],{"class":57,"line":218},[55,3916,356],{"emptyLinePlaceholder":355},[55,3918,3919,3921,3924,3926,3929,3932,3934,3936,3938,3940],{"class":57,"line":224},[55,3920,62],{"class":61},[55,3922,3923],{"class":65}," square",[55,3925,70],{"class":69},[55,3927,3928],{"class":73},"in",[55,3930,3931],{"class":61}," \u003C-chan",[55,3933,981],{"class":61},[55,3935,403],{"class":69},[55,3937,3664],{"class":61},[55,3939,981],{"class":61},[55,3941,125],{"class":69},[55,3943,3944,3946,3948,3950,3952,3954,3956],{"class":57,"line":230},[55,3945,3673],{"class":69},[55,3947,599],{"class":61},[55,3949,3678],{"class":65},[55,3951,70],{"class":69},[55,3953,3683],{"class":61},[55,3955,981],{"class":61},[55,3957,376],{"class":69},[55,3959,3960,3962,3964],{"class":57,"line":242},[55,3961,3692],{"class":61},[55,3963,366],{"class":61},[55,3965,1304],{"class":69},[55,3967,3968,3970,3972],{"class":57,"line":251},[55,3969,3701],{"class":61},[55,3971,3704],{"class":65},[55,3973,3707],{"class":69},[55,3975,3976,3978,3981,3983,3985],{"class":57,"line":471},[55,3977,3712],{"class":61},[55,3979,3980],{"class":69}," n ",[55,3982,599],{"class":61},[55,3984,660],{"class":61},[55,3986,3987],{"class":69}," in {\n",[55,3989,3990,3992,3994,3996,3998],{"class":57,"line":494},[55,3991,3736],{"class":69},[55,3993,2695],{"class":61},[55,3995,3980],{"class":69},[55,3997,95],{"class":61},[55,3999,999],{"class":69},[55,4001,4002],{"class":57,"line":499},[55,4003,2714],{"class":69},[55,4005,4006],{"class":57,"line":504},[55,4007,3750],{"class":69},[55,4009,4010,4012],{"class":57,"line":526},[55,4011,183],{"class":61},[55,4013,3757],{"class":69},[55,4015,4016],{"class":57,"line":549},[55,4017,254],{"class":69},[55,4019,4020],{"class":57,"line":554},[55,4021,356],{"emptyLinePlaceholder":355},[55,4023,4024,4026,4029,4031,4033,4035,4037,4039,4042],{"class":57,"line":559},[55,4025,62],{"class":61},[55,4027,4028],{"class":65}," sum",[55,4030,70],{"class":69},[55,4032,3928],{"class":73},[55,4034,3931],{"class":61},[55,4036,981],{"class":61},[55,4038,403],{"class":69},[55,4040,4041],{"class":61},"int",[55,4043,125],{"class":69},[55,4045,4046,4049,4051],{"class":57,"line":593},[55,4047,4048],{"class":69},"    total ",[55,4050,599],{"class":61},[55,4052,4053],{"class":134}," 0\n",[55,4055,4056,4058,4060,4062,4064],{"class":57,"line":608},[55,4057,652],{"class":61},[55,4059,3980],{"class":69},[55,4061,599],{"class":61},[55,4063,660],{"class":61},[55,4065,3987],{"class":69},[55,4067,4068,4071,4073],{"class":57,"line":614},[55,4069,4070],{"class":69},"        total ",[55,4072,1083],{"class":61},[55,4074,999],{"class":69},[55,4076,4077],{"class":57,"line":632},[55,4078,144],{"class":69},[55,4080,4081,4083],{"class":57,"line":644},[55,4082,183],{"class":61},[55,4084,4085],{"class":69}," total\n",[55,4087,4088],{"class":57,"line":649},[55,4089,254],{"class":69},[55,4091,4092],{"class":57,"line":666},[55,4093,356],{"emptyLinePlaceholder":355},[55,4095,4096,4099,4101,4103,4105,4108,4110,4113,4115,4117,4119,4122,4124,4127,4129,4132,4134,4136],{"class":57,"line":675},[55,4097,4098],{"class":69},"result ",[55,4100,599],{"class":61},[55,4102,4028],{"class":65},[55,4104,70],{"class":69},[55,4106,4107],{"class":65},"square",[55,4109,70],{"class":69},[55,4111,4112],{"class":65},"gen",[55,4114,70],{"class":69},[55,4116,3784],{"class":134},[55,4118,80],{"class":69},[55,4120,4121],{"class":134},"2",[55,4123,80],{"class":69},[55,4125,4126],{"class":134},"3",[55,4128,80],{"class":69},[55,4130,4131],{"class":134},"4",[55,4133,80],{"class":69},[55,4135,724],{"class":134},[55,4137,4138],{"class":69},")))\n",[16,4140,4141,4142,312],{},"Каждая стадия — горутина с in\u002Fout каналами. Закрытие upstream → автоматическое закрытие downstream через ",[40,4143,4144],{},"range",[16,4146,4147],{},"Подводные камни:",[1353,4149,4150,4160],{},[1356,4151,4152,4153,4156,4157,312],{},"Утечка горутин при раннем выходе. Решение: ",[40,4154,4155],{},"context.Context"," + ",[40,4158,4159],{},"select { case ... \u003C- ctx.Done() }",[1356,4161,4162],{},"Backpressure через буфер каналов или unbuffered (синхронная передача).",[16,4164,4165],{},"Подробнее — в теме «Паттерны конкурентности».",[35,4167,4169],{"id":4168},"worker-pool-ограничение-concurrency","Worker Pool — ограничение concurrency",[16,4171,4172],{},"Проблема: 10 000 задач, нельзя запустить 10 000 горутин одновременно (память, открытые соединения).",[47,4174,4176],{"className":49,"code":4175,"language":51,"meta":5,"style":5},"func WorkerPool(ctx context.Context, jobs \u003C-chan Job, workers int) \u003C-chan Result {\n    results := make(chan Result)\n    var wg sync.WaitGroup\n\n    for i := 0; i \u003C workers; i++ {\n        wg.Add(1)\n        go func() {\n            defer wg.Done()\n            for job := range jobs {\n                select {\n                case \u003C-ctx.Done():\n                    return\n                case results \u003C- process(job):\n                }\n            }\n        }()\n    }\n\n    go func() {\n        wg.Wait()\n        close(results)\n    }()\n\n    return results\n}\n",[40,4177,4178,4221,4238,4253,4257,4279,4293,4302,4315,4330,4337,4353,4358,4373,4378,4383,4388,4392,4396,4404,4413,4421,4425,4429,4436],{"__ignoreMap":5},[55,4179,4180,4182,4185,4187,4189,4191,4193,4195,4197,4200,4202,4205,4207,4210,4212,4214,4216,4219],{"class":57,"line":58},[55,4181,62],{"class":61},[55,4183,4184],{"class":65}," WorkerPool",[55,4186,70],{"class":69},[55,4188,1423],{"class":73},[55,4190,1426],{"class":65},[55,4192,312],{"class":69},[55,4194,1431],{"class":65},[55,4196,80],{"class":69},[55,4198,4199],{"class":73},"jobs",[55,4201,3931],{"class":61},[55,4203,4204],{"class":65}," Job",[55,4206,80],{"class":69},[55,4208,4209],{"class":73},"workers",[55,4211,981],{"class":61},[55,4213,403],{"class":69},[55,4215,3664],{"class":61},[55,4217,4218],{"class":65}," Result",[55,4220,125],{"class":69},[55,4222,4223,4226,4228,4230,4232,4234,4236],{"class":57,"line":109},[55,4224,4225],{"class":69},"    results ",[55,4227,599],{"class":61},[55,4229,3678],{"class":65},[55,4231,70],{"class":69},[55,4233,3683],{"class":61},[55,4235,4218],{"class":65},[55,4237,376],{"class":69},[55,4239,4240,4243,4246,4248,4250],{"class":57,"line":128},[55,4241,4242],{"class":61},"    var",[55,4244,4245],{"class":69}," wg ",[55,4247,1255],{"class":65},[55,4249,312],{"class":69},[55,4251,4252],{"class":65},"WaitGroup\n",[55,4254,4255],{"class":57,"line":141},[55,4256,356],{"emptyLinePlaceholder":355},[55,4258,4259,4261,4263,4265,4267,4270,4272,4275,4277],{"class":57,"line":147},[55,4260,652],{"class":61},[55,4262,3715],{"class":69},[55,4264,599],{"class":61},[55,4266,160],{"class":134},[55,4268,4269],{"class":69},"; i ",[55,4271,3723],{"class":61},[55,4273,4274],{"class":69}," workers; i",[55,4276,3729],{"class":61},[55,4278,125],{"class":69},[55,4280,4281,4284,4287,4289,4291],{"class":57,"line":165},[55,4282,4283],{"class":69},"        wg.",[55,4285,4286],{"class":65},"Add",[55,4288,70],{"class":69},[55,4290,3784],{"class":134},[55,4292,376],{"class":69},[55,4294,4295,4298,4300],{"class":57,"line":175},[55,4296,4297],{"class":61},"        go",[55,4299,366],{"class":61},[55,4301,1304],{"class":69},[55,4303,4304,4307,4310,4313],{"class":57,"line":180},[55,4305,4306],{"class":61},"            defer",[55,4308,4309],{"class":69}," wg.",[55,4311,4312],{"class":65},"Done",[55,4314,1209],{"class":69},[55,4316,4317,4320,4323,4325,4327],{"class":57,"line":194},[55,4318,4319],{"class":61},"            for",[55,4321,4322],{"class":69}," job ",[55,4324,599],{"class":61},[55,4326,660],{"class":61},[55,4328,4329],{"class":69}," jobs {\n",[55,4331,4332,4335],{"class":57,"line":212},[55,4333,4334],{"class":61},"                select",[55,4336,125],{"class":69},[55,4338,4339,4342,4345,4348,4350],{"class":57,"line":218},[55,4340,4341],{"class":61},"                case",[55,4343,4344],{"class":61}," \u003C-",[55,4346,4347],{"class":69},"ctx.",[55,4349,4312],{"class":65},[55,4351,4352],{"class":69},"():\n",[55,4354,4355],{"class":57,"line":224},[55,4356,4357],{"class":61},"                    return\n",[55,4359,4360,4362,4365,4367,4370],{"class":57,"line":230},[55,4361,4341],{"class":61},[55,4363,4364],{"class":69}," results ",[55,4366,2695],{"class":61},[55,4368,4369],{"class":65}," process",[55,4371,4372],{"class":69},"(job):\n",[55,4374,4375],{"class":57,"line":242},[55,4376,4377],{"class":69},"                }\n",[55,4379,4380],{"class":57,"line":251},[55,4381,4382],{"class":69},"            }\n",[55,4384,4385],{"class":57,"line":471},[55,4386,4387],{"class":69},"        }()\n",[55,4389,4390],{"class":57,"line":494},[55,4391,144],{"class":69},[55,4393,4394],{"class":57,"line":499},[55,4395,356],{"emptyLinePlaceholder":355},[55,4397,4398,4400,4402],{"class":57,"line":504},[55,4399,3692],{"class":61},[55,4401,366],{"class":61},[55,4403,1304],{"class":69},[55,4405,4406,4408,4411],{"class":57,"line":526},[55,4407,4283],{"class":69},[55,4409,4410],{"class":65},"Wait",[55,4412,1209],{"class":69},[55,4414,4415,4418],{"class":57,"line":549},[55,4416,4417],{"class":65},"        close",[55,4419,4420],{"class":69},"(results)\n",[55,4422,4423],{"class":57,"line":554},[55,4424,3750],{"class":69},[55,4426,4427],{"class":57,"line":559},[55,4428,356],{"emptyLinePlaceholder":355},[55,4430,4431,4433],{"class":57,"line":593},[55,4432,183],{"class":61},[55,4434,4435],{"class":69}," results\n",[55,4437,4438],{"class":57,"line":608},[55,4439,254],{"class":69},[16,4441,4442],{},"Когда применять: HTTP-клиенты к внешним API, обработка очередей задач, batch-операции.",[35,4444,4446],{"id":4445},"context-propagation","Context propagation",[16,4448,4449,4451],{},[40,4450,4155],{}," — Go-идиома для отмены, дедлайнов и передачи request-scoped значений по цепочке вызовов.",[47,4453,4455],{"className":49,"code":4454,"language":51,"meta":5,"style":5},"func (s *Service) FetchUser(ctx context.Context, id UserID) (*User, error) {\n    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)\n    defer cancel()\n\n    return s.userRepo.Get(ctx, id)\n}\n",[40,4456,4457,4505,4526,4535,4539,4551],{"__ignoreMap":5},[55,4458,4459,4461,4463,4465,4467,4470,4472,4475,4477,4479,4481,4483,4485,4487,4489,4492,4494,4496,4499,4501,4503],{"class":57,"line":58},[55,4460,62],{"class":61},[55,4462,874],{"class":69},[55,4464,2296],{"class":73},[55,4466,95],{"class":61},[55,4468,4469],{"class":65},"Service",[55,4471,403],{"class":69},[55,4473,4474],{"class":65},"FetchUser",[55,4476,70],{"class":69},[55,4478,1423],{"class":73},[55,4480,1426],{"class":65},[55,4482,312],{"class":69},[55,4484,1431],{"class":65},[55,4486,80],{"class":69},[55,4488,3038],{"class":73},[55,4490,4491],{"class":65}," UserID",[55,4493,92],{"class":69},[55,4495,95],{"class":61},[55,4497,4498],{"class":65},"User",[55,4500,80],{"class":69},[55,4502,103],{"class":61},[55,4504,106],{"class":69},[55,4506,4507,4510,4512,4514,4517,4519,4521,4523],{"class":57,"line":109},[55,4508,4509],{"class":69},"    ctx, cancel ",[55,4511,599],{"class":61},[55,4513,3362],{"class":69},[55,4515,4516],{"class":65},"WithTimeout",[55,4518,3325],{"class":69},[55,4520,4121],{"class":134},[55,4522,95],{"class":61},[55,4524,4525],{"class":69},"time.Second)\n",[55,4527,4528,4530,4533],{"class":57,"line":128},[55,4529,2575],{"class":61},[55,4531,4532],{"class":65}," cancel",[55,4534,1209],{"class":69},[55,4536,4537],{"class":57,"line":141},[55,4538,356],{"emptyLinePlaceholder":355},[55,4540,4541,4543,4546,4548],{"class":57,"line":147},[55,4542,183],{"class":61},[55,4544,4545],{"class":69}," s.userRepo.",[55,4547,1555],{"class":65},[55,4549,4550],{"class":69},"(ctx, id)\n",[55,4552,4553],{"class":57,"line":165},[55,4554,254],{"class":69},[16,4556,4557],{},"Правила:",[1353,4559,4560,4566,4572,4585],{},[1356,4561,4562,4563,312],{},"Первый параметр функции, всегда ",[40,4564,4565],{},"ctx context.Context",[1356,4567,4568,4569,1181],{},"Не хранить в структурах (только короткоживущие исключения, как ",[40,4570,4571],{},"http.Request",[1356,4573,4574,4575,4577,4578,4581,4582,312],{},"Не передавать ",[40,4576,3328],{}," — используйте ",[40,4579,4580],{},"context.TODO()"," или ",[40,4583,4584],{},"context.Background()",[1356,4586,4587,4588,4590],{},"В ",[40,4589,3365],{}," — только request-scoped данные (request ID, trace ID), не бизнес-параметры.",[35,4592,4594],{"id":4593},"circuit-breaker-кратко","Circuit Breaker — кратко",[16,4596,4597],{},"Защита от каскадных сбоев. Если внешний сервис стабильно падает — перестаём в него ходить, возвращаем ошибку сразу.",[47,4599,4604],{"className":4600,"code":4602,"language":4603,"meta":5},[4601],"language-text","Closed (норма) ── failures > threshold ──→ Open (отбой)\n                                            │\n                                       таймаут\n                                            ↓\n              Closed ←── success ── Half-Open (пробуем)\n                                            │\n                                       failure\n                                            ↓\n                                          Open\n","text",[40,4605,4602],{"__ignoreMap":5},[47,4607,4609],{"className":49,"code":4608,"language":51,"meta":5,"style":5},"import \"github.com\u002Fsony\u002Fgobreaker\"\n\ncb := gobreaker.NewCircuitBreaker(gobreaker.Settings{\n    Name:        \"external-api\",\n    MaxRequests: 3,\n    Timeout:     30 * time.Second,\n    ReadyToTrip: func(c gobreaker.Counts) bool {\n        return c.ConsecutiveFailures > 5\n    },\n})\n\nresult, err := cb.Execute(func() (interface{}, error) {\n    return http.Get(\"https:\u002F\u002Fflaky.example.com\u002Fapi\")\n})\n",[40,4610,4611,4625,4629,4654,4664,4673,4685,4712,4724,4729,4733,4737,4766,4781],{"__ignoreMap":5},[55,4612,4613,4616,4619,4622],{"class":57,"line":58},[55,4614,4615],{"class":61},"import",[55,4617,4618],{"class":121}," \"",[55,4620,4621],{"class":65},"github.com\u002Fsony\u002Fgobreaker",[55,4623,4624],{"class":121},"\"\n",[55,4626,4627],{"class":57,"line":109},[55,4628,356],{"emptyLinePlaceholder":355},[55,4630,4631,4634,4636,4639,4642,4644,4647,4649,4652],{"class":57,"line":128},[55,4632,4633],{"class":69},"cb ",[55,4635,599],{"class":61},[55,4637,4638],{"class":69}," gobreaker.",[55,4640,4641],{"class":65},"NewCircuitBreaker",[55,4643,70],{"class":69},[55,4645,4646],{"class":65},"gobreaker",[55,4648,312],{"class":69},[55,4650,4651],{"class":65},"Settings",[55,4653,191],{"class":69},[55,4655,4656,4659,4662],{"class":57,"line":141},[55,4657,4658],{"class":69},"    Name:        ",[55,4660,4661],{"class":121},"\"external-api\"",[55,4663,713],{"class":69},[55,4665,4666,4669,4671],{"class":57,"line":147},[55,4667,4668],{"class":69},"    MaxRequests: ",[55,4670,4126],{"class":134},[55,4672,713],{"class":69},[55,4674,4675,4678,4680,4682],{"class":57,"line":165},[55,4676,4677],{"class":69},"    Timeout:     ",[55,4679,620],{"class":134},[55,4681,422],{"class":61},[55,4683,4684],{"class":69}," time.Second,\n",[55,4686,4687,4690,4692,4694,4697,4700,4702,4705,4707,4710],{"class":57,"line":175},[55,4688,4689],{"class":69},"    ReadyToTrip: ",[55,4691,62],{"class":61},[55,4693,70],{"class":69},[55,4695,4696],{"class":73},"c",[55,4698,4699],{"class":65}," gobreaker",[55,4701,312],{"class":69},[55,4703,4704],{"class":65},"Counts",[55,4706,403],{"class":69},[55,4708,4709],{"class":61},"bool",[55,4711,125],{"class":69},[55,4713,4714,4716,4719,4721],{"class":57,"line":180},[55,4715,131],{"class":61},[55,4717,4718],{"class":69}," c.ConsecutiveFailures ",[55,4720,1071],{"class":61},[55,4722,4723],{"class":134}," 5\n",[55,4725,4726],{"class":57,"line":194},[55,4727,4728],{"class":69},"    },\n",[55,4730,4731],{"class":57,"line":212},[55,4732,3614],{"class":69},[55,4734,4735],{"class":57,"line":218},[55,4736,356],{"emptyLinePlaceholder":355},[55,4738,4739,4742,4744,4747,4750,4752,4754,4756,4759,4762,4764],{"class":57,"line":224},[55,4740,4741],{"class":69},"result, err ",[55,4743,599],{"class":61},[55,4745,4746],{"class":69}," cb.",[55,4748,4749],{"class":65},"Execute",[55,4751,70],{"class":69},[55,4753,62],{"class":61},[55,4755,1033],{"class":69},[55,4757,4758],{"class":61},"interface",[55,4760,4761],{"class":69},"{}, ",[55,4763,103],{"class":61},[55,4765,106],{"class":69},[55,4767,4768,4770,4772,4774,4776,4779],{"class":57,"line":230},[55,4769,183],{"class":61},[55,4771,2043],{"class":69},[55,4773,1555],{"class":65},[55,4775,70],{"class":69},[55,4777,4778],{"class":121},"\"https:\u002F\u002Fflaky.example.com\u002Fapi\"",[55,4780,376],{"class":69},[55,4782,4783],{"class":57,"line":242},[55,4784,3614],{"class":69},[16,4786,4787],{},"Применяется к любому внешнему вызову (HTTP, RPC, БД). Подробнее — в отдельной теме про resilience.",[28,4789],{},[11,4791,4793],{"id":4792},"production-patterns-для-сервисов-вроде-ratedesk","Production patterns для сервисов вроде RateDesk",[16,4795,4796],{},"Паттерны полезны, когда они закрывают конкретный риск. Для сервиса конвертаций чаще всего нужны не абстрактные GoF-названия, а несколько рабочих решений на границах.",[837,4798,4799,4815],{},[4800,4801,4802],"thead",{},[4803,4804,4805,4809,4812],"tr",{},[4806,4807,4808],"th",{},"Риск",[4806,4810,4811],{},"Паттерн",[4806,4813,4814],{},"Где живёт",[4816,4817,4818,4830,4841,4852,4863,4874,4885],"tbody",{},[4803,4819,4820,4824,4827],{},[4821,4822,4823],"td",{},"Популярные пары валют часто читаются",[4821,4825,4826],{},"Cache-Aside",[4821,4828,4829],{},"use case + cache adapter",[4803,4831,4832,4835,4838],{},[4821,4833,4834],{},"Одновременно пришло 100 запросов на один устаревший курс",[4821,4836,4837],{},"Singleflight",[4821,4839,4840],{},"provider\u002Fcache boundary",[4803,4842,4843,4846,4849],{},[4821,4844,4845],{},"Provider временно отвечает 500\u002Ftimeout",[4821,4847,4848],{},"Retry with backoff + Circuit Breaker",[4821,4850,4851],{},"provider adapter",[4803,4853,4854,4857,4860],{},[4821,4855,4856],{},"Повторная доставка Kafka-события",[4821,4858,4859],{},"Idempotent Consumer \u002F Inbox",[4821,4861,4862],{},"consumer adapter + storage",[4803,4864,4865,4868,4871],{},[4821,4866,4867],{},"Нужно опубликовать событие только после commit",[4821,4869,4870],{},"Transactional Outbox",[4821,4872,4873],{},"use case transaction + outbox adapter",[4803,4875,4876,4879,4882],{},[4821,4877,4878],{},"Batch import не должен открыть 1000 соединений",[4821,4880,4881],{},"Bounded Worker Pool",[4821,4883,4884],{},"application service",[4803,4886,4887,4890,4892],{},[4821,4888,4889],{},"HTTP\u002FgRPC\u002FKafka shutdown должен быть управляемым",[4821,4891,4446],{},[4821,4893,4894],{},"все public methods\u002Fadapters",[16,4896,4897],{},"Пример cache-aside без утечки Redis в use case:",[47,4899,4901],{"className":49,"code":4900,"language":51,"meta":5,"style":5},"type RateCache interface {\n    Get(ctx context.Context, pair rates.CurrencyPair) (rates.Rate, error)\n    Set(ctx context.Context, rate rates.Rate, ttl time.Duration) error\n}\n\nfunc (uc *GetLatestRate) Execute(ctx context.Context, pair rates.CurrencyPair) (rates.Rate, error) {\n    rate, err := uc.cache.Get(ctx, pair)\n    if err == nil && rate.IsFresh(uc.clock.Now()) {\n        return rate, nil\n    }\n    if err != nil && !errors.Is(err, ErrCacheMiss) {\n        uc.log.WarnContext(ctx, \"rate cache unavailable\", \"error\", err)\n    }\n\n    rate, err = uc.repo.Latest(ctx, pair)\n    if err != nil {\n        return rates.Rate{}, fmt.Errorf(\"load latest rate: %w\", err)\n    }\n    _ = uc.cache.Set(ctx, rate, uc.cacheTTL) \u002F\u002F cache write failure is not a business failure\n    return rate, nil\n}\n",[40,4902,4903,4914,4957,4996,5000,5004,5056,5071,5098,5107,5111,5135,5155,5159,5163,5177,5189,5217,5221,5238,5246],{"__ignoreMap":5},[55,4904,4905,4907,4910,4912],{"class":57,"line":58},[55,4906,285],{"class":61},[55,4908,4909],{"class":65}," RateCache",[55,4911,1411],{"class":61},[55,4913,125],{"class":69},[55,4915,4916,4918,4920,4922,4924,4926,4928,4930,4933,4936,4938,4941,4943,4946,4948,4951,4953,4955],{"class":57,"line":109},[55,4917,1418],{"class":65},[55,4919,70],{"class":69},[55,4921,1423],{"class":73},[55,4923,1426],{"class":65},[55,4925,312],{"class":69},[55,4927,1431],{"class":65},[55,4929,80],{"class":69},[55,4931,4932],{"class":73},"pair",[55,4934,4935],{"class":65}," rates",[55,4937,312],{"class":69},[55,4939,4940],{"class":65},"CurrencyPair",[55,4942,92],{"class":69},[55,4944,4945],{"class":65},"rates",[55,4947,312],{"class":69},[55,4949,4950],{"class":65},"Rate",[55,4952,80],{"class":69},[55,4954,103],{"class":61},[55,4956,376],{"class":69},[55,4958,4959,4961,4963,4965,4967,4969,4971,4973,4976,4978,4980,4982,4984,4986,4988,4990,4992,4994],{"class":57,"line":128},[55,4960,1455],{"class":65},[55,4962,70],{"class":69},[55,4964,1423],{"class":73},[55,4966,1426],{"class":65},[55,4968,312],{"class":69},[55,4970,1431],{"class":65},[55,4972,80],{"class":69},[55,4974,4975],{"class":73},"rate",[55,4977,4935],{"class":65},[55,4979,312],{"class":69},[55,4981,4950],{"class":65},[55,4983,80],{"class":69},[55,4985,1485],{"class":73},[55,4987,395],{"class":65},[55,4989,312],{"class":69},[55,4991,400],{"class":65},[55,4993,403],{"class":69},[55,4995,1496],{"class":61},[55,4997,4998],{"class":57,"line":141},[55,4999,254],{"class":69},[55,5001,5002],{"class":57,"line":147},[55,5003,356],{"emptyLinePlaceholder":355},[55,5005,5006,5008,5010,5013,5015,5018,5020,5022,5024,5026,5028,5030,5032,5034,5036,5038,5040,5042,5044,5046,5048,5050,5052,5054],{"class":57,"line":165},[55,5007,62],{"class":61},[55,5009,874],{"class":69},[55,5011,5012],{"class":73},"uc ",[55,5014,95],{"class":61},[55,5016,5017],{"class":65},"GetLatestRate",[55,5019,403],{"class":69},[55,5021,4749],{"class":65},[55,5023,70],{"class":69},[55,5025,1423],{"class":73},[55,5027,1426],{"class":65},[55,5029,312],{"class":69},[55,5031,1431],{"class":65},[55,5033,80],{"class":69},[55,5035,4932],{"class":73},[55,5037,4935],{"class":65},[55,5039,312],{"class":69},[55,5041,4940],{"class":65},[55,5043,92],{"class":69},[55,5045,4945],{"class":65},[55,5047,312],{"class":69},[55,5049,4950],{"class":65},[55,5051,80],{"class":69},[55,5053,103],{"class":61},[55,5055,106],{"class":69},[55,5057,5058,5061,5063,5066,5068],{"class":57,"line":175},[55,5059,5060],{"class":69},"    rate, err ",[55,5062,599],{"class":61},[55,5064,5065],{"class":69}," uc.cache.",[55,5067,1555],{"class":65},[55,5069,5070],{"class":69},"(ctx, pair)\n",[55,5072,5073,5075,5077,5079,5081,5084,5087,5090,5093,5095],{"class":57,"line":180},[55,5074,112],{"class":61},[55,5076,1608],{"class":69},[55,5078,118],{"class":61},[55,5080,135],{"class":134},[55,5082,5083],{"class":61}," &&",[55,5085,5086],{"class":69}," rate.",[55,5088,5089],{"class":65},"IsFresh",[55,5091,5092],{"class":69},"(uc.clock.",[55,5094,236],{"class":65},[55,5096,5097],{"class":69},"()) {\n",[55,5099,5100,5102,5105],{"class":57,"line":194},[55,5101,131],{"class":61},[55,5103,5104],{"class":69}," rate, ",[55,5106,248],{"class":134},[55,5108,5109],{"class":57,"line":212},[55,5110,144],{"class":69},[55,5112,5113,5115,5117,5119,5121,5123,5126,5129,5132],{"class":57,"line":218},[55,5114,112],{"class":61},[55,5116,1608],{"class":69},[55,5118,2876],{"class":61},[55,5120,135],{"class":134},[55,5122,5083],{"class":61},[55,5124,5125],{"class":61}," !",[55,5127,5128],{"class":69},"errors.",[55,5130,5131],{"class":65},"Is",[55,5133,5134],{"class":69},"(err, ErrCacheMiss) {\n",[55,5136,5137,5140,5143,5145,5148,5150,5153],{"class":57,"line":224},[55,5138,5139],{"class":69},"        uc.log.",[55,5141,5142],{"class":65},"WarnContext",[55,5144,3325],{"class":69},[55,5146,5147],{"class":121},"\"rate cache unavailable\"",[55,5149,80],{"class":69},[55,5151,5152],{"class":121},"\"error\"",[55,5154,1948],{"class":69},[55,5156,5157],{"class":57,"line":230},[55,5158,144],{"class":69},[55,5160,5161],{"class":57,"line":242},[55,5162,356],{"emptyLinePlaceholder":355},[55,5164,5165,5167,5169,5172,5175],{"class":57,"line":251},[55,5166,5060],{"class":69},[55,5168,430],{"class":61},[55,5170,5171],{"class":69}," uc.repo.",[55,5173,5174],{"class":65},"Latest",[55,5176,5070],{"class":69},[55,5178,5179,5181,5183,5185,5187],{"class":57,"line":471},[55,5180,112],{"class":61},[55,5182,1608],{"class":69},[55,5184,2876],{"class":61},[55,5186,135],{"class":134},[55,5188,125],{"class":69},[55,5190,5191,5193,5195,5197,5199,5202,5205,5207,5210,5213,5215],{"class":57,"line":494},[55,5192,131],{"class":61},[55,5194,4935],{"class":65},[55,5196,312],{"class":69},[55,5198,4950],{"class":65},[55,5200,5201],{"class":69},"{}, fmt.",[55,5203,5204],{"class":65},"Errorf",[55,5206,70],{"class":69},[55,5208,5209],{"class":121},"\"load latest rate: ",[55,5211,5212],{"class":134},"%w",[55,5214,1142],{"class":121},[55,5216,1948],{"class":69},[55,5218,5219],{"class":57,"line":499},[55,5220,144],{"class":69},[55,5222,5223,5226,5228,5230,5232,5235],{"class":57,"line":504},[55,5224,5225],{"class":69},"    _ ",[55,5227,430],{"class":61},[55,5229,5065],{"class":69},[55,5231,1658],{"class":65},[55,5233,5234],{"class":69},"(ctx, rate, uc.cacheTTL) ",[55,5236,5237],{"class":628},"\u002F\u002F cache write failure is not a business failure\n",[55,5239,5240,5242,5244],{"class":57,"line":526},[55,5241,183],{"class":61},[55,5243,5104],{"class":69},[55,5245,248],{"class":134},[55,5247,5248],{"class":57,"line":549},[55,5249,254],{"class":69},[16,5251,5252],{},"Важно: cache miss не является доменной ошибкой, а Redis outage не должен автоматически превращаться в 500, если основной источник доступен. Такая policy должна быть явной, иначе поведение под инцидентом будет случайным.",[16,5254,5255],{},"Пример singleflight на provider boundary:",[47,5257,5259],{"className":49,"code":5258,"language":51,"meta":5,"style":5},"type ProviderWithSingleflight struct {\n    next RateProvider\n    g    singleflight.Group\n}\n\nfunc (p *ProviderWithSingleflight) Fetch(ctx context.Context, pair rates.CurrencyPair) (rates.Rate, error) {\n    v, err, _ := p.g.Do(pair.String(), func() (any, error) {\n        return p.next.Fetch(ctx, pair)\n    })\n    if err != nil {\n        return rates.Rate{}, err\n    }\n    return v.(rates.Rate), nil\n}\n",[40,5260,5261,5272,5279,5292,5296,5300,5353,5386,5397,5401,5413,5426,5430,5448],{"__ignoreMap":5},[55,5262,5263,5265,5268,5270],{"class":57,"line":58},[55,5264,285],{"class":61},[55,5266,5267],{"class":65}," ProviderWithSingleflight",[55,5269,291],{"class":61},[55,5271,125],{"class":69},[55,5273,5274,5276],{"class":57,"line":109},[55,5275,1804],{"class":69},[55,5277,5278],{"class":65},"RateProvider\n",[55,5280,5281,5284,5287,5289],{"class":57,"line":128},[55,5282,5283],{"class":69},"    g    ",[55,5285,5286],{"class":65},"singleflight",[55,5288,312],{"class":69},[55,5290,5291],{"class":65},"Group\n",[55,5293,5294],{"class":57,"line":141},[55,5295,254],{"class":69},[55,5297,5298],{"class":57,"line":147},[55,5299,356],{"emptyLinePlaceholder":355},[55,5301,5302,5304,5306,5309,5311,5314,5316,5319,5321,5323,5325,5327,5329,5331,5333,5335,5337,5339,5341,5343,5345,5347,5349,5351],{"class":57,"line":165},[55,5303,62],{"class":61},[55,5305,874],{"class":69},[55,5307,5308],{"class":73},"p ",[55,5310,95],{"class":61},[55,5312,5313],{"class":65},"ProviderWithSingleflight",[55,5315,403],{"class":69},[55,5317,5318],{"class":65},"Fetch",[55,5320,70],{"class":69},[55,5322,1423],{"class":73},[55,5324,1426],{"class":65},[55,5326,312],{"class":69},[55,5328,1431],{"class":65},[55,5330,80],{"class":69},[55,5332,4932],{"class":73},[55,5334,4935],{"class":65},[55,5336,312],{"class":69},[55,5338,4940],{"class":65},[55,5340,92],{"class":69},[55,5342,4945],{"class":65},[55,5344,312],{"class":69},[55,5346,4950],{"class":65},[55,5348,80],{"class":69},[55,5350,103],{"class":61},[55,5352,106],{"class":69},[55,5354,5355,5358,5360,5363,5365,5368,5371,5374,5376,5378,5380,5382,5384],{"class":57,"line":175},[55,5356,5357],{"class":69},"    v, err, _ ",[55,5359,599],{"class":61},[55,5361,5362],{"class":69}," p.g.",[55,5364,1297],{"class":65},[55,5366,5367],{"class":69},"(pair.",[55,5369,5370],{"class":65},"String",[55,5372,5373],{"class":69},"(), ",[55,5375,62],{"class":61},[55,5377,1033],{"class":69},[55,5379,903],{"class":65},[55,5381,80],{"class":69},[55,5383,103],{"class":61},[55,5385,106],{"class":69},[55,5387,5388,5390,5393,5395],{"class":57,"line":180},[55,5389,131],{"class":61},[55,5391,5392],{"class":69}," p.next.",[55,5394,5318],{"class":65},[55,5396,5070],{"class":69},[55,5398,5399],{"class":57,"line":194},[55,5400,1337],{"class":69},[55,5402,5403,5405,5407,5409,5411],{"class":57,"line":212},[55,5404,112],{"class":61},[55,5406,1608],{"class":69},[55,5408,2876],{"class":61},[55,5410,135],{"class":134},[55,5412,125],{"class":69},[55,5414,5415,5417,5419,5421,5423],{"class":57,"line":218},[55,5416,131],{"class":61},[55,5418,4935],{"class":65},[55,5420,312],{"class":69},[55,5422,4950],{"class":65},[55,5424,5425],{"class":69},"{}, err\n",[55,5427,5428],{"class":57,"line":224},[55,5429,144],{"class":69},[55,5431,5432,5434,5437,5439,5441,5443,5446],{"class":57,"line":230},[55,5433,183],{"class":61},[55,5435,5436],{"class":69}," v.(",[55,5438,4945],{"class":65},[55,5440,312],{"class":69},[55,5442,4950],{"class":65},[55,5444,5445],{"class":69},"), ",[55,5447,248],{"class":134},[55,5449,5450],{"class":57,"line":242},[55,5451,254],{"class":69},[16,5453,5454],{},"Это adapter\u002Fdecorator, а не доменная логика. Домену всё равно, как именно мы защищаем provider от stampede.",[28,5456],{},[11,5458,5460],{"id":5459},"testing","Testing",[35,5462,5464],{"id":5463},"table-driven-tests","Table-driven tests",[47,5466,5468],{"className":49,"code":5467,"language":51,"meta":5,"style":5},"func TestSquare(t *testing.T) {\n    tests := []struct {\n        name  string\n        input int\n        want  int\n    }{\n        {\"positive\", 5, 25},\n        {\"zero\", 0, 0},\n        {\"negative\", -3, 9},\n    }\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            got := Square(tt.input)\n            if got != tt.want {\n                t.Errorf(\"Square(%d) = %d, want %d\", tt.input, got, tt.want)\n            }\n        })\n    }\n}\n",[40,5469,5470,5492,5506,5513,5520,5527,5532,5552,5570,5591,5595,5609,5636,5649,5662,5691,5695,5700,5704],{"__ignoreMap":5},[55,5471,5472,5474,5477,5479,5481,5483,5485,5487,5490],{"class":57,"line":58},[55,5473,62],{"class":61},[55,5475,5476],{"class":65}," TestSquare",[55,5478,70],{"class":69},[55,5480,392],{"class":73},[55,5482,422],{"class":61},[55,5484,5459],{"class":65},[55,5486,312],{"class":69},[55,5488,5489],{"class":65},"T",[55,5491,106],{"class":69},[55,5493,5494,5497,5499,5501,5504],{"class":57,"line":109},[55,5495,5496],{"class":69},"    tests ",[55,5498,599],{"class":61},[55,5500,86],{"class":69},[55,5502,5503],{"class":61},"struct",[55,5505,125],{"class":69},[55,5507,5508,5511],{"class":57,"line":128},[55,5509,5510],{"class":69},"        name  ",[55,5512,301],{"class":61},[55,5514,5515,5518],{"class":57,"line":141},[55,5516,5517],{"class":69},"        input ",[55,5519,817],{"class":61},[55,5521,5522,5525],{"class":57,"line":147},[55,5523,5524],{"class":69},"        want  ",[55,5526,817],{"class":61},[55,5528,5529],{"class":57,"line":165},[55,5530,5531],{"class":69},"    }{\n",[55,5533,5534,5537,5540,5542,5544,5546,5549],{"class":57,"line":175},[55,5535,5536],{"class":69},"        {",[55,5538,5539],{"class":121},"\"positive\"",[55,5541,80],{"class":69},[55,5543,724],{"class":134},[55,5545,80],{"class":69},[55,5547,5548],{"class":134},"25",[55,5550,5551],{"class":69},"},\n",[55,5553,5554,5556,5559,5561,5564,5566,5568],{"class":57,"line":180},[55,5555,5536],{"class":69},[55,5557,5558],{"class":121},"\"zero\"",[55,5560,80],{"class":69},[55,5562,5563],{"class":134},"0",[55,5565,80],{"class":69},[55,5567,5563],{"class":134},[55,5569,5551],{"class":69},[55,5571,5572,5574,5577,5579,5582,5584,5586,5589],{"class":57,"line":194},[55,5573,5536],{"class":69},[55,5575,5576],{"class":121},"\"negative\"",[55,5578,80],{"class":69},[55,5580,5581],{"class":61},"-",[55,5583,4126],{"class":134},[55,5585,80],{"class":69},[55,5587,5588],{"class":134},"9",[55,5590,5551],{"class":69},[55,5592,5593],{"class":57,"line":212},[55,5594,144],{"class":69},[55,5596,5597,5599,5602,5604,5606],{"class":57,"line":218},[55,5598,652],{"class":61},[55,5600,5601],{"class":69}," _, tt ",[55,5603,599],{"class":61},[55,5605,660],{"class":61},[55,5607,5608],{"class":69}," tests {\n",[55,5610,5611,5614,5617,5620,5622,5624,5626,5628,5630,5632,5634],{"class":57,"line":224},[55,5612,5613],{"class":69},"        t.",[55,5615,5616],{"class":65},"Run",[55,5618,5619],{"class":69},"(tt.name, ",[55,5621,62],{"class":61},[55,5623,70],{"class":69},[55,5625,392],{"class":73},[55,5627,422],{"class":61},[55,5629,5459],{"class":65},[55,5631,312],{"class":69},[55,5633,5489],{"class":65},[55,5635,106],{"class":69},[55,5637,5638,5641,5643,5646],{"class":57,"line":230},[55,5639,5640],{"class":69},"            got ",[55,5642,599],{"class":61},[55,5644,5645],{"class":65}," Square",[55,5647,5648],{"class":69},"(tt.input)\n",[55,5650,5651,5654,5657,5659],{"class":57,"line":242},[55,5652,5653],{"class":61},"            if",[55,5655,5656],{"class":69}," got ",[55,5658,2876],{"class":61},[55,5660,5661],{"class":69}," tt.want {\n",[55,5663,5664,5667,5669,5671,5674,5676,5679,5681,5684,5686,5688],{"class":57,"line":251},[55,5665,5666],{"class":69},"                t.",[55,5668,5204],{"class":65},[55,5670,70],{"class":69},[55,5672,5673],{"class":121},"\"Square(",[55,5675,1139],{"class":134},[55,5677,5678],{"class":121},") = ",[55,5680,1139],{"class":134},[55,5682,5683],{"class":121},", want ",[55,5685,1139],{"class":134},[55,5687,1142],{"class":121},[55,5689,5690],{"class":69},", tt.input, got, tt.want)\n",[55,5692,5693],{"class":57,"line":471},[55,5694,4382],{"class":69},[55,5696,5697],{"class":57,"line":494},[55,5698,5699],{"class":69},"        })\n",[55,5701,5702],{"class":57,"line":499},[55,5703,144],{"class":69},[55,5705,5706],{"class":57,"line":504},[55,5707,254],{"class":69},[16,5709,5710,5711,5714,5715,312],{},"Все кейсы в одном месте, легко добавить новый, ",[40,5712,5713],{},"t.Run"," даёт subtest-имена в выводе и параллелизм через ",[40,5716,5717],{},"t.Parallel()",[28,5719],{},[11,5721,5723],{"id":5722},"go-specific-anti-patterns","Go-specific anti-patterns",[35,5725,5727,5730],{"id":5726},"interface-без-необходимости",[40,5728,5729],{},"interface{}"," без необходимости",[47,5732,5734],{"className":49,"code":5733,"language":51,"meta":5,"style":5},"\u002F\u002F Плохо\nfunc Process(data interface{}) { \u002F* type switch внутри *\u002F }\n\n\u002F\u002F Хорошо\nfunc Process[T Processable](data T) { \u002F* generics *\u002F }\n\u002F\u002F или конкретный тип\nfunc ProcessOrder(o *Order) { \u002F* ... *\u002F }\n",[40,5735,5736,5741,5763,5767,5772,5801,5806],{"__ignoreMap":5},[55,5737,5738],{"class":57,"line":58},[55,5739,5740],{"class":628},"\u002F\u002F Плохо\n",[55,5742,5743,5745,5748,5750,5753,5755,5758,5761],{"class":57,"line":109},[55,5744,62],{"class":61},[55,5746,5747],{"class":65}," Process",[55,5749,70],{"class":69},[55,5751,5752],{"class":73},"data",[55,5754,1411],{"class":61},[55,5756,5757],{"class":69},"{}) { ",[55,5759,5760],{"class":628},"\u002F* type switch внутри *\u002F",[55,5762,1532],{"class":69},[55,5764,5765],{"class":57,"line":128},[55,5766,356],{"emptyLinePlaceholder":355},[55,5768,5769],{"class":57,"line":141},[55,5770,5771],{"class":628},"\u002F\u002F Хорошо\n",[55,5773,5774,5776,5778,5780,5782,5785,5788,5790,5793,5796,5799],{"class":57,"line":147},[55,5775,62],{"class":61},[55,5777,5747],{"class":65},[55,5779,2501],{"class":69},[55,5781,5489],{"class":73},[55,5783,5784],{"class":65}," Processable",[55,5786,5787],{"class":69},"](",[55,5789,5752],{"class":73},[55,5791,5792],{"class":65}," T",[55,5794,5795],{"class":69},") { ",[55,5797,5798],{"class":628},"\u002F* generics *\u002F",[55,5800,1532],{"class":69},[55,5802,5803],{"class":57,"line":165},[55,5804,5805],{"class":628},"\u002F\u002F или конкретный тип\n",[55,5807,5808,5810,5813,5815,5817,5819,5821,5823,5825],{"class":57,"line":175},[55,5809,62],{"class":61},[55,5811,5812],{"class":65}," ProcessOrder",[55,5814,70],{"class":69},[55,5816,3478],{"class":73},[55,5818,422],{"class":61},[55,5820,98],{"class":65},[55,5822,5795],{"class":69},[55,5824,2265],{"class":628},[55,5826,1532],{"class":69},[16,5828,5829,5831,5832,5834],{},[40,5830,5729],{}," (или ",[40,5833,903],{},") ломает type safety, требует assertion'ов в runtime, мешает компилятору. Используйте только когда реально нужно (универсальные библиотеки, JSON-парсинг).",[35,5836,5838],{"id":5837},"java-style-getterssetters","Java-style getters\u002Fsetters",[47,5840,5842],{"className":49,"code":5841,"language":51,"meta":5,"style":5},"\u002F\u002F Плохо — getter\u002Fsetter без логики\nfunc (u *User) GetName() string  { return u.name }\nfunc (u *User) SetName(n string) { u.name = n }\n",[40,5843,5844,5849,5878],{"__ignoreMap":5},[55,5845,5846],{"class":57,"line":58},[55,5847,5848],{"class":628},"\u002F\u002F Плохо — getter\u002Fsetter без логики\n",[55,5850,5851,5853,5855,5857,5859,5861,5863,5866,5868,5870,5873,5875],{"class":57,"line":109},[55,5852,62],{"class":61},[55,5854,874],{"class":69},[55,5856,3263],{"class":73},[55,5858,95],{"class":61},[55,5860,4498],{"class":65},[55,5862,403],{"class":69},[55,5864,5865],{"class":65},"GetName",[55,5867,1278],{"class":69},[55,5869,1036],{"class":61},[55,5871,5872],{"class":69},"  { ",[55,5874,3572],{"class":61},[55,5876,5877],{"class":69}," u.name }\n",[55,5879,5880,5882,5884,5886,5888,5890,5892,5895,5897,5899,5901,5904,5906],{"class":57,"line":128},[55,5881,62],{"class":61},[55,5883,874],{"class":69},[55,5885,3263],{"class":73},[55,5887,95],{"class":61},[55,5889,4498],{"class":65},[55,5891,403],{"class":69},[55,5893,5894],{"class":65},"SetName",[55,5896,70],{"class":69},[55,5898,978],{"class":73},[55,5900,572],{"class":61},[55,5902,5903],{"class":69},") { u.name ",[55,5905,430],{"class":61},[55,5907,5908],{"class":69}," n }\n",[16,5910,5911,5912,5915],{},"Если поле просто читается\u002Fпишется — экспортируйте как ",[40,5913,5914],{},"Name",". Геттеры нужны там, где есть валидация, lazy-инициализация, защита инкапсуляции (в DDD-агрегате, например).",[35,5917,5919],{"id":5918},"over-abstracted-интерфейсы","Over-abstracted интерфейсы",[47,5921,5923],{"className":49,"code":5922,"language":51,"meta":5,"style":5},"\u002F\u002F Плохо — интерфейс с одной реализацией \"на будущее\"\ntype UserService interface { \u002F* ... *\u002F }\ntype userService struct{} \u002F\u002F единственная реализация\n",[40,5924,5925,5930,5945],{"__ignoreMap":5},[55,5926,5927],{"class":57,"line":58},[55,5928,5929],{"class":628},"\u002F\u002F Плохо — интерфейс с одной реализацией \"на будущее\"\n",[55,5931,5932,5934,5937,5939,5941,5943],{"class":57,"line":109},[55,5933,285],{"class":61},[55,5935,5936],{"class":65}," UserService",[55,5938,1411],{"class":61},[55,5940,2262],{"class":69},[55,5942,2265],{"class":628},[55,5944,1532],{"class":69},[55,5946,5947,5949,5952,5954,5957],{"class":57,"line":128},[55,5948,285],{"class":61},[55,5950,5951],{"class":65}," userService",[55,5953,291],{"class":61},[55,5955,5956],{"class":69},"{} ",[55,5958,5959],{"class":628},"\u002F\u002F единственная реализация\n",[16,5961,5962,5963,5966],{},"Интерфейсы в Go объявляются на стороне ",[23,5964,5965],{},"потребителя",", а не производителя. Объявляйте интерфейс там, где он нужен, и только когда есть ≥ 2 реализации (или мок для тестов реально нужен).",[5968,5969,5970],"blockquote",{},[16,5971,5972],{},"«Accept interfaces, return structs.»",[35,5974,5976],{"id":5975},"пустые-структуры-с-бизнес-логикой-в-сервисах","Пустые структуры с бизнес-логикой в сервисах",[16,5978,5979],{},"Анемичная модель — описана в теме про DDD. В Go легко скатиться: «структура — это просто контейнер данных, логика — в сервисе». В результате вся проверка инвариантов размазана.",[35,5981,5983],{"id":5982},"игнор-паники-в-горутине","Игнор паники в горутине",[47,5985,5987],{"className":49,"code":5986,"language":51,"meta":5,"style":5},"go func() {\n    \u002F\u002F если паникнём — упадёт ВСЯ программа\n    doWork()\n}()\n",[40,5988,5989,5997,6002,6009],{"__ignoreMap":5},[55,5990,5991,5993,5995],{"class":57,"line":58},[55,5992,51],{"class":61},[55,5994,366],{"class":61},[55,5996,1304],{"class":69},[55,5998,5999],{"class":57,"line":109},[55,6000,6001],{"class":628},"    \u002F\u002F если паникнём — упадёт ВСЯ программа\n",[55,6003,6004,6007],{"class":57,"line":128},[55,6005,6006],{"class":65},"    doWork",[55,6008,1209],{"class":69},[55,6010,6011],{"class":57,"line":141},[55,6012,6013],{"class":69},"}()\n",[16,6015,6016,6017,6020],{},"Лечение: ",[40,6018,6019],{},"defer recover()"," на уровне worker'ов или внешний supervisor.",[47,6022,6024],{"className":49,"code":6023,"language":51,"meta":5,"style":5},"go func() {\n    defer func() {\n        if r := recover(); r != nil {\n            log.Errorf(\"panic in worker: %v\\n%s\", r, debug.Stack())\n        }\n    }()\n    doWork()\n}()\n",[40,6025,6026,6034,6042,6064,6090,6094,6098,6104],{"__ignoreMap":5},[55,6027,6028,6030,6032],{"class":57,"line":58},[55,6029,51],{"class":61},[55,6031,366],{"class":61},[55,6033,1304],{"class":69},[55,6035,6036,6038,6040],{"class":57,"line":109},[55,6037,2575],{"class":61},[55,6039,366],{"class":61},[55,6041,1304],{"class":69},[55,6043,6044,6047,6050,6052,6055,6058,6060,6062],{"class":57,"line":128},[55,6045,6046],{"class":61},"        if",[55,6048,6049],{"class":69}," r ",[55,6051,599],{"class":61},[55,6053,6054],{"class":65}," recover",[55,6056,6057],{"class":69},"(); r ",[55,6059,2876],{"class":61},[55,6061,135],{"class":134},[55,6063,125],{"class":69},[55,6065,6066,6069,6071,6073,6076,6079,6081,6084,6087],{"class":57,"line":141},[55,6067,6068],{"class":69},"            log.",[55,6070,5204],{"class":65},[55,6072,70],{"class":69},[55,6074,6075],{"class":121},"\"panic in worker: ",[55,6077,6078],{"class":134},"%v\\n%s",[55,6080,1142],{"class":121},[55,6082,6083],{"class":69},", r, debug.",[55,6085,6086],{"class":65},"Stack",[55,6088,6089],{"class":69},"())\n",[55,6091,6092],{"class":57,"line":147},[55,6093,2714],{"class":69},[55,6095,6096],{"class":57,"line":165},[55,6097,3750],{"class":69},[55,6099,6100,6102],{"class":57,"line":175},[55,6101,6006],{"class":65},[55,6103,1209],{"class":69},[55,6105,6106],{"class":57,"line":180},[55,6107,6013],{"class":69},[28,6109],{},[11,6111,6113],{"id":6112},"идиоматичность-что-не-тащить-из-java","Идиоматичность: что НЕ тащить из Java",[837,6115,6116,6126],{},[4800,6117,6118],{},[4803,6119,6120,6123],{},[4806,6121,6122],{},"Java\u002FC#",[4806,6124,6125],{},"Go-идиома",[4816,6127,6128,6140,6154,6165,6173,6181,6196,6206,6214],{},[4803,6129,6130,6135],{},[4821,6131,6132],{},[40,6133,6134],{},"AbstractFactoryFactoryBuilder",[4821,6136,6137,6139],{},[40,6138,42],{}," функция",[4803,6141,6142,6149],{},[4821,6143,6144,6145,6148],{},"Аннотации ",[40,6146,6147],{},"@Inject",", DI-контейнер",[4821,6150,6151,6152],{},"Явная передача зависимостей в ",[40,6153,1377],{},[4803,6155,6156,6162],{},[4821,6157,6158,6161],{},[40,6159,6160],{},"IUserService"," интерфейс на каждом сервисе",[4821,6163,6164],{},"Интерфейс там, где он нужен потребителю",[4803,6166,6167,6170],{},[4821,6168,6169],{},"Геттеры\u002Fсеттеры на всех полях",[4821,6171,6172],{},"Экспортируемое поле или метод с логикой",[4803,6174,6175,6178],{},[4821,6176,6177],{},"Иерархии наследования",[4821,6179,6180],{},"Композиция, embedding",[4803,6182,6183,6188],{},[4821,6184,6185],{},[40,6186,6187],{},"Optional\u003CT>",[4821,6189,6190,4581,6193],{},[40,6191,6192],{},"(T, error)",[40,6194,6195],{},"(T, ok bool)",[4803,6197,6198,6201],{},[4821,6199,6200],{},"Pattern Matching через Visitor",[4821,6202,6203],{},[40,6204,6205],{},"switch x.(type)",[4803,6207,6208,6211],{},[4821,6209,6210],{},"Reactive Streams",[4821,6212,6213],{},"Каналы + goroutines",[4803,6215,6216,6219],{},[4821,6217,6218],{},"Singleton с двойной проверкой",[4821,6220,6221],{},[40,6222,1219],{},[16,6224,6225],{},"Гайдлайн: если вы переписываете Java-проект на Go и сохраняете архитектуру 1:1 — вы пишете Java на Go. Это работает плохо.",[28,6227],{},[11,6229,6231],{"id":6230},"чек-лист-ревью","Чек-лист ревью",[1353,6233,6236,6245,6251,6257,6263,6269,6281,6287,6293,6299,6305,6311,6317],{"className":6234},[6235],"contains-task-list",[1356,6237,6240,6244],{"className":6238},[6239],"task-list-item",[6241,6242],"input",{"disabled":355,"type":6243},"checkbox"," Конструкторы валидируют входы, возвращают ошибку?",[1356,6246,6248,6250],{"className":6247},[6239],[6241,6249],{"disabled":355,"type":6243}," Опциональных параметров > 3 → Functional Options?",[1356,6252,6254,6256],{"className":6253},[6239],[6241,6255],{"disabled":355,"type":6243}," Singleton действительно нужен (один на процесс) или это лень DI?",[1356,6258,6260,6262],{"className":6259},[6239],[6241,6261],{"disabled":355,"type":6243}," Интерфейсы объявлены у потребителя, а не у производителя?",[1356,6264,6266,6268],{"className":6265},[6239],[6241,6267],{"disabled":355,"type":6243}," У интерфейса есть ≥ 1 реальной реализации (не «на будущее»)?",[1356,6270,6272,6274,6275,6277,6278,6280],{"className":6271},[6239],[6241,6273],{"disabled":355,"type":6243}," Нет ",[40,6276,5729],{},"\u002F",[40,6279,903],{}," там, где можно обойтись конкретным типом или generic'ом?",[1356,6282,6284,6286],{"className":6283},[6239],[6241,6285],{"disabled":355,"type":6243}," Нет Java-style геттеров\u002Fсеттеров без логики?",[1356,6288,6290,6292],{"className":6289},[6239],[6241,6291],{"disabled":355,"type":6243}," Все горутины завершаются при ctx.Done(), нет утечек?",[1356,6294,6296,6298],{"className":6295},[6239],[6241,6297],{"disabled":355,"type":6243}," Worker Pool ограничивает concurrency для тяжёлых задач?",[1356,6300,6302,6304],{"className":6301},[6239],[6241,6303],{"disabled":355,"type":6243}," Внешние вызовы обёрнуты Circuit Breaker \u002F Retry с backoff?",[1356,6306,6308,6310],{"className":6307},[6239],[6241,6309],{"disabled":355,"type":6243}," Транзакции управляются на уровне Use Case (Unit of Work), не в репозиториях?",[1356,6312,6314,6316],{"className":6313},[6239],[6241,6315],{"disabled":355,"type":6243}," Тесты — table-driven где это уместно?",[1356,6318,6320,6322],{"className":6319},[6239],[6241,6321],{"disabled":355,"type":6243}," panic в горутинах ловится через defer recover?",[11,6324,6326],{"id":6325},"вопросы-для-интервью","Вопросы для интервью",[6328,6329,6330,6333,6336,6339,6342,6345,6348,6351,6354,6357,6360,6363],"ol",{},[1356,6331,6332],{},"Чем Functional Options лучше Builder в Go?",[1356,6334,6335],{},"Когда Singleton оправдан, когда нет?",[1356,6337,6338],{},"Где объявлять интерфейсы — у потребителя или у производителя? Почему?",[1356,6340,6341],{},"Что такое Repository и Unit of Work? В чём разница?",[1356,6343,6344],{},"Расскажи про Pipeline и Worker Pool на каналах. Когда что выбирать?",[1356,6346,6347],{},"Что такое Circuit Breaker? Какие у него состояния?",[1356,6349,6350],{},"Что такое Adapter и чем он отличается от портов\u002Fадаптеров в Hexagonal?",[1356,6352,6353],{},"Что такое Command pattern и как он связан с CQRS?",[1356,6355,6356],{},"Какие Go-специфичные анти-паттерны знаешь?",[1356,6358,6359],{},"Что значит «Accept interfaces, return structs»?",[1356,6361,6362],{},"Как защититься от утечек горутин в Pipeline?",[1356,6364,6365,6366,6368],{},"Зачем нужен ",[40,6367,4155],{}," и какие правила его использования?",[28,6370],{},[11,6372,6374],{"id":6373},"практика","Практика",[6376,6377,6379,6382,6399],"quiz",{"answer":4131,"id":6378,"xp":1202},"arch-patterns-q1",[16,6380,6381],{},"Как лучше относиться к паттернам в Go?",[6383,6384,6385],"template",{"v-slot:options":5},[1353,6386,6387,6390,6393,6396],{},[1356,6388,6389],{},"Использовать как можно больше паттернов GoF",[1356,6391,6392],{},"Заменять любую функцию структурой с интерфейсом",[1356,6394,6395],{},"Всегда начинать с Singleton",[1356,6397,6398],{},"Использовать паттерн только когда он делает код проще и понятнее",[6383,6400,6401],{"v-slot:explanation":5},[16,6402,6403],{},"В Go многие паттерны становятся обычными функциями, интерфейсами или каналами. Паттерн полезен как язык команды, но вреден как самоцель.",[6405,6406,6410,6413,6581],"predict",{"answer":6407,"id":6408,"xp":6409},"100\\n90","arch-patterns-p1","15",[16,6411,6412],{},"Что выведет программа?",[6383,6414,6415],{"v-slot:code":5},[47,6416,6418],{"className":49,"code":6417,"language":51,"meta":5,"style":5},"package main\n\nimport \"fmt\"\n\ntype Discount func(int) int\n\nfunc noDiscount(v int) int { return v }\nfunc tenPercent(v int) int { return v * 90 \u002F 100 }\n\nfunc main() {\n    fmt.Println(noDiscount(100))\n    fmt.Println(tenPercent(100))\n}\n",[40,6419,6420,6428,6432,6443,6447,6464,6468,6493,6529,6533,6542,6560,6577],{"__ignoreMap":5},[55,6421,6422,6425],{"class":57,"line":58},[55,6423,6424],{"class":61},"package",[55,6426,6427],{"class":65}," main\n",[55,6429,6430],{"class":57,"line":109},[55,6431,356],{"emptyLinePlaceholder":355},[55,6433,6434,6436,6438,6441],{"class":57,"line":128},[55,6435,4615],{"class":61},[55,6437,4618],{"class":121},[55,6439,6440],{"class":65},"fmt",[55,6442,4624],{"class":121},[55,6444,6445],{"class":57,"line":141},[55,6446,356],{"emptyLinePlaceholder":355},[55,6448,6449,6451,6454,6456,6458,6460,6462],{"class":57,"line":147},[55,6450,285],{"class":61},[55,6452,6453],{"class":65}," Discount",[55,6455,366],{"class":61},[55,6457,70],{"class":69},[55,6459,4041],{"class":61},[55,6461,403],{"class":69},[55,6463,817],{"class":61},[55,6465,6466],{"class":57,"line":165},[55,6467,356],{"emptyLinePlaceholder":355},[55,6469,6470,6472,6475,6477,6480,6482,6484,6486,6488,6490],{"class":57,"line":175},[55,6471,62],{"class":61},[55,6473,6474],{"class":65}," noDiscount",[55,6476,70],{"class":69},[55,6478,6479],{"class":73},"v",[55,6481,981],{"class":61},[55,6483,403],{"class":69},[55,6485,4041],{"class":61},[55,6487,2262],{"class":69},[55,6489,3572],{"class":61},[55,6491,6492],{"class":69}," v }\n",[55,6494,6495,6497,6500,6502,6504,6506,6508,6510,6512,6514,6516,6518,6521,6524,6527],{"class":57,"line":180},[55,6496,62],{"class":61},[55,6498,6499],{"class":65}," tenPercent",[55,6501,70],{"class":69},[55,6503,6479],{"class":73},[55,6505,981],{"class":61},[55,6507,403],{"class":69},[55,6509,4041],{"class":61},[55,6511,2262],{"class":69},[55,6513,3572],{"class":61},[55,6515,3773],{"class":69},[55,6517,95],{"class":61},[55,6519,6520],{"class":134}," 90",[55,6522,6523],{"class":61}," \u002F",[55,6525,6526],{"class":134}," 100",[55,6528,1532],{"class":69},[55,6530,6531],{"class":57,"line":194},[55,6532,356],{"emptyLinePlaceholder":355},[55,6534,6535,6537,6540],{"class":57,"line":212},[55,6536,62],{"class":61},[55,6538,6539],{"class":65}," main",[55,6541,1304],{"class":69},[55,6543,6544,6546,6548,6550,6553,6555,6557],{"class":57,"line":218},[55,6545,3796],{"class":69},[55,6547,2086],{"class":65},[55,6549,70],{"class":69},[55,6551,6552],{"class":65},"noDiscount",[55,6554,70],{"class":69},[55,6556,3789],{"class":134},[55,6558,6559],{"class":69},"))\n",[55,6561,6562,6564,6566,6568,6571,6573,6575],{"class":57,"line":224},[55,6563,3796],{"class":69},[55,6565,2086],{"class":65},[55,6567,70],{"class":69},[55,6569,6570],{"class":65},"tenPercent",[55,6572,70],{"class":69},[55,6574,3789],{"class":134},[55,6576,6559],{"class":69},[55,6578,6579],{"class":57,"line":230},[55,6580,254],{"class":69},[6383,6582,6583],{"v-slot:hint":5},[16,6584,6585],{},"Strategy в Go часто выглядит как обычная функция.",[6587,6588,6592,6599,6717],"code-task",{"expected":6589,"id":6590,"xp":6591},"auth\\nlog\\nhandler","arch-patterns-ct1","20",[16,6593,6594,6595,6598],{},"Реализуй ",[40,6596,6597],{},"Chain",": она должна вызвать middleware по порядку, а затем handler.",[6383,6600,6601],{"v-slot:template":5},[47,6602,6604],{"className":49,"code":6603,"language":51,"meta":5,"style":5},"package main\n\nimport \"fmt\"\n\nfunc Chain(steps []string) {\n    fmt.Println(\"todo\")\n}\n\nfunc main() {\n    Chain([]string{\"auth\", \"log\"})\n\n    _ = fmt.Println\n}\n",[40,6605,6606,6612,6616,6626,6630,6648,6661,6665,6669,6677,6700,6704,6713],{"__ignoreMap":5},[55,6607,6608,6610],{"class":57,"line":58},[55,6609,6424],{"class":61},[55,6611,6427],{"class":65},[55,6613,6614],{"class":57,"line":109},[55,6615,356],{"emptyLinePlaceholder":355},[55,6617,6618,6620,6622,6624],{"class":57,"line":128},[55,6619,4615],{"class":61},[55,6621,4618],{"class":121},[55,6623,6440],{"class":65},[55,6625,4624],{"class":121},[55,6627,6628],{"class":57,"line":141},[55,6629,356],{"emptyLinePlaceholder":355},[55,6631,6632,6634,6637,6639,6642,6644,6646],{"class":57,"line":147},[55,6633,62],{"class":61},[55,6635,6636],{"class":65}," Chain",[55,6638,70],{"class":69},[55,6640,6641],{"class":73},"steps",[55,6643,86],{"class":69},[55,6645,1036],{"class":61},[55,6647,106],{"class":69},[55,6649,6650,6652,6654,6656,6659],{"class":57,"line":165},[55,6651,3796],{"class":69},[55,6653,2086],{"class":65},[55,6655,70],{"class":69},[55,6657,6658],{"class":121},"\"todo\"",[55,6660,376],{"class":69},[55,6662,6663],{"class":57,"line":175},[55,6664,254],{"class":69},[55,6666,6667],{"class":57,"line":180},[55,6668,356],{"emptyLinePlaceholder":355},[55,6670,6671,6673,6675],{"class":57,"line":194},[55,6672,62],{"class":61},[55,6674,6539],{"class":65},[55,6676,1304],{"class":69},[55,6678,6679,6682,6685,6687,6690,6693,6695,6698],{"class":57,"line":212},[55,6680,6681],{"class":65},"    Chain",[55,6683,6684],{"class":69},"([]",[55,6686,1036],{"class":61},[55,6688,6689],{"class":69},"{",[55,6691,6692],{"class":121},"\"auth\"",[55,6694,80],{"class":69},[55,6696,6697],{"class":121},"\"log\"",[55,6699,3614],{"class":69},[55,6701,6702],{"class":57,"line":218},[55,6703,356],{"emptyLinePlaceholder":355},[55,6705,6706,6708,6710],{"class":57,"line":224},[55,6707,5225],{"class":69},[55,6709,430],{"class":61},[55,6711,6712],{"class":69}," fmt.Println\n",[55,6714,6715],{"class":57,"line":230},[55,6716,254],{"class":69},[6383,6718,6719],{"v-slot:hints":5},[1353,6720,6721,6727,6733],{},[1356,6722,6723,6724,6726],{},"Сначала выведи все элементы ",[40,6725,6641],{}," в исходном порядке",[1356,6728,6729,6730],{},"После них выведи ",[40,6731,6732],{},"\"handler\"",[1356,6734,6735],{},"Это упрощённая модель middleware pipeline",[6737,6738,6739],"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 pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}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}",{"title":5,"searchDepth":109,"depth":109,"links":6741},[6742,6743,6750,6754,6759,6763,6770,6771,6774,6782,6783,6784,6785],{"id":13,"depth":109,"text":14},{"id":32,"depth":109,"text":33,"children":6744},[6745,6747,6748,6749],{"id":37,"depth":128,"text":6746},"Factory — NewXxx()",{"id":267,"depth":128,"text":268},{"id":763,"depth":128,"text":764},{"id":1212,"depth":128,"text":1213},{"id":1383,"depth":109,"text":1384,"children":6751},[6752,6753],{"id":1387,"depth":128,"text":1388},{"id":1724,"depth":128,"text":1725},{"id":2145,"depth":109,"text":2146,"children":6755},[6756,6757,6758],{"id":2149,"depth":128,"text":2150},{"id":2461,"depth":128,"text":2462},{"id":2728,"depth":128,"text":2729},{"id":2995,"depth":109,"text":2996,"children":6760},[6761,6762],{"id":2999,"depth":128,"text":3000},{"id":3161,"depth":128,"text":3162},{"id":3630,"depth":109,"text":3631,"children":6764},[6765,6766,6767,6768,6769],{"id":3634,"depth":128,"text":3635},{"id":3811,"depth":128,"text":3812},{"id":4168,"depth":128,"text":4169},{"id":4445,"depth":128,"text":4446},{"id":4593,"depth":128,"text":4594},{"id":4792,"depth":109,"text":4793},{"id":5459,"depth":109,"text":5460,"children":6772},[6773],{"id":5463,"depth":128,"text":5464},{"id":5722,"depth":109,"text":5723,"children":6775},[6776,6778,6779,6780,6781],{"id":5726,"depth":128,"text":6777},"interface{} без необходимости",{"id":5837,"depth":128,"text":5838},{"id":5918,"depth":128,"text":5919},{"id":5975,"depth":128,"text":5976},{"id":5982,"depth":128,"text":5983},{"id":6112,"depth":109,"text":6113},{"id":6230,"depth":109,"text":6231},{"id":6325,"depth":109,"text":6326},{"id":6373,"depth":109,"text":6374},1781022066842]