Хороший продукт это тот который появился в условиях свободного рынка, а не под упряжкой в телеге.
Давайте сойдемся на том, что хороший коммерческий продукт с гораздо большей вероятностью появится в условиях рыночной конкуренции. А то как агитка выглядит.
не, это не про "возможность роста" в том смысле, что могут вырасти. Там про то, что "бла-бла-бла, все дорожает, но нельзя исключать, что провайдеры просто задирают цены при любой возможности"
Оппортунистический рост цен - это типа "повышаем, потому что можем". Сложились условия на рынке, что у конкурентов либо нет аналога, либо миграция очень дорогая - поднимаем цены, потому что "а куда ты денешься с подводной лодки"
Думаю, наиболее легкое объяснение горутинам такое: горутина (goroutine) — это легковесный оберточный функционал над потоком.
Что такое "оберточный функционал над потоком"? Оно мало того, что не понятно, так еще и искажает суть явления.
A goroutine is a lightweight thread managed by the Go runtime.
Легковесный тред, управляемый рантаймом Go. Это не обертка над потоком. Обертка над потоком - это Machine. Горутина - это штука, которая исполняется на этом потоке.
Планировщик операционной системы, в которой работает программа, переключает Машины.
Планировщик операционной системы не имеет прямого отношения к "Машинам". Он тупо запускает N (GOMAXPROCS) системных тредов, в которых они потом так и живут, ничего он не переключает. А вот планировщик в рантайме Go распределяет горутины по машинам, на которых они исполняются.
Для запуска функции как горутину необходимо написать go func() , где func() - функция, которую хотите запустить.
Не совсем так. go func() не запускает функцию, а всего лишь ставит горутину в очередь исполнения, передает ее планировщику, чтобы он по возможности отдал ее одной из "машин" на исполнение.
Собственно, поэтому следующее тоже не совсем верно:
Ничего не произойдет, ведь горутина выполняется параллельно с "main()"
В вашем случае с некоторой ненулевой вероятностью горутина вообще не выполняется, тупо в очереди находится (тем более в go playground, который, возможно, вообще однопоточный)
// создаем WaitGroup
wg := &sync.WaitGroup{}
Вот тут & излишне. Дальше ваша wg передается замыканием, а замыкание в Go проходит по указателю.
"mutual exclusion", что в переводе означает "взаимное изолирование"
В переводе это означает "взаимное исключение"
это некая примитивная синхронизация
Это вы так synchronization primitive перевели? Не "некая примитивная синхронизация", а "примитив синхронизации". Как, к слову, и WaitGroup и даже, внезапно, канал.
Перед обновлением счетчика, мы вызываем "lock.Lock()" для создания блокировки. Теперь ни одна другая горутина не сможет использовать "counter".
Ну вы бы хоть рассказали, почему не сможет. Это примитив синхронизации. Он заблокирует горутину (да, он не значение блокирует, а горутину), пока инструкция не сможет быть исполнена.
Т.е. если вы лочите мьютекс из горутины №1, а потом пытаетесь сделать то же самое из горутины №2, то вторая горутина заблокируется, пока первая не разлочит мьютекс.
Кстати, раз уж вы решили разложить все по полочкам, то примитивы синхронизации надо было начинать с атомиков. Потому что у мьютекса и вейтгруппы внутри как раз они и есть.
Атомарные операции - это низкоуровневые примитивы синхронизации, которые обеспечивают способ выполнения операций чтения-модификации-записи (read-modify-write) над общими переменными без необходимости в блокировке (Атомарная операция — операция, которая либо выполняется целиком, либо не выполняется вовсе; операция, которая не может быть частично выполнена и частично не выполнена).
Ну вот, в формулировке read-modify-write, а в примере ниже save/load. Не надо так.
Ну и вот это "Атомарная операция — операция, которая либо выполняется целиком, либо не выполняется вовсе" - это скорее к теории баз данных формулировка, и это про транзакционность.
В трактовке многопоточных приложений - a sequence of instructions that are executed as a single, indivisible unit of work
Последовательность инструкций, исполняемых как одна неделимая единица работы. Т.е. это гарантия, что в процессе выполнения не произойдет переключение контекста.
Например i++ не атомарна. Это получение значения переменной + увеличение значения на 1 + запись значения в переменную - 3 операции, соседний поток может вклиниться между любой из них, что приведет к неконсистентности данных. Атомарные операции отличаются тем, что соседний поток вклиниться не может, они для того и нужны.
Технически - это конвейер, откуда можно считывать или помещать данные. То есть одна горутина может отправить данные в канал, а другая — считать помещенные в этот канал данные.
Ничего не понятно. Технически канал - примитив синхронизации, по сути - семафор.
Канал можно и закрыть.
Канал нужно закрывать. Просто за правило возьмите: как только записали в канал все, что хотели - закройте канал.
Для этого надо вызвать функцию close(Ваш канал), тем самым заблокировав доступ к чтению из него.
В смысле, заблокировав доступ к чтению??? Чтение из закрытого канала возвращает zero-value для типа канала. С чего оно заблокируется-то?
val, ok := <- Ваш канал, где val - переменная, в которую запишется значение с канала, если это возможно, а ok - булева переменная, где true означает, что из канала можно прочитать данные, а false - что он закрыт/невозможно считать.
Ну доку-то почитайте! false означает, что канал закрыт (и все значения из него прочитаны, в случае буферизованного канала). Никаких невозможно читать там нет, даже напротив, читать из него очень даже возможно.
С помощью for range можно читать данные из закрытого буферизированного канала, так как данные остаются в буфере даже после закрытия канала.
А без range'а нельзя? Данные в буфере пропадают? Что вы несете?
Если Вы будете считывать данные из пустого канала, Вы получите ошибку "Deadlock", которая появляется при бесконечном считывании из канала.
Дичь. Эталонная. Дедлок - это ситуация, когда все горутины находятся в спящем состоянии и не могут быть "разбужены", тчк.
select - это почти что switch, но без аргументов.
Гениальное определение. Неверное, ничего не объясняющее, но гениальное.
Вот такое будет ближе к истине:
Примитив синхронизации, блокирующий поток исполнения, пока один из case'ов не сможет быть выполнен.
Я долго писал эту статью, вытаскивая из каждого источника самое важно
Достаточно было одного источника, https://go.dev/doc/
Если речь не идет о специфическом ПО, работающем только под Windows, Linux может выполнять все задачи, которые обычно нужны среднестатистическому человеку. Это офис, серфинг по сети, программирование (тут есть специфика, конечно) и все такое прочее.
У вас "(тут есть специфика, конечно)" отклеилась. Правильно вот так:
офис (тут есть специфика, конечно), серфинг по сети, программирование и все такое прочее.
В заголовке - про извлечение данных из Linux, а в содержимом - gps, уровень wifi, уровень батареи и свободное место на диске.
Половина этого к Linux имеет весьма опосредованное отношение, вторая - вообще кроссплатформенными кутишными апи решается. И все это вместе достаточно сложно назвать прям уж "данными".
Признаться, ожидал какого-то сурового парсинга терабайтных файлов или анализа гигабайтных чанков логов ядра...
не бывает так. Любые отчисления идут последовательно. Если 30% от суммы продаж, то Valve будет брать свою долю от уменьшенной суммы, комиссия за перевод будет от суммы перевода и т.д. С российской стороны налог будет браться уже от того остатка, который пришёл на счёт. Дважды обложить отчислением одну и ту же сумму нельзя
По идее, если отстраниться от различия между кооперативной и преемптивной асинхронностью (т.е. наличием/отсутствием явного await), tokio::spawn работает точно так же, как ключевое слово go.
Не совсем. tokio::spawn - паттерн actor, go - csp.
Вот тут есть сравнение подходов: https://en.wikipedia.org/wiki/Communicating_sequential_processes
Прям раздел есть Actor vs CSP
Вкратце:
актор - управляемый, CSP - нет.
csp - обмен сообщениями эксклюзивно через каналы
actor позволяет отправку сообщения с отложенной обработкой, csp - блокирует отправку, пока читатель не готов к получению.
Из побочек: Rust, видимо, стектрейс ломать не дает. Т.е., фактически, мы не можем завершить выполнение вызывающего метода (как минимум стек вызова чистить нельзя), пока заспавненный не завершился + мы иерархию задач имеем. В Go - ипанул и забыл.
tokio::spawn работает точно так же, как ключевое слово go.
M:N - задачи, закинутые в spawn, распределяются по всем доступным ядрам, если не сказать прямо "работаем только в текущем потоке".
async при этом явно говорит "работаем в текущем потоке".
tokio::spawn(async {
Присмотритесь внимательно:
tokio::spawn - 1:N
async {} - M:1
Это не M:N, это, по сути, от thread::spawn отличается только наличием "легковесного потока".
То, что вы написали - это "запустить поток, внутри которого выполнить асинхронный код". Т.е. это будет N раз по M:1. Это не то же самое, что M:N.
Просто для ясности ситуации:
Допустим, вы спавните (tokio::spawn) 2 потока, должны дождаться выполнения обоих и с полученными данными дальше что-то делать. В первом вы асинхронно запускаете 10 задач, во втором - 2. Допустим, время выполнения асинхронной задачи константно, берем за единицу.
За сколько выполнится ваш код? Очевидно, за 10 (потому что 10 задач на одном потоке выполнялись, а второй выполнил 2 и благополучно сдох).
Теперь представим, что у нас 2 потока доступно гошному рантайму (на самом деле мне, как разработчику, откровенно насрать, сколько их там, я могу и имею право даже не знать). Мы запустили 10 горутин в одном блоке и 2 - в другом. Итого 12. Делим на 2 потока - сделали за 6.
Все, на этом "фишки" языка Go окончены. Он создан ровно ради этой фичи. Во всех остальных сценариях Rust круче, C# круче, Java кручке, все круче (кроме, простихоспади, PHP, Basic'а и "Языка программирования 1С" ).
Я не покажу, я в расте так себе. Но скорее всего, оно M:N, потому что токио - мультитредовый рантайм.
Да, токио - мультитредовый рантайм, и нет, оно не M:N. Оно 1:N.
async/await - M:1, tokyo - 1:N. M:N, кроме Go, из коробки, вроде, Haskell только (или Erlang, не помню, не большой специалист в хаскелях и эрлангах) и умеют. Собственно, весь язык ровно из плясок над "M:N с человеческим лицом" и состоит. Все, что мешает этой концепции - совершенно обоснованно идет лесом. Исключения, например, одна из вещей, которая мешает.
Как типизация влияет на 1:N/M:1/M:N? Разговор был о том, что в го так себе реализованы ошибки.
Типизация, в общем смысле, не мешает. А try/catch, ? и прочие примитивы обработки исключительных/ошибочных ситуаций - мешает. Я вот про это говорю.
И, не смотря на то, что вы считаете, что в других языках так же, то нет, не так же, а получше.
Дык, там и M:N нету. Другие языки решают другие проблемы.
В Go нет исключений и они вряд ли там к месту. Забейте уже на них)
Я бы уже давно забил, если бы вы мне в каждом первом комментарии не рассказывали, какие они классные в C#, и почему в Go они прямо необходимы.
Читаемость роняет мусорный бойлерплейт.
Все же, еще раз про исключения: в го они этим самым мусорным бойлерплейтом и станут, без вариантов.
А растаманский подход с Result/Optional и т.д... ну, в виде дженериков - хоть сейчас пиши. Полноценная реализация на уровне языка - кортежи мешают (они слишком дофига свободы дают)
Еще раз приведу примеры из стандартной библиотеки, где это делается.
Еще раз говорю, это не проброс ошибки, а возврат значения.
Проброс - отдельный флоу, предполагающий наличие перехватчика выше. Значение отличается тем, что его можно законно проигнорировать. Go - это не просто "другой синтаксис", его отличия от других языков несколько глубже. И эти отличия накладывают некоторые ограничения, с которыми приходится жить.
И это не хорошо и не плохо, это просто цена 100-тысячных RPS.
Может этот код и не вызывается в горутинах, не копал, но явно противоречит вашим ультимативным заявлениям, что так никто не делает и вообще не рекомендуется.
В том и дело, что Go явным образом утверждает, что любой код может запускаться в горутине. А еще Go настаивает, что ошибка - это значение, а значит, может быть проигнорирована. В случае запуска в отдельной горутине, более того, будет проигнорирована.
Поинт в том, что код внутри функции, по определению, должен быть готов к ситуации, что ошибку (или любое другое возвращенное значение) могут проигнорировать. Поэтому никакой сценарий работы функции не может приводить к панике (кроме тех случаев, когда паника явным образом нужна) или исключительной ситуации.
На выходе мы имеем 2 сценария групповой обработки ошибок/исключительных ситуаций:
обработать те, с которыми мы знаем, что делать, остальные уйдут по стеку наверх (exception-style) с падением приложения при выходе за границы стека - неприменимо в Go, т.к. наличие "наверх" никто не гарантирует. Мы вырождаемся в обязанность никогда не возвращать ошибки, что выглядит как бред.
гарантировать обработку всех ошибок (назовем это rust-style) - ну, какбэ, бойлерплейт по обработке рискует превратиться в портянку такого размера, что if err != nil {} покажется цветочками.
А потом поняли, что так жить грустно и начали фичи завозить.
Фичи завозят, стоит признать, аккуратно, дозированно, и только те, которые признаны удачными.
Богатый синтаксис - это ведь круто.
А вот тут - не всегда. Смотрел списки фич в Python'е, чейнджлоги того же PHP... А работать когда?
Ошибка возникает в parse_string_to_smth. Что должен делать parse_file? На месте обработать? Кинуть панику или вернуть что-то пустое?
Если знает, что с ошибкой делать - обработать на месте. Если не знает - обернуть своей ошибкой и вернуть. Панику - а зачем? Ну, если нужна паника - кинуть панику. В чем проблема-то?
Так это почти всегда (если нормально делать) не зона ответственности этой функции.
Ну, я и говорю: если не зона ответственности
if err != nil { return nil, err }
if err != nil { return nil, fmt.Errorf("%w: some package error", err) }
если зона ответственности: if err != nil { // do something }
И в одном случае там может быть просто варнинг, потому что данные из файла вспомогательные и у нас есть какое-то дефолтное значение. А во втором случае надо паниковать, потому что данные из файла необходимы.
А вот тут вы, внезапно, не очень умную вещь сказали.
"потому что данные из файла" - тут, как минимум, "из файла" - лишнее.
Бизнесовой логике нужны данные, ей не важно, откуда они взялись. Если для бизнесового слоя, почему-то важно, из файла данные пришли, из кеша зачитались или по сети запросом получены - это серьезный повод пересмотреть архитектуру.
А дальше, собственно, от требований пляшем.
Если, допустим, вызывающей функции надо четко отличать ситуацию "все пропало" от "говно пришло какое-то (например, не парсится, или еще какая беда)" - ну так у вас функция, которую мы вызываем, должна вернуть соответствующую информацию. Если не возвращает - функция не подходит, ищем/пишем другую.
Так что обычно как-раз пробрасывают ошибку от "утилитарной" до какой-то более "сведущей" функции.
Ну вы же понимаете, что идея о том, что где-то наверху есть функция, более "сведущая" в том, что делать с конкретной ошибкой, которую возвращает какой-то парсер из-за ошибки в какой-то строке какого-то файла, находящегося за 4-мя слоями абстракции от этой "сведущей функции"... Ну, мягко говоря, утопичная идея. Вам не кажется?
Я хз, может в го не так, там свой way, надо всегда паниковать или nil вместо значений возвращать
Паниковать надо примерно никогда, ошибки обрабатывать в месте их возникновения, если пришла ошибка, в значение рядом с ней не смотреть. Все, набор рекомендаций закончен.
, но почему тогда я в го-коде вижу if err != nil { return err } ?
Потому что таков путь, и в 99% случаев этого достаточно. Можно бы было забить, но все еще есть 1%, где этой конструкции не хватает. Ну да, такова жизнь, более удачных решений не наблюдается (и не надо опять поднимать песню про исключения. Они а) сами далеки от идеала, б) полноценно нереализуемы в парадигме Go (а эрзац - нафиг не нужен).
Давайте сойдемся на том, что хороший коммерческий продукт с гораздо большей вероятностью появится в условиях рыночной конкуренции. А то как агитка выглядит.
не, это не про "возможность роста" в том смысле, что могут вырасти. Там про то, что "бла-бла-бла, все дорожает, но нельзя исключать, что провайдеры просто задирают цены при любой возможности"
Оппортунистический рост цен - это типа "повышаем, потому что можем". Сложились условия на рынке, что у конкурентов либо нет аналога, либо миграция очень дорогая - поднимаем цены, потому что "а куда ты денешься с подводной лодки"
Если вы не против, покритикую:
Что такое "оберточный функционал над потоком"? Оно мало того, что не понятно, так еще и искажает суть явления.
A goroutine is a lightweight thread managed by the Go runtime.
Легковесный тред, управляемый рантаймом Go. Это не обертка над потоком. Обертка над потоком - это Machine. Горутина - это штука, которая исполняется на этом потоке.
Планировщик операционной системы не имеет прямого отношения к "Машинам". Он тупо запускает N (GOMAXPROCS) системных тредов, в которых они потом так и живут, ничего он не переключает. А вот планировщик в рантайме Go распределяет горутины по машинам, на которых они исполняются.
Не совсем так. go func() не запускает функцию, а всего лишь ставит горутину в очередь исполнения, передает ее планировщику, чтобы он по возможности отдал ее одной из "машин" на исполнение.
Собственно, поэтому следующее тоже не совсем верно:
В вашем случае с некоторой ненулевой вероятностью горутина вообще не выполняется, тупо в очереди находится (тем более в go playground, который, возможно, вообще однопоточный)
Вот тут & излишне. Дальше ваша wg передается замыканием, а замыкание в Go проходит по указателю.
В переводе это означает "взаимное исключение"
Это вы так synchronization primitive перевели? Не "некая примитивная синхронизация", а "примитив синхронизации". Как, к слову, и WaitGroup и даже, внезапно, канал.
Ну вы бы хоть рассказали, почему не сможет. Это примитив синхронизации. Он заблокирует горутину (да, он не значение блокирует, а горутину), пока инструкция не сможет быть исполнена.
Т.е. если вы лочите мьютекс из горутины №1, а потом пытаетесь сделать то же самое из горутины №2, то вторая горутина заблокируется, пока первая не разлочит мьютекс.
Кстати, раз уж вы решили разложить все по полочкам, то примитивы синхронизации надо было начинать с атомиков. Потому что у мьютекса и вейтгруппы внутри как раз они и есть.
Ну вот, в формулировке read-modify-write, а в примере ниже save/load. Не надо так.
Ну и вот это "Атомарная операция — операция, которая либо выполняется целиком, либо не выполняется вовсе" - это скорее к теории баз данных формулировка, и это про транзакционность.
В трактовке многопоточных приложений - a sequence of instructions that are executed as a single, indivisible unit of work
Последовательность инструкций, исполняемых как одна неделимая единица работы. Т.е. это гарантия, что в процессе выполнения не произойдет переключение контекста.
Например i++ не атомарна. Это получение значения переменной + увеличение значения на 1 + запись значения в переменную - 3 операции, соседний поток может вклиниться между любой из них, что приведет к неконсистентности данных. Атомарные операции отличаются тем, что соседний поток вклиниться не может, они для того и нужны.
Ничего не понятно. Технически канал - примитив синхронизации, по сути - семафор.
Канал нужно закрывать. Просто за правило возьмите: как только записали в канал все, что хотели - закройте канал.
В смысле, заблокировав доступ к чтению??? Чтение из закрытого канала возвращает zero-value для типа канала. С чего оно заблокируется-то?
Ну доку-то почитайте! false означает, что канал закрыт (и все значения из него прочитаны, в случае буферизованного канала). Никаких невозможно читать там нет, даже напротив, читать из него очень даже возможно.
А без range'а нельзя? Данные в буфере пропадают? Что вы несете?
Дичь. Эталонная. Дедлок - это ситуация, когда все горутины находятся в спящем состоянии и не могут быть "разбужены", тчк.
Гениальное определение. Неверное, ничего не объясняющее, но гениальное.
Вот такое будет ближе к истине:
Примитив синхронизации, блокирующий поток исполнения, пока один из case'ов не сможет быть выполнен.
Достаточно было одного источника, https://go.dev/doc/
вероятно, будет. Планировщик же.
В любом случае, меньше - абсолютно точно не будет. Насколько больше - мерить надо. Возможно, единицы процентов, а может быть, и десятки.
пользователи меняют процессоры в ноутбуках? А вы точно программист?
У вас "(тут есть специфика, конечно)" отклеилась. Правильно вот так:
Можно и я покритикую?
В заголовке - про извлечение данных из Linux, а в содержимом - gps, уровень wifi, уровень батареи и свободное место на диске.
Половина этого к Linux имеет весьма опосредованное отношение, вторая - вообще кроссплатформенными кутишными апи решается. И все это вместе достаточно сложно назвать прям уж "данными".
Признаться, ожидал какого-то сурового парсинга терабайтных файлов или анализа гигабайтных чанков логов ядра...
Фигня там, а не e2e. end-to-end с MitM в комплекте
для интереса: чем вызвано "нехотение"? Отторжение cvs как таковых, или просто другая cvs'ка была "ближе к телу"?
может, пограничные сети?
не бывает так. Любые отчисления идут последовательно. Если 30% от суммы продаж, то Valve будет брать свою долю от уменьшенной суммы, комиссия за перевод будет от суммы перевода и т.д. С российской стороны налог будет браться уже от того остатка, который пришёл на счёт. Дважды обложить отчислением одну и ту же сумму нельзя
Крутая штука. Интересно, когда до домашнего сегмента 3д печати доберется?
Машинный перевод, шуточки за 300 и отсутствие вменяемого ответа на вопрос из первого абзаца.
Вы серьезно? Это же просто подборка инфоцыганских курсов! Что это делает на хабре?
Не совсем. tokio::spawn - паттерн actor, go - csp.
Вот тут есть сравнение подходов: https://en.wikipedia.org/wiki/Communicating_sequential_processes
Прям раздел есть Actor vs CSP
Вкратце:
актор - управляемый, CSP - нет.
csp - обмен сообщениями эксклюзивно через каналы
actor позволяет отправку сообщения с отложенной обработкой, csp - блокирует отправку, пока читатель не готов к получению.
Из побочек: Rust, видимо, стектрейс ломать не дает. Т.е., фактически, мы не можем завершить выполнение вызывающего метода (как минимум стек вызова чистить нельзя), пока заспавненный не завершился + мы иерархию задач имеем. В Go - ипанул и забыл.
tokio:spawn
сложнее, чемgo
. Горутины легче.Да в смысле "неа"?
У вас async внутри tokyo::spawn! Буквально M асинхронных задач внутри одного потока, M:1 внутри 1:N.
Асинхронные задачи, которые будут выполняться внутри этой таски, исполнятся ровно в том потоке, в котором таска запущена!
В Go - любая асинхронная задача может выполниться на произвольном потоке.
async при этом явно говорит "работаем в текущем потоке".
Присмотритесь внимательно:
tokio::spawn - 1:N
async {} - M:1
Это не M:N, это, по сути, от thread::spawn отличается только наличием "легковесного потока".
То, что вы написали - это "запустить поток, внутри которого выполнить асинхронный код". Т.е. это будет N раз по M:1. Это не то же самое, что M:N.
Просто для ясности ситуации:
Допустим, вы спавните (tokio::spawn) 2 потока, должны дождаться выполнения обоих и с полученными данными дальше что-то делать. В первом вы асинхронно запускаете 10 задач, во втором - 2. Допустим, время выполнения асинхронной задачи константно, берем за единицу.
За сколько выполнится ваш код? Очевидно, за 10 (потому что 10 задач на одном потоке выполнялись, а второй выполнил 2 и благополучно сдох).
Теперь представим, что у нас 2 потока доступно гошному рантайму (на самом деле мне, как разработчику, откровенно насрать, сколько их там, я могу и имею право даже не знать). Мы запустили 10 горутин в одном блоке и 2 - в другом. Итого 12. Делим на 2 потока - сделали за 6.
Все, на этом "фишки" языка Go окончены. Он создан ровно ради этой фичи. Во всех остальных сценариях Rust круче, C# круче, Java кручке, все круче (кроме, простихоспади, PHP, Basic'а и "Языка программирования 1С" ).
Да, токио - мультитредовый рантайм, и нет, оно не M:N. Оно 1:N.
async/await - M:1, tokyo - 1:N. M:N, кроме Go, из коробки, вроде, Haskell только (или Erlang, не помню, не большой специалист в хаскелях и эрлангах) и умеют. Собственно, весь язык ровно из плясок над "M:N с человеческим лицом" и состоит. Все, что мешает этой концепции - совершенно обоснованно идет лесом. Исключения, например, одна из вещей, которая мешает.
Типизация, в общем смысле, не мешает. А try/catch, ? и прочие примитивы обработки исключительных/ошибочных ситуаций - мешает. Я вот про это говорю.
Дык, там и M:N нету. Другие языки решают другие проблемы.
Я бы уже давно забил, если бы вы мне в каждом первом комментарии не рассказывали, какие они классные в C#, и почему в Go они прямо необходимы.
Все же, еще раз про исключения: в го они этим самым мусорным бойлерплейтом и станут, без вариантов.
А растаманский подход с Result/Optional и т.д... ну, в виде дженериков - хоть сейчас пиши. Полноценная реализация на уровне языка - кортежи мешают (они слишком дофига свободы дают)
Еще раз говорю, это не проброс ошибки, а возврат значения.
Проброс - отдельный флоу, предполагающий наличие перехватчика выше. Значение отличается тем, что его можно законно проигнорировать. Go - это не просто "другой синтаксис", его отличия от других языков несколько глубже. И эти отличия накладывают некоторые ограничения, с которыми приходится жить.
И это не хорошо и не плохо, это просто цена 100-тысячных RPS.
В том и дело, что Go явным образом утверждает, что любой код может запускаться в горутине. А еще Go настаивает, что ошибка - это значение, а значит, может быть проигнорирована. В случае запуска в отдельной горутине, более того, будет проигнорирована.
Поинт в том, что код внутри функции, по определению, должен быть готов к ситуации, что ошибку (или любое другое возвращенное значение) могут проигнорировать. Поэтому никакой сценарий работы функции не может приводить к панике (кроме тех случаев, когда паника явным образом нужна) или исключительной ситуации.
На выходе мы имеем 2 сценария групповой обработки ошибок/исключительных ситуаций:
обработать те, с которыми мы знаем, что делать, остальные уйдут по стеку наверх (exception-style) с падением приложения при выходе за границы стека - неприменимо в Go, т.к. наличие "наверх" никто не гарантирует. Мы вырождаемся в обязанность никогда не возвращать ошибки, что выглядит как бред.
гарантировать обработку всех ошибок (назовем это rust-style) - ну, какбэ, бойлерплейт по обработке рискует превратиться в портянку такого размера, что
if err != nil {}
покажется цветочками.Фичи завозят, стоит признать, аккуратно, дозированно, и только те, которые признаны удачными.
А вот тут - не всегда. Смотрел списки фич в Python'е, чейнджлоги того же PHP... А работать когда?
Если знает, что с ошибкой делать - обработать на месте. Если не знает - обернуть своей ошибкой и вернуть. Панику - а зачем? Ну, если нужна паника - кинуть панику. В чем проблема-то?
Ну, я и говорю:
если не зона ответственности
if err != nil { return nil, err }
if err != nil { return nil, fmt.Errorf("%w: some package error", err) }
if err != nil { return nil, errors.New("error parsing string") }
если зона ответственности:
if err != nil { // do something }
А вот тут вы, внезапно, не очень умную вещь сказали.
"потому что данные из файла" - тут, как минимум, "из файла" - лишнее.
Бизнесовой логике нужны данные, ей не важно, откуда они взялись. Если для бизнесового слоя, почему-то важно, из файла данные пришли, из кеша зачитались или по сети запросом получены - это серьезный повод пересмотреть архитектуру.
А дальше, собственно, от требований пляшем.
Если, допустим, вызывающей функции надо четко отличать ситуацию "все пропало" от "говно пришло какое-то (например, не парсится, или еще какая беда)" - ну так у вас функция, которую мы вызываем, должна вернуть соответствующую информацию. Если не возвращает - функция не подходит, ищем/пишем другую.
Ну вы же понимаете, что идея о том, что где-то наверху есть функция, более "сведущая" в том, что делать с конкретной ошибкой, которую возвращает какой-то парсер из-за ошибки в какой-то строке какого-то файла, находящегося за 4-мя слоями абстракции от этой "сведущей функции"... Ну, мягко говоря, утопичная идея. Вам не кажется?
Паниковать надо примерно никогда, ошибки обрабатывать в месте их возникновения, если пришла ошибка, в значение рядом с ней не смотреть. Все, набор рекомендаций закончен.
Потому что таков путь, и в 99% случаев этого достаточно. Можно бы было забить, но все еще есть 1%, где этой конструкции не хватает. Ну да, такова жизнь, более удачных решений не наблюдается (и не надо опять поднимать песню про исключения. Они а) сами далеки от идеала, б) полноценно нереализуемы в парадигме Go (а эрзац - нафиг не нужен).