Как стать автором
Обновить

Комментарии 207

Все равно подчеркивание влепить проще, чем писать обработчик. Так, что проблема решена не до конца;)
Так и задача направить, а не заставить любой ценой. Фокус в том что с подчеркиванием это место получается выделенным и если что на него проще обратить внимание и заподозрить неладное
Ну и пропуск значений при присваивании это стандартная конструкция языка, тут все соответствует принципу заложенному в Go — чем проще тем лучше.
«Влепить» — да, но это бросается в глаза и создает дискомфорт. К примеру, «не проверить код возврата в С» — не создает дискомфорта и не бросается в глаза — потому и используется повсевместно :)
Зависит от программиста. Мне например не комфортно писать на C не проверяя коды ошибок. Возможно причина в том, что я много занимался разработкой POS-терминалов, где как и в других финансовых приложениях лучше перебдеть, чем недобдеть.
Интересное замечание. В этом как раз вся суть — на С можно великолепно разруливать ошибки, равно как и в любом другом языке, и любым другим способом. Те же исключения — уверен, что какая-то часть читателей статьи напишут «да вы просто не умеете работать с исключениями».

Но в этом и соль — если с одним инструментом, для того чтобы «делать правильно» нужно набраться опыта в течении 5-ти лет и прочитать 3 талмуда от зубров computer science, а с другим — достаточно просто взять и начать пользоваться инструментом — то второй вариант будет эффективнее в долгосрочной перспективе.

Попробую иначе сформулировать — научиться можно всему, любой технике любой сложности. Но в общем случае, люди будут выбирать более простой путь и более простые инструменты. Go позволяет не читая талмуды писать качественный и надежный в плане обработки ошибок код. Не инвестируя дополнительных усилий именно в этот аспект.
Просто взять и начать пользоваться не получится, этот ньюанс с обработкой ошибок является дополнительным порожком сложности который надо будет преодолеть. То есть чтобы начать писать программу сначала нужно подучить теорию, а люди достаточно ленивы…
к сожалению пока не изобрели клонирование людей, поэтому нужно брать «в среднем по больнице».

В Go попроще чем с кодами возврата — возвращается по сути текст ошибки и в большинстве случаев ее просто перекидываешь наверх, плохо что не всегда в сторонних либах реализована возможность нормально определить что именно за ошибка произошла.
Вот тут Dave Chaney интересную методологию авторам библиотек предлагает — dave.cheney.net/2014/12/24/inspecting-errors — для популярных типов ошибок (вроде Timeout или Temporary Error) реализовывать соответствующие интерфейсы, а не просто передавать значения. Не знаю, правда, подхвати ли эту идею кто-то или нет.
понятно, что error это интерфейс и никто не мешает передавать значения с более широким набором методов, тоже самое используется и в языках с исключениями — через создание кастомных классов от базового исключения.
Я в простеньком бинарном парсере это придумал года полтора назад — возвращал в случае сбоя свой RecoverableError, где хранилась информация, откуда начинать поиск нового фрейма. Вроде нормальная идея.

Но тот проект все равно хочется переписать на rust.
Это от того что у вас есть опыт, сначала составляется алгоритм со всеми нюансами а потом он реализовывается. Но многие люди делают не так, они не составляют алгоритмы заранее — они сразу пишут программу, и само собой — обработка ошибок очень быстро начинает сбивать с мысли и мешать поэтому откладывается на потом. А мы сами знаем что бывает с «на потом».
Интересно, а как в Go будет написан код, когда подряд идут несколько операций, которые могу вернуть ошибку?
Например:
file1, err := os.Open("test1.txt")
file2, err := os.Open("test2.txt")


Нужно каждый раз проверять что err не null?
С try/catch будет всего один catch блок.
да каждый раз нужно проверять, с общим try/catch и поведение разное, тут выполнение не прервется если есть ошибка
Да, поведение разное. Ну это аргументированно тем, что если ошибка произошла (в Java это называется исключением) то и не нужно дальше продолжать работать — типа, это не нормальное поведение, поэтому дальше код выполнять не нужно. А в Go какая философия по этому поводу? Если метод выполнился с ошибкой, то дальше можно работать? Это я почему спрашиваю — часто в try/catch происходит работа с методами, которые могу вернуть ошибки и если каждый раз проверять возвращаемое значение, то будет долго. Например операции ввода/вывода — они практически все могут вернуть ошибку.
Лично я, в случаях когда ошибка не позволяет далее продолжить работу программы, пишу следующий однострочник:
if err != nil {log.Fatal("Some error description: ", err)}
Т.к. я пишу на го обычно маленькие утилитки, то необходимости в инструментах форматирования не возникало. Конечно по культуре пример надо писать в три строчки, но в коде, в котором работаешь один, имхо вполне можно не чураться использовать подобные срезания углов.
Просто это часть философии языка — что код все форматируют одинаково
Нет, лучше себя приучать писать код так, как будто он выложен в паблик и на него смотрит весь мир. Хорошая практика.
А 'go fmt' включенный по-умолчанию при сохранении исходника — этому помогает, правда. Попробуйте, через недельку не поймете, как можно было без этого жить :)
Я понял, что без этого нельзя жить уже после третьего сохранения файла :)
Нет-нет, не так. Исключение — это не «ошибка в Java» — это метод сообщить вышестоящей функции о том, что вызываемая функция завершилась с ошибкой. Важно не путать обработку ошибок и их передачу.

Для *исключительных* ситуаций — когда вот точно все, капец наступил и программа дальше продолжаться не может, есть механизм panic()/recover() — blog.golang.org/defer-panic-and-recover. Его можно использовать как замену исключениям, но так почти никто не делает — это плохая практика.
«Исключение — это не «ошибка в Java» — это метод сообщить вышестоящей функции о том, что вызываемая функция завершилась с ошибкой»

Не только, исключения ещё и очень удобный способ сократить число проверок. Если нужно вызвать 10 функций для выполнения задачи, каждая может вернуть ошибку, и обработка любой ошибки одинакова — оборачиваем в try {} catch() {} finally{}. Да и исключения внутри блока try {} могут возникнуть не только по вине функций, я сам могу их там бросить если понятно что не нужно продолжать выполнение этого блока.

В вашем примере с go ситуация выглядит не очень хорошо в плане обработки ошибок. Имхо, исключения намного более гибкий способ, учитывая типизацию, наследования и прочие плюшки ООП, применимые к исключениям. И если не хочется пропускать ошибки — декларируйте все функции как «throws Exception» — и пропустить ошибку компиятор уже не даст, придется или обрабатывать или кидать далее.
Ну, про «сократить» число проверок уже обсудили комментом ниже. Код, в котором вызывается 10 раз подряд одна и та же функция, ошибки от которой нужно обрабатывать абсолютно одинаково — то да, исключения дают более читабельный код. Нюанс в том, что такой код редко встречается, и если и встречается — то это повод для рефакторинга.

В большинстве реального кода, ошибки функции обрабатываются не так уж одинаково. а создавая для каждого типа ошибок свою блок обработки — количество кода в сравнении с явным возвратом ошибки не уменьшается, зато уменьшается читабельность программы.

Под «читабельностью программы» я подразумеваю количество времени и усилий необходимых понять, как именно работает код, при этом, как уже упоминалсь в некоторых комментариях — важно понимать, что «обработка ошибок» — это неотъемлимая часть программы, а не опциональная.

Например, когда я смотрю на чужой код в Go — я сразу вижу, что он делает и как себя ведет, когда есть ошибка и когда нет. Мне не нужно листать вниз и вверх, прыгать по файлам и склеивать прочитанное в голове. В коде написанном с помощью исключений, мне нужно дополнительное усилие, чтобы составить полноценную картину — как и что происходит в случае ошибок.

Но главная беда все же в другом — а именно в том, что вот эта легкость «не обрабатывать ошибку, а просто выбросить эксепшн или разобраться потом» создает стимул для того, чтобы именно так и поступать. Именно поэтому для правильного использования исключений в реальном мире вам нужны «программисты со стажем не менее 5 лет» и толстые книжки. И именно поэтому большая часть кода, который я видел в своей жизни — использует исключения далеко не так эффективно, как это описывают адепты исключений.
Код, в котором вызывается 10 раз подряд одна и та же функция, ошибки от которой нужно обрабатывать абсолютно одинаково — то да, исключения дают более читабельный код. Нюанс в том, что такой код редко встречается, и если и встречается — то это повод для рефакторинга.


Как раз таки очень часто встречается. Представьте себе например, что вы работаете с базой. Или с сетью. И вот у вас кусок кода который в каждой строчке в эту базу или сеть лазит. И из каждой строчки потенциально может вылететь IOException.

Не имея возможности завернуть этот кусок кода в единый try catch, проверяя ошибочный результат на каждую операцию, вы просто задолбаетесь.

А код распухнет вдвое, его логику будет сложнее читать из-за этих if err != nil на каждой второй строчке.
Спасибо за ссылку.
Но вообще этот вопрос настолько важен, что мне кажется в статье про «серебряную пулю для обработки ошибок» нельзя это не упомянуть. Это я, разумеется, не Вам, а автору статьи.

Что касается решения, которое изложено в ссылке, то оно не является полным решением проблемы

1. Что делать, если мне все-таки нужно поведение, когда, при возникновении ошибки в очередной строчке, мне нужно сразу же завершить выполнение фрагмента? А отложить ошибку в сторону и проверить в конце нельзя, так как остаток фрагмента кода будет работать с битыми данными, а это плохо.

2. Предлагаемый в ссылке паттерн на самом деле сводит основное рекламируемое свойство Go на нет: программист в этом паттерне волен легко проигнорировать ошибку. То есть это уже не будет иметь никаких принципиальных преимуществ над обычными кодами возврата.
Что делать, если мне все-таки нужно поведение, когда, при возникновении ошибки в очередной строчке, мне нужно сразу же завершить выполнение фрагмента?
вот я тоже несколько раз пытался задать тут этот вопрос, но как-то не получилось. у пайка в статье тоже рассмотрены несколько вариантов, но не этот.

насколько я понял из комментариев, единственный выход в этом случае — это оформлять фрагмент в отдельную процедуру, и после каждого вызова, который может вернуть ошибку, писать что-то вроде if r.err != nil { return }

Дело в том, что этот вариант является, как мне кажется, основным разумным поведением по умолчанию. И именно так работает обработка ошибок на исключениях, люди к этому уже привыкли.

После каждого вызова писать if r.err — это не выход, это принять страдания и смириться. Но ведь тут речь идет об удобстве, о том что простой и удобный синтаксис будет подталкивать программистов к правильному написанию программ.

Я всецело согласен с тезисом, что простота и удобство синтаксиса является мощным мотиватором для программистов, но я вижу, что обработка ошибок в Go создает огромную проблему, которая перекрывает все преимущества такого подхода. И решения этой проблемы я пока не вижу. Поэтому исключения для меня пока что выглядят куда проще и удобней, чем этот механизм.
в go есть исключения они называются panic/recover, но для того чтобы вернуть предусмотренную ошибку внешнему коду, нужно использовать error
то есть когда вы пишите либу — на выходе состояние нужно возвращать в виде error, а именно взаимодействие с внешним миром, не нашли что-то в базе и т.п. это все error.
panic на уровне между пакетами это синоним неправильно написанной программы, значит вы с этим пакетом взаимодействуете не правильно.
внутри одного пакета вы можете использовать panic как аналог исключений, если без него вам никак, просто гарантируя что вы его перехватите и внешний код получает результат нормальной работы только в виде error
Ну я так понял, что panic — это не основная схема, а вспомогательная. Что по задумке авторов языка panic нельзя злоупотреблять, что основным способом обработки ошибок должен быть error.

Но если оказывается, что основная схема неудобна, то есть опасность что пользователь перейдет на вспомогательную, но более удобную для него.

Например, на все нужные ему стандартные пакеты напишет врапперы, которые превратят error в panic, забьет на инновацию языка авторов и будет работать по старому, с исключениями. Можно даже сделать библиотеку error2panic, выложить в opensource и она будет пользоваться популярностью.

Да, это хорошо, что язык предоставляет возможность обойти авторскую задумку и работать по старому, но мне все-таки интересно, можно ли удобно пользоваться новым способом.
внутри одного пакета вы можете использовать panic как аналог исключений
… ровно до того момента, пока все, или хотя бы большинство процедур, которые вы зовёте при таком подходе — ваши собственные, логика которых тоже реализована через panic.

Как только речь заходит о чужих библиотеках — этот подход ломается.

Собственно, на мой взгляд, неоднократно упомянутая статья Пайка грешит тем же — вы можете делать так, можете эдак вместо тупого написания после каждой строки «if err != nil», но это всё для местного употребления, грубо говоря, в пределах одного пакета. А наружу передавайте ошибки стандартным образом. В итоге каждый кодер каждый раз будет изобретать заново способ написания кода, чтобы не снижать его читаемость однотипными «if err».

Один вопрос интересует — согласны ли вы с тем, что описанная ситуация (когда при возникновении ошибки остаток фрагмента кода просто не нужно выполнять, а обработка возникающих при этом ошибок однотипна) является наиболее частой?
вопрос на самом деле другой. Я уже много раз написал зачем нужны error зачем panic.
panic для отработки состояния это плохо, реально плохо и давайте на примере токенизатора html разберем почему:
go.googlesource.com/net/+/master/html/token.go
от читает поток, тоесть чтение любого байта может закончится ошибкой.

Так вот условия с проверкой ошибок в этом файле встречается около 50 раз, это примерно 0.5% от всего файла или 1-1.5% от кода. (тысячи строк проверок ага)
и большинство из них содержат код который выполняется если эта ошибка произошла, то есть это всё были бы блоки try/catch, который делают throw в конце catch
так чем же в данном случае исключения были бы лучше

а теперь про плохо:
что будет если над проектом работаете не вы один, а команда и когда кто-то изменит порядок обработки, и вызовет метод который генерирует исключения, или вызывается метод который вызывает метод который генерит исключение?
правильно программа посыпется вся, потому что это исключения ДОЛЖНЫ были поймать но не поймали. Таким образом программа которая должна не падать, покрывается блоками try/catch где нужно и где не нужно.
А когда состояние обрабатывается вместе с результатом и не отделимо от него, сложнее допустить ошибку.
Кстати когда говорят Go вас не заставляет обрабатывать ошибки, всегда можно написать _ — так это защита от ошибки, а не от диверсии.
большинство из них содержат код который выполняется если эта ошибка произошла
о, теперь наконец-то я вроде понял.
да, наверное вы правы. throw внутри catch и метастазы try по всему коду ничем не лучше.
Так вот условия с проверкой ошибок в этом файле встречается около 50 раз, это примерно 0.5% от всего файла или 1-1.5% от кода. (тысячи строк проверок ага)
50 раз, судя по исходнику большинство условий породили 3 строки или более, это 150 строк или 12% от всего файла (1219 строк), кроме этого сделаны конструкции что-бы хранить/передавать эту ошибку (z.err), т.е. ещё +х%
При этом не происходит защиты от ошибок в коде (чтение чужой памяти и т.п.)

большинство из них содержат код который выполняется если эта ошибка произошла, то есть это всё были бы блоки try/catch, который делают throw в конце catch
Где это? Там у большинства `z.err` стоит return/break, да и вообще зачем продолжать если чтение закончилось ошибкой.

читает поток, тоесть чтение любого байта может закончится ошибкой.
Теперь посмотрим как бы могло выглядеть это с try-catch: В лучше случае тут try-catch вообще не нужен, а использование могло бы выглядеть так:
try:
    for item in NewTokenizerFragment(stream):
        process(item)
except ErrorToken:
    log('Ошибка в данных')
except IOError:
    log('Ошибка передачи данных')
except Exception:
    log('Прочие ошибки')

Тут мы итератором получаем данные которые удалось обработать, при это происходит защита от «чтение любого байта может закончится ошибкой», защита от ошибок в библиотеке, и для информативности — ошибка в данных ErrorToken.

Меньше кода, больше защиты (надежность).
> При этом не происходит защиты от ошибок в коде (чтение чужой памяти и т.п.)
какой чужой памяти?! что за фантазии

> Где это? Там у большинства `z.err` стоит return/break, да и вообще зачем продолжать если чтение закончилось ошибкой.
650, 669, 696, 706, 721, 735, 849, 918, 932

> Теперь посмотрим как бы могло выглядеть это с try-catch
это из серии, как нарисовать сову, вот кружок и вот уже всё классно
я вам про то что внутри NewTokenizerFragment, вы мне про то как его вызывать и как классно это с иключениями

> Меньше кода, больше защиты (надежность).
Тоесть чем меньше ошибок мы обрабатываем, тем больше надежность… ну ок
Ну и про память:
если это происходит значит вы неправильно написали программу и в этом случае в Go будет сгенерирована паника, которая работает в точности как исключения только с оглядкой на Go (будут вызваны defer при раскручивании стека вызовов)
то есть в Go разделена обработка тех ошибок которые должны происходить (например io) с теми что не должны происходить («чужая память», выход за границу массива)
потомучто когда у вас сеть упала программа должна это пережить, она должно отреагировать на это (начать по таймауту делать повторы и тп.), а когда у вас она начинает в «чужую память» лазить — то вы уже не можете просто подавить это исключение просто написав except Exception: log('Прочие ошибки') — вы не знаете что еще сломано и что вы сломаете если начнете дальше работать с этими данными и реакция по умолчанию это свалить всё приложение.
Как я уже писал это не противопоставление только исключением это та практика которая годами существует в C++ одновременное использование кода возврата и исключений, как раз для нормальной работы и не нормальной
я вам про то что внутри NewTokenizerFragment, вы мне про то как его вызывать и как классно это с иключениями
Читайте внимательнее, про то что внутри NewTokenizerFragment я написал — `В лучшем случае тут try-catch вообще не нужен`.

Тоесть чем меньше ошибок мы обрабатываем, тем больше надежность… ну ок
Наоборот, чем большее «покрыте» кода мы делаем тем надежнее, что и делает подход try-catch.

650, 669, 696, 706, 721, 735, 849, 918, 932
Ну вот ваш первый пример (649), при ошибке происходит return (651), при этом обработка не продолжается (653), идем в родительскую ф-ию там опять return, в следующей род. ф-ии опять return — т.е. при ошибке идет прямой выход наружу при этом ошибка хранится в z.err
То что тут есть 650, в подходе с try-catch этой строки не будет либо будет по другому/в другом месте, как вариант можно просто инкрементировать значение на каждом чтении, либо у ридера можно будет узнать последнюю позицию где он остановился, либо сам ридер будет в except-объекте передавать позицию, и т.п.
Теперь давайте сравним с библиотекой html5lib которая тоже парсит html: github.com/html5lib/html5lib-python/blob/master/html5lib/html5parser.py
всего 2 try-except на 2700 строк, это 0.2% «лишнего кода» против 12% в исходнике выше.
> После каждого вызова писать if r.err — это не выход
не все вызовы могу содержать ошибку, в том и фишка вы обрабатываете только то что должно и будет содержать ошибку, вместо того чтобы забить на всё и падать до самой main
Если лишь 1 вызов из многих содержит ошибку, то конечно проблемы нет.

Однако бывают код, и это отнюдь не редкость, а вполне распространен на практике, в котором интенсивность содержания ошибок высокая. И с этим тоже надо что-то делать. Собственно я именно про такой код и задал вопрос.

В случае исключений до самой main падать не нужно. Обычно используется паттерн с большим try catch, наложенным на какую-то высокоуровневую операцию, который ловит все подряд. Ну тот же пример с web server, который генерит страницу. В процессе генерации страницы может случится всякое, ведь он же в том числе ходит за данными и во внешние источники. Но что бы ни случилось, обработка будет одна и та же: пользователю отдаем страницу 500, а техническому специалисту печатаем текст ошибки в лог.

Паттерн отлично работает, причем web сервер лишь пример, его можно применять еще много где.

Вот это действительно удобный подход, который позволяет программисту писать простой линейный код и снимает с него головную боль обработки бесчисленных fail path, которые случиться во время генерации. Соотношение код обработки ошибок/основной код сведено к минимуму.
ну и поводу предложенного паттерна errWriter вы проверяете r.err после 10 вызовов write, а не каждого, обертка в виде errWriter гарантирует что когда ошибка произойдет на 2м вызове 8 последующих не будут ничего делать и после них вы получите ошибку из 2 вызова и обработаете ее нормальным порядком
Но вообще этот вопрос настолько важен, что мне кажется в статье про «серебряную пулю для обработки ошибок» нельзя это не упомянуть. Это я, разумеется, не Вам, а автору статьи.

Только увидел ваш комментарий. Я, вероятно, еще не научился доносить ясно мысль, но называть статью статьей «про серебрянную пулю для обработки ошибок» я считаю оскорблением :)

Статья абсолютно не о том, что тот или иной метод — серебрянная пуля. И в комментариях выше я явно дал понять, что да, есть частные случаи, когда краткосрочная выгода от использования исключений больше. Но её недостаточно, чтобы перекрыть долгосрочные недостатки.
Ну она так воспринимается. Что есть старый путь, через исключения, «неправильный». А есть новый, «правильный». Который вроде как благодаря легкости и простоте должен подтолкнуть программистов к правильной обработке ошибок.

А комментарии же уже после идут, отдельно. Извиняюсь, если оскорбил.
Ну, я там смайлик специально поставил, чтобы понятно было, что это как-бы шутка )

Но теперь ваша версия звучит более удачно — один метод более эффективный(«правильный») в долгосрочной перспективе, чем другой.
Но это не то же самое, что метафора «серебрянная пуля», согласитесь.
Ну почти тоже самое. Серебряная пуля — метод который решает существующие проблемы гораздо эффективнее, чем предыдущие методы. Представляем java — новый язык, на котором мы будем писать программы в несколько раз эффективней, чем на допотопном C++. Представляем обработку ошибок в Go — мы будем обрабатывать их гораздо эффективнее чем в предыдущих языках. Метафора в общем то про это.

С долгосрочностью/краткосрочностью совсем не понятно. Что является осью времени, на которой мы измеряем эту долгосрочность? Работа программы? Жизнь программиста? Развитие IT индустрии?
Из перечисленного, скорее, «Развитие IT индустрии». Go не «представляет» какой-то уникальный метод обработки ошибок — но дизайн этого аспекта (под которым подразумевается не только наличие и форма определенных фич, но и отсутствие других) создает стимул для более внимательной обработки ошибок и стимулирует в головах программистов переход от «ошибки — это что-то опциональное» до «всегда проверять/хендлить ошибки».
А стимул умноженный на миллионы человеко-часов приведет к более качественной обработке ошибок в целом, к меньшему количеству сбоев и к повышению культуры программирования в сумме.
Время покажет.
Мне этот подход скорее напоминает checked exceptions в java.

То бишь есть обычные exceptions, а есть checked exceptions.
Есть обычные коды возвращаемых ошибок, а тут checked коды.

В обоих случаях авторы фичи преследуют благую, казалось бы, цель: сделать возврат ошибок более явным, чтобы было понятно какие ошибки откуда берутся и их было сложнее проигнорировать.

Но сейчас, годы спустя, checked exceptions считается неудавшимся дизайном. Например, в том же C# от них осознанно отказались.

Здесь очень похожая ситуация, форсирование обработки, только механизм передачи сделан не через исключение, а через возвращаемое значение.
Возможно, но в пользу решения в Go есть два момента:

1) (объективный) — авторы Go прекрасно ознакомлены с проблемами созданными checked exceptions, и об этом в одной из статей по ссылкам выше написано, У меня есть огромный кредит доверия этим товарищам, которые, в конце-концов, написали Unix, поэтому я предполагаю, что они знают, о чем говорят.

2) (субъективный) — я вижу изменения по себе. До Go я считал обработку ошибок чем-то второстепенным и опциональным, отвлекающим и мешающим. В языках с исключениями соблазн «просто-выкинуть-исключение-и-забыть» был столь высок, что я это делал везде. Да, можно сколько угодно говорить, что я просто плохой программист и мне нужно было прочитать пару толстых книжек и научиться правильно работать с исключениями — но это то, что было в реальном мире — люди идут простым путем, и я не исключение. И вот только с Go мне это осознание пришло естественным путем, просто потому-что язык так сделан, и не случайно, а намеренно для этого.
До Go я считал обработку ошибок чем-то второстепенным и опциональным, отвлекающим и мешающим.

Думаю, что дело здесь совсем не в языке, а в решаемой задаче и опыте. Есть большое количество задач, где выбросил исключение и забыл не просто уместно, но еще и самый правильный способ. В других же задачах, где нужен более тонкий подход, независимо от языка нужно хорошее чутьё и точное понимание как будет происходить работа с ошибками и в какой мере, а это приходит только с опытом и от языка вряд ли зависит. Язык же в итоге дает нам инструмент для решения этих задач. Лично в моём восприятии, исключения чуть более удобные, чем обработка возвращаемых значений тем, что позволяет делать те же самые вещи проще.
> типизацию, наследования и прочие плюшки ООП
так и в Go error это:
interface error {
    Error() string
}

что там будет кроме этого реализовано уже на вашей совести

тут как раз и вопрос что когда у вас контекст перед глазами вам проще обработать его, напишите тот же вариант с двумя файлами на Java и попробуйте ответить на вопрос правильно ли все работает с учетом того что между try {} и catch {} будет кусок кода, вам как минимум придется скролить вниз и смотреть чтоже там написано, а так у вас весь код перед глазами и когда вы пишите так 50 раз за день при том же ревью если код написан по другому (нет закрытия файла например) у вас глаз уже зацепится за это

опять же по коду видно когда именно он может завершиться, тоесть когда вы вызываете чужой код из его вызова не видно может там быть исключение или нет, а в Go явно видно где точки выхода из метода
Не, не только для исключительных. Есть, например, парсер H264 SPS, где на BitReader-е последовательно примерно 30 раз вызываются 4 разных метода, из которых 2 точно могут вернуть ошибку и непременно вернут, если ты пропустил хотя бы одно чтение перед ними.

30 if-ов — нет уж, увольте, я выбираю плохую практику.
if _, err := foo(); err != nil {
	// ...
} else if _, err := bar(); err != nil {
	// ...
} else if _, err := spam(); err != nil {
	// ..
} else {
	// ..
}

Плохо?
ужасно. для трех уже ужасно, для тридцати трех будет просто убийственно. невероятное количество визуального мусора.

я сам не мог привести примера лучше.
Ну тогда несколько раз тут упомянутая обертка в помощь
Я уже ниже ответил, но повторюсь — вы просто неправильно представляете, о чем я говорю. Там формат контекстно-зависим и линеен.

Сделаем проще. Вот мааленький кусочек кода:
    pic_order_cnt_type := r.Ue()
    if pic_order_cnt_type == 0 {
        r.Ue() /* log2_max_pic_order_cnt_lsb_minus4 */
    } else if pic_order_cnt_type == 1 {
        r.U(1) /* delta_pic_order_always_zero_flag */
        r.Se() /* offset_for_non_ref_pic */
        r.Se() /* offset_for_top_to_bottom_field */
        num_ref_frames_in_pic_order_cnt_cycle := r.Ue()
        for i := uint32(0); i <num_ref_frames_in_pic_order_cnt_cycle; i++ {
            r.Se() /* offset_for_ref_frame[ i ] */
        }
    }


где r.Ue() — чтение exp-golomb encoded числа, r.Se() — exp-signed golomb, а r.U(n) — чтение n бит. Все это — чтения нефиксированной длины, одно пропущенное чтение может сломать следующее чтение exp-golomb кода. А может не сломать, просто прочитается что-то не то. И такой if там не один.

Короче, нет, не работает тот метод. Только panic-recover.
для реализации паттерна в r сделайте свойство err в своих методах проверяете if r.err != nil { return }
и всё будет в точности как описано
для поля profile_idc, которое здесь не фигурирует, 0 не является приемлемым дефолтным значением. то есть, посреди кода будет торчать одна единственная проверка, выглядеть будет изрядно нелепо.

ну и вообще, если в языке есть подходящая конструкция(panic-recover), не использовать ее только ради какого-то абстрактного пуризма мне не по нраву. все равно с уровня выше это выглядит абсолютно одинаково.
а я и не говорил что не надо использовать panic, если вы гарантированного его перехватываете, можете использовать
ну, у меня сегодня просто неудачный день — аналогичная проблема возникла в ревью рабочего кода. =)
Ужас!
тут уже давали ссылку blog.golang.org/errors-are-values, в парсерах в go в том числе и в пакетах от google применяется так называемый errReader/errWriter
это обертка над стандартными Reader/Writer которая сохраняет err, тоесть если вы просто вызываете 5 методов Write и если на втором например произойдет ошибка, то остальные не выполняться, а вы после них всех проверите свойство у объекта writer.err() != nil
сложно будет применить в описанном случае. тут парсер контекстно-зависимый, если можно так сказать — дальнейшая структура может варьироваться в зависимости от предыдущих полей.

нормальная идея, но не всегда применима.
операции ввода/вывода (и не только) в go возвращают ошибки (error), это часть нормальной работы, то есть предусмотренное поведение, как аналог исключений можно рассматривать конструкции panic (например выход за границу массива)

Например io.EOF это тоже error и после него скор. всего не нужно падать, а можно например сказать что все хорошо, просто данные кончились.

Да, это как раз тот единственный случай, когда исключения дают более читабельный код.

Но на практике такой код (особенно если речь идет о более чем 3-х повторениях) встречается редко, и если уж и встречается — то это повод для рефакторинга — вот тут как раз недавно Роб Пайк на эту тему написал, как можно такие ситуации красиво разруливать: blog.golang.org/errors-are-values.

Важно понимать, что в Go исключений нет не потому, что авторы считают, что «исключение — это зло в высшей инстанции», а потому что не видят способа, как можно добавить исключения и не сделать язык более сложным и более подверженным неправильному использованию обработки ошибок с их помощью.

Ну и вот есть имплементация Try() Catch() Finally() для Go — но вряд ли ей кто-то будет пользоваться. Чисто proof-of-concept эксперимент: github.com/manucorporat/try
кстати ветка ниже навела на мысль, что это не так, если не удастся открыть второй файл, то первый нужно будет закрыть… и всеравно придется добавлять блок finally с проверками открыт ли файл, если да то закрыть его
+1
Это к вопросу о том, что обработка ошибок — такая же полноценная часть программы, а не что-то отвлекающее, что нужно спрятать подальше. В данном примере соблазнительность метода «завернуть все в блок try..catch… и язык все сам разрулит» очень легко порождает подобные ошибки, да.
«Классическая» обработка исключений (try/catch) построена ровно на одном — сплошь и рядом есть фрагменты кода, которые работают как единое целое, в них последующие операции бессмысленны при ошибке в предыдущих. Если внутри такого блока произошло что-то, то дальше этот блок выполнять нет смысла. Именно поэтому исключения обрабатываются не после каждой строчки, а в конце блока.

В философии Go практика такого структурирования кода является порочной?
Конечно. Ошибка — не означает автоматически, что нужно завершить функцию. Если ошибка некритична — возможно нужно просто записать в лог, или подставить дефолтное значение, или попробовать fallback-метод. Тонна вариантов и явный возврат ошибок делает обработку гибче и яснее.

Но я не хочу развивать тему exceptions vs return-values — поинт статьи был несколько в другом. Решения дизайна по Go принимал не я, в конце концов. :)
Ошибка — не означает автоматически, что нужно завершить функцию
Само собой. И, как в другой ветке уже упомянуто, огромные куски кода под try/catch — конечно же порочны. Но заставлять кодера маниакально писать анализ после каждого оператора… Крайность же, возведённая в стандарт )) какой-то «on error resume next» ))

я не хочу развивать тему exceptions vs return-values
Мне кажется, зря. Если что-то преподносится как преимущество, как обойтись без сравнения?

Ну, мне хватило опыта подобных дискуссий :)
Есть люди, которые считают, что «чем сложнее, тем лучше, нужно просто научиться» и все доводы этой статьи им будут смешны.

Но заставлять кодера маниакально писать анализ после каждого оператора… Крайность же, возведённая в стандарт ))

Не заставлять. Если кодер сильно хочет и считает, что он прав — он может заглушить обработку ошибки, но это остается на его совести. В конце концов, хороший программист и отличается от плохого тем, что знает, когда можно забить на проверку ошибки, а когда нет ;-)
Если кодер сильно хочет и считает, что он прав — он может заглушить обработку ошибки
… при этом потеряв логическую связность кода — при заглушенной ошибке он нарвётся на клад где-то в совершенно другом месте, попытавшись использовать результат «заглушенной» функции.

Просто подобной логики обработки ошибок (весьма упрощённой, но идея та же) я наелся от души ещё в vbscript/vba. Иная простота, как говорится…
Я потерял нить дискуссии.
Вы хотите сказать, что явный возврат ошибок подталкивает к тому, что программист будет их игнорировать и аппелируете к опыту в vbscript? Это, кстати, частый аргумент в спорах о Go — «я уже видел подобное в Java/VB/C#/Python/whatever и считаю, что это плохо». Нюанс в том, что в Go это сделано иначе и детали решают. Например, не будь при всем этом «обязательной проверки неиспользованных переменных» — вся эта конструкция с возвратом err была бы не ценнее, чем возврат кодов ошибок в C.

Ну и правда в том, что реальность иная — люди не игнорируют ошибки в Go. Возможно я упускаю какие-то другие причины, но в Go считается дурным тоном игнорировать обработку ошибок. Для интереса можно прогнать утилиту errcheck по всем open-source проектам на Go и сравнить :)
Всё, прошу меня простить, в соседней ветке мне пояснили. Я достаточно долго не мог сообразить, что вариантов поведения при ошибке не два (обработать или проигнорировать), а три (явно вернуть полученную ошибку наверх).

В этом случае всё становится на свои места, просто всё, что было бы блоками try/catch, нужно оформлять отдельными процедурами.

Вопрос снимается, извините ещё раз. В статье этот момент не подчёркнут, и меня сбила с толку аналогия с vbs/vba
вы передергиваете, писать обработку нужно только там где это необходимо, если не нужно обрабатывать делаете return этой ошибки, тоесть в момент написания кода вас подтолкнули принять решение, вернуть ошибку, обработать или забить (через _) и вы его приняли.
Если тот код который вы вызываете не возвращает error то тоже не нужно ничего обрабатывать.
Простите, но «обработать или забить» — это чушь, не выбор. Большинство функций при ошибке не вернут осмысленного результата (или я чего-то не понимаю). Попытка использовать этот результат без обработки приведёт к настолько плохо диагностируемым ошибкам, что уши завернутся.
А, простите, я не сразу понял Вас. Вместо блоков try-catch предлагается использовать отдельные процедуры и вместо бросания исключения делать return? Ну тогда да, тоже подход
Однотипный код лучше выносить в отдельную функцию. Однотипность в данном случае — это операция открытия файла и обработка ошибки открытия.
Что в данном случае можно вынести и куда?
func diff(filename1, filename2 string) string, error {
     file1, err := os.Open(filename1)
     if err != nil {
         return "", err
     }
     defer file1.Close()

     file2, err := os.Open(filename2)
     if err != nil {
         return "", err
     }
     defer file2.Close();

     // some work
}
Пользуясь ссылкой, приведённой выше, например, так (используя замыкание):

func diff(filename1, filename2 string) string, error {

    var err error

    open := func(filename string) typeOfFileIDontRememberIt {
        if err != nil {
            return
        }

        file, err := os.Open(filename)
        defer file1.Close()

        return file
    }

    file1 := open(filename1)
    file2 := open(filename2)

    if err != nil {
        return "", err
    }

    // some work
}


Другой вопрос, что конкретно в этом примере читаемость ухудшилась, поэтому я бы не извращался.

Именно, для двух повторений оно того не стоит.
А вот если нужно с десяток однотипных вызовов-и-проверко делать — то стоит.

Еще можно в вашем примере в замыкании возвращать func(), которая будет использоваться для defer в основной функции. Сейчас код не соберется изза defer file1.Close().
Писал без проверки, а исправить уже поздно. Должно быть
defer file.Close()
Еще хуже — file закроется при выходе из замыкания :)
Эээ… почему? Разве не при выходе из diff?
Всё-все, понял… Согласен, нужно другое решение.
golang.org/ref/spec#Defer_statements

A «defer» statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.


анонимная функция в данном случае ничем не отличается от обычной функции
это не равноценный код, у вас файл будет закрыт при выходе из анонимной функции
Этот код имхо не скомпилируется: операция := не просто присваивание, а объявление переменной совмещенное с инициализацией, а так получается что 'err' объявляется два раза.
Впрочем, если бы переменные file1, file2 и err были объявлены выше, а здесь использовалось бы присваивание, то да — первая ошибка потеряется.
Наверное, решение в этом случае — иммутабельность переменных по умолчанию, хоть мне, стороннику низкоуровневости, это и не нравится.
Интересно, это фича или баг в языковой архитектуре?
Еще и типы должны совпадать
Проблема в том, что обработка ошибок «разбавляет» простой и ясный код, делая его менее понятным и менее читабельным. С точки зрения программиста, который потом будет читать код, пытаясь понять логику работы, мириады обработчиков ошибок — это своего рода мусор.
А кодеры стремятся к красоте кода. Что толку в правильно работающем коде, если код этот уродлив? И Go в этом плане ничуть не лучше, т.к. if'ы загромождают код и отвлекают при чтении ничуть не меньше, чем try/catch. Вот если бы какой-либо язык позволял надёжно отделить обработку ошибок от основного функционала программы… Ну что-то вроде «заметок на полях», где кратенько расписывается, что делать в случае ошибки. Как-то так:
 file1=os.Open("test1.txt") | someErrorHandler(HANDLE_AND_EXIT)
 file2=os.Open("log.txt") | someErrorHandler2(HANDLE_AND_CONTINUE)
 file3=os.Open("test2.txt") | errHandler3(HANDLE_AND_EXIT_CUR_FUNCTION,ER_CODE)

Наверняка можно и ещё короче/красивее сделать, чтобы обработка ошибок не мешалась и не загромождала основной код. Ещё и IDE подключить, чтобы эти «окончания» строчек выделялись бледным шрифтом и не слишком бросались в глаза.
Вот — это оно.

В большинстве популярных языков, «проверка ошибок» выглядит как нечто опциональное, загромождающее, мешающее и отвлекающее от понимания красоты кода.

Это очень и очень вредная практика, которая, к сожалению, популярна — это тот самый «второй лагерь», про который я писал в посте. Понимание того, что обработка ошибок — это неотъемлимая часть любого кода — рано или поздно приходит ко всем, и чаще с негативным опытом. И вина в этом лежит именно на инструментарии, а не на программисте, который «просто прочитал мало правильных книг еще». В этом и поинт статьи.

Go вынуждает людей переосознать важность корректной обработки ошибок. Да, это зачастую вынуждает разрушать стереотипы, подобные вышеозвученному, но если вы когда-нибудь будете писать критически важный код, то, возможно, разрушение этих стереотипов важно для спасения чьей нибудь жизни ;-)
Ну тут дело не в языке, а в том, на чем я, как разработчик и человек, читающий код, хочу сосредотачивать свое внимание. Кстати говоря, подход, предложенный выше, очень интересный. Можно его даже расширить и каждому классу или файлу ставить в соответствие соседний класс или файл, где содержатся только обработчики ошибок.
Да ладно вам, любой, кто пишет Go код больше месяца уже умеет читать между строк. То есть все эти проверки на ошибки выглядят как шум на фоне (однако, они есть). И да, если правильно разделять блоки пустыми строчками, то код становится очень читабельным.
это не мусор, это часть работы программы, если не обрабатывать ошибки то программы будут падать или работать не правильно, портить данные и т.п.

всё что вы написали псевдокодом с кущей констант уже есть:
> file1=os.Open(«test1.txt») | someErrorHandler(HANDLE_AND_EXIT)
if file1, err = os.Open("test1.txt"); err != nil {
    log.Fatal("Aborting: ", err)
}


> file2=os.Open(«log.txt») | someErrorHandler2(HANDLE_AND_CONTINUE)
if file2, err = os.Open("log.txt"); err != nil {
    someErrorHandler2(err)
}


> file3=os.Open(«test2.txt») | errHandler3(HANDLE_AND_EXIT_CUR_FUNCTION,ER_CODE)
вообще для того чтобы что-то возвращать нужно иметь соответствующее объявление функции

if file3, err = os.Open("test2.txt"); err != nil {
    errHandler3(err)
    return ER_CODE
}

Вот если бы какой-либо язык позволял надёжно отделить обработку ошибок от основного функционала программы…
Вы таки будете смеяться, но этот невероятно читабельный язык… Perl:
open my $file1, 'test1.txt'     or die someErrorHandler($!);
open my $file1, 'log.txt'       or someErrorHandler2($!);
open my $file3, 'test2.txt'     or errHandler3($!), return ER_CODE;
Выше коммент про perl, но perl — это просто страшно :)
Но точно так же (or blablabla and die) можно написать на Ruby, что уже лучше.
В защиту Exception стоит вспомнить, что в Java сигнатура метода должна содержать перечень исключений, который в этом методе могут быть выброшены. И на этапе компиляции проверяется, что все исключения попадают либо в catch блок, либо бросаются вызывающему методу. Это заставляет программиста проверять все исключения вызываемых методов.
Что интересно, в соседнем посте говорят об избавлении C++ от exception specification как о благе — реализовано было странно, работало только в рантайме. Каждый язык своей дорогой идёт.
Не должна, а может содержать. В Java поддерживаются как checked- так и unchecked-исключения.
Некоторые считают, что это добавило (ещё больше) проблем и геммороя в Java-мире: blog.informatech.cr/2014/04/04/jdk-8-interface-pollution/
Хотя инициатива была, конечно, из благих побуждений.
Думаю, не стоит так оголтело разделять людей на лагеря. Лучше подумать о том, что у разных языков и у инструментов разные задачи, и что исключения это удобный инструмент для своих задач. Никто же не говорит, что фрезерный станок хуже, чем электролобзик, потому что им можно отпилить сразу все пальцы. В целом, люди действительно по-разному относятся к обработке ошибок, но, боюсь, в этом вопросе эстетический компонент это не основная движущая программистом сила.
Ну, я сравнивал языки, которые претендуют на примерно одну и ту же нишу.

Не очень понял, что вы подразумеваете под «эстетическим компонентом». Мой поинт был в том, что подход Go делает сложным «забивать на обработку ошибок», а это приводит к тому, что программист так или иначе начинает быть более внимательным к этому аспекту разработки.
Под «эстетическим компонентом» я подразумевал, что из ваших слов Go мотивирует программиста не забивать на обработку ошибок исключительно из эстетических побуждений (потому что для компилятора, например, в отличии от чекд исключений в джаве, в гоу достаточно просто _ написать), а это (как мне кажется) как минимум не главный мотивирующий фактор. Не уверен, что у java и go одна ниша — разве на Go пишут «энтерпрайз» системы?
«Энтерпрайз системы» — это всего лишь прикладные программы в очень большом масштабе.
Java, да, к сожалению много ниш заняла — по крайней мере системный/серверный софт и командные утилиты на ней пишут.
Java, да, к сожалению много ниш заняла

почему к сожалению?

по крайней мере системный/серверный софт и командные утилиты на ней пишут

пишут, да, но это не основной сегмент
Не могу найти одну занимательную статью насчет Java и максимального количества строк (условно), после которого проект больше не может расти из-за багов (по опыту автора).

Суть в том, что некоторые языки заставляют программистов дублировать информацию. Вот, например, С++: хочу я ввести новый класс. Я пишу имя класса в хедере, определяю сигнатуру и имена функций. Потом я создаю cpp файл и начинаю дублировать каждую функцию с ее сигнатурой. Если так подумать, то зачем задавать одну и ту же информацию дважды? Понятно, что это связано с особенностью компиляции, но также понятно, что это создает дублирование текста на уровне языка. Захотел поменять код — будь добр у каждой из функций, у которой ты поменял аргумент, найти также ее близнеца.

Это только часть проблем, поднятых в статье, надеюсь, что кто-то найдет и выложит ссылку.
Суть в том, что некоторые языки заставляют программистов дублировать информацию.

Не очень понял связь. Вы говорите о какой-то статье про разработку на java, а пример приводите про c++. Java конечно многословна, но какого-то дублирования которое было бы вынужденным из за языка в ней особо нет. Могу только вспомнить врапперы, которые действительно за неимением средств языка требуют серьезного дублирования, но в целом из строготипизированных языков я знаю только котлин, который умеет эту проблему решать. Систему на любом языке можно написать и хорош и плохо, я вот видел люди и на джаваскрипте пишут большие приложения, и ничего, работает. Java в этом смысле достаточно неплохо работает для больших проектов, язык достаточно прост и строг, чтобы быть хорошим инструментом для больших систем.
Автор в основном говорил про Java, но в целом проблема касается и других языков. Я привел пример из c++, поскольку в Java не разбираюсь, но хотел по аналогии привести пример дублирования кода на уровне языка.
Было бы интересно почитать про подобные дублирования в java. Если найдется ссылочка, выложите пожалуйста здесь в комментарии.
Посмотрите на юзкейзы проекта Lombok. Он весь именно про выкидывание дублирования из java.
почему к сожалению?

Это эмоциональное, не хочу никого задеть, простите. Просто практически все программы на Java, с которыми мне приходилось работать за последние 10 лет — были символом тормознутости, неудобства и глючности. Может, конечно, просто совпадение, но мне хватило IBM Lotus Notes, Openproj и Amazon EC2 CommandLine Client чтобы при слове «написано на Java» начинало подташнивать.
Про адский ад с зависимостями и установкой/апгрейдом нужной версии — молчу.

Единственная программа на Java, которая более менее меня радовала (хоть и жрала всю возможную память, но там специфика позволяла) — это agent-based симулятор Gama.
энтерпрайз это годы разработки, первая публичная версия Go появилась меньше 4х лет назад, версия 1.0 зарелизилась меньше 3х лет назад
его просто не успели еще написать
Не успели написать или их и не пишут?
ну enterprise это очень широкое понятие начиная от потребностей гугл, заканчивая софтом для тестирования учащихся в школе.
Go используется на вскидку в гугл, фейсбук, яндекс, и из самого известного софта на go это docker, так что кое что уже успели написать
а чтобы за 3 года после выхода все было бы заполнено софтом написанным на go — это он сам должен за программиста писать тогда
Последнее время часто слышу «у нас всё было на Python, 2 года разработки, пока мы не упоролись и за месяц не переписали всё на Go и покрыли тестами». Всё-таки ПО это не столько код, сколько идеи. Если софт 3 года писался, это вовсе не значит (никогда не значит), что там просто набирали текст столько времени.
Тот же код на Python:
file = open('test.txt', 'r')

А вот и нет. Сейчас используют менеджер контекста:


with open(newfile, 'w') as f:
    f.read()

В этом случае, файл будет закрыт вне зависимости от того, будет ли исключение внутри блока.
Зачем закрывать файл «вне зависимости»? Исключение выбросится, если файла не существует и он и не был открыт.

Сути ваш пример не меняет — в нем на обработку ошибок забито по принципу «python сам все разрулит». Потому что это легко и просто.
Если файл успешно открыт, но во время обработки возникло исключение, то он будет корректно закрыт. Пример не в пику статье, а о правильном открытии файлов в Питоне.
Понятно. Вот за это я отдельно люблю Go — за обещание не ломать API.

Многие языки, вводя новые изменения, создают просто месиво из «правильно» и «неправильно» (в угоду обратной совместимости, конечно же). 3 года проходит — и всё, половина из того, что ты знал и как делал — уже неправильно и не кошерно :)
Где гарантия, что через 3 года не появится условный Go3, где скажут, что раньше всё было неправильно? Ну и это хорошо, язык развивается, вводятся новые конструкции.

Этому менеджеру контекста в питоне больше лет, чем всему Go.
Гарантий нет, конечно же, но есть два важных отличия у Go:
1) авторы считают «обратно несовместимые» изменения злейшим злом, и будут стремиться делать все возможное, чтобы даже их не было даже в мажорных версиях
2) Go это не эксперимент в стиле «давайте создадим еще один фичастый язык». После публичного анонса языка и до версии 1.0 изменения были минимальны — базис Go был изначально очень фундаментально продуман и лишь слегка обрастал и менялся до релиза 1.0. Такого треша как в Rust — когда половина всего выбрасывается, переписывается и чистится уже который раз подряд — в Go не было и не будет.

Исходя из этого, я более чем уверен, что если даже когда либо и будет Go2 (а это уж точно не ближайшие 3 года, его даже в roadmap-е нет), то в нем не будет изменений в стиле «открывать файлы через os.Open() теперь не правильно».
Менеджеры контекста окончательно появились в Python 2.6. Это 2008 год. Что плохого в том, что инструмент развивается и совершенствуется? Если Python 3 вызывает много вопросов, то тут то чего старпёрством заниматься, такие изменения явно к лучшему. Используйте современные возможности инструмента и лучшие практики, тогда не будет никакого месива. :)
(про глобальные переменные...) любой из «правильных вариантов» — А какие варианты сейчас правильные?
Зависит от того, для чего переменная. Если это, скажем, дескриптор сокета/базы-данных/mongo-сессии/whatever, который используется в запросах бекенда — то явно нужно его заворачивать в некий контекст, и уже его передавать функциями, работающими с этим. Если это некое значение по умолчанию — то либо в константу, либо в конфиг, либо в параметр командной строки (как пример). Если это переменная, описывающая состояние state-машины — то избавиться от такой переменной вообще. Ну и в таком духе.
А я вот предпочитаю всегда делать logger глобальной переменной. Это как по Вашему?
Плохо, конечно же. Глобальные переменные, даже read-only (а логгер не является read-only), имеют много скрытых проблем. По этой теме книжки можно писать. Как только ваша программа будет расти, и с ней будут работать другие люди, в том числе, писать тесты и рефакторить — этот ваш логгер в виде глобальной переменной быстро станет источником потенциальных проблем и геммороя.

Насколько я понимаю с глобальными переменными две беды — они делят общее пространство имен и их часто пытаются использовать как индикатор состояния (или они сами зависят от состояния).

В Питоне например первой проблемы нет вообще за отсутствием глобальной глобальной области видимости. Самое глобальное, куда можно засунуть переменную — модуль. Что касается состояния, логер не является источником этих проблем.

Получается, логер в глобальной переменной модуля в Питоне можно? Или есть еще какие-то причины так не делать?
По-моему с логгером только так и нужно делать. Встречал я передачу работу с логгером как self.logger — неудобнее трудно себе представить. ценность логгера уровня модуля в том, что он может выдавать файл и строку вызова в лог.
А вообще — при обоснованном использовании глобальных переменных, проблема в основном в четких соглашениях и дисциплине, ИМХО.
Кажется, Роб Пайк на какой-то конференции упоминал об этой особенности языка. Что-то в этом есть. Не полагаться на то, что программа всегда отрабатывает как надо и завершается успешно, а считать, что fail path — такая же неотъемлемая часть программы, как и прочее.

Интересен подход Rust к обработке ошибок. Функция может возвращать Option, содержащий валидный результат вычислений при нормальном исполнении, или None в случае ошибки. Можно использовать Result, если одного значения None мало:
// fn parse_version(header: &[u8]) -> Result<Version, ParseError>

let version = parse_version(&[1, 2, 3, 4]);
match version {
    Ok(v) => {
        println!("working with version: {:?}", v);
    }
    Err(e) => {
        println!("error parsing header: {:?}", e);
    }
}


При этом тоже есть некоторый вариант «считерить», не обработав ошибку:
io::stdin().read_line().unwrap();

В рантайме всё в порядке — unwrap() вытаскивает нужное значение из Option, что-то пошло не так — программа входит в состояние panic! и завершается. Похожий на Go подход, но более жёсткий.
Ну и try!() для проброса ошибки наверх.
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
Хочется сделать одно замечание. Касается оно обработки ошибок в программах написанных на Python. В частности и примера, приведенного в тексет статьи.
Так вот, мой опыт показывает, что в 90% случаев исключения можно не обрабатывать, а в 70% процентах обработанных в программах на питоне исключений, хочется убить программистов, которые их обработали.
Поясняю. Если исключение на обрабатывать (например в случае невозможности открытия файла) интерпетатор напечатает стек вызовов и завершится. Из стека будет довольно таки ясно, в каком месте и что случилось.
В случае же обработки, 50% процентов программистов, или пишут не мудрствуя лукаво pass, либо выводят в лог, stderr или даже в stdio сообщение, из которого вообще, фиг чего поймешь.
Про обработку ошибок кстати нарыл забавную статью от Роба Пайка, прочтение которой уберегло бы например меня от задавания глупых вопросов в комментариях.
Да, хорошая статья, она тут уже в комментариях проскакивала. )
НЛО прилетело и опубликовало эту надпись здесь
Собственно об этом и статья. Ошибку можно спрятать, в некое подобие union и давать читать либо переменную, либо ошибку, а можно выбрасывать исключение. Но в Go принято ошибку возвращать *явно* — от этого «не заметить» ошибку становится невозможно — что порождает стимул всегда обрабатывать ошибки, где они могут возникать. В этом и поинт.
НЛО прилетело и опубликовало эту надпись здесь
Да, этот пример тут проскакивал уже.
Rust-овский Result — по сути дела лишь union, в котором может храниться либо значение, либо ошибка. Это выглядит симпатично, хотя «явность наличия ошибки» тут заключается, если я правильно понимаю, лишь в том, что File::open() возвращает IoResult, в котором явно указан взаимоисключащий тип IoError. Если убрать «плюс» union в экономии места, то это ничем не отличается от возврата структуры с двумя полями, вместо двух значений, как в Go. И вот здесь «возврат двух значений» как раз явнее, потому что со вторым значением что-то таки нужно делать, а поле в структуре (точнее в union) можно молча «забыть».

Сумбурно, но как-то так.

Rust, конечно, интересный язык, и я стараюсь на него посматривать иногда, но, да простят меня фанаты Rust, пока что очень отталкивает своей непродуманностью. Код, который чуть ли не с нуля переписывают несколько раз подряд, в котором сначала добавляют огромное количество фич, которые потом же начисто выпиливают — не внушает ни доверия, ни желания его изучать. Высокий порог входа и достаточно вырвиглазный синтаксис тоже не добавляют доверия.
Но следить за развитием, конечно, интересно — ниша все-таки важная и амбиции большие.
Все немного не так, в Rust нельзя не обработать ошибку, если делать матчинг, то нужно обработать все возможные варианты (есть ошибка или все ок), если делать unwrap, то в случае ошибки программа упадет целиком и этого никак не предотвратить. Оба подхода более явные и менее обходимые, чем подход в Go.
Ок, принимается. Тогда да, очень правильный подход.
да в Rust вариант более строгий, хотя смысл очень похож:
if file, err := os.Open("foo.txt"); err == nil {
    // здесь код, который может работать с файлом
} else {
   // здесь можно вывести сообщение об ошибке
}

// здесь нет ни file ни err

только строчек примерно в 2 раза меньше, ну и как я писал в обоих случаях можно работать с результатом
Насчёт строчек странное замечание. Можно и так написать, если сильно захотеть

match File::open(&Path::new("foo.txt")) { Ok(f) => {
        // здесь код, который может работать с файлом с помощью переменной f, к переменной e в этой ветке доступа нет
    } Err(e) => {
        // здесь можно вывести сообщение об ошибке ипользуя переменную e, к переменной f в этой ветке доступа нет
    }
}

Если пробрасывать ошибку на уровень выше, то ещё короче (обычно так и делают)
let file = try!(File::open(&Path::new("foo.txt")));
let line = try!(file.read_line());
результаты и ошибки тоже разные бывают, например если это Read() ([]byte, error) то []byte может содержать байты прочитанные до возникновения ошибки
а про file -обратится можно, только там nil будет

на самом деле в Go нет ничего чтобы было сделано именно для обработки ошибок: возврат нескольких значений, короткий синтаксис объявления переменных, возможность пропустить значение при присваивании нескольких значений — все это общие конструкции.

Как я писал выше Go не заставляет обрабатывать ошибки, но он уберегает (в большинстве случаев) когда вы можете забыть это сделать, во всех случаях вы сами приняли решение что делать с ошибкой: игнорировать, обработать, или вернуть выше.
Ручная обработка ошибок как и try-catch позволяют (как минимум) сделать 3 вещи:
1. Забить на ошибку
2. Обработать ошибку
3. Передать ошибку наверх

Т.е. try-catch по сути делает тоже самое, только дефолтное поведение — это передача ошибки наверх, как отписались выше это то что нужно в 90% случаев.
Дефолтное поведение в golang — это видимо «забить» на ошибку, + авторы строго рекомендуют использовать 2 и 3 варианты.

Так же try-catch позволяет писать меньше кода, а меньше кода — это меньше ошибок, лучше читаемость и т.д. А то что try-catch «неявно покрывает» весь код, делает приложение более надежным (хотя это может быть не очевидно).

PS: В разных языках try-catch может иметь разные нюансы, я написал взляд из python.
Т.е. try-catch по сути делает тоже самое, только дефолтное поведение — это передача ошибки наверх, как отписались выше это то что нужно в 90% случаев.

если у вас задача падать то да, а вот если работать, то обрабатывать ошибки придется, и вот тогда у вас этих try/catch будет огого как много.
При использовании исключений если вы ее не поймате сразу же то может статься что её поймают где-то в глобальном catch всего приложения, и продолжить выполнение вы уже не сможете.
error в Go это не исключительная ситуация, это часть работы приложения, исходя из того что вы вызываете вы сами принимаете решение что делать, с ошибками, например если вы файл по сети читаете то их нужно обрабатывать, а если вы сделали Reader (стандарный интерфейс для чтения потока с 1 методом Read() ([]byte, error)) из куска оперативы, то там и обрабатывать нечего.
> Дефолтное поведение в golang — это видимо «забить» на ошибку, + авторы строго рекомендуют использовать 2 и 3 варианты.
его нет, error это часть результата некой функции, как из 3х вариантов выбрать придется прописать явно*

* строго говоря есть 1 случай когда error это единственный результат работы функции и тогда можно просто написать вызов (_ := писать не заставляют)
> если у вас задача падать то да, а вот если работать, то обрабатывать ошибки придется, и вот тогда у вас этих try/catch будет огого как много.

1) В скриптах выход «падать», ИМХО может быть основным способом обработки ошибок. Это экономит массу времени при написании без ухудшения записи в лог.
2) Вторая задача работать — должна быть нескколько переформулирована, запиать место и причину исключения в лог и продолжать работать.
Как в случае 1), так и в случае 2) try/catch великолепно работает.
а вот если работать, то обрабатывать ошибки придется, и вот тогда у вас этих try/catch будет огого как много

это не совсем так. В общем случае обработка нужна в ключевых точках, где мы можем управлять судьбой приложения, а не в каждом конкретном месте открытия файла. А так как стандартное поведение это прокидывать эксепшн выше, то обычно это вполне неплохо работает. Хотя нужно отметить, что в java с оборачиванием эксепшенов в больших системах периодически случается чехарда с обработкой, но при хорошей организации системы эксепшенов обычно этого удается избежать.
тогда у вас этих try/catch будет огого как много.

Нет, например в веб-сервере, при формировании страницы клиенту достаточно одного try-catch, который будет отправлять 500 ошибку и писать в лог, при этом весь сервер будет работать стабильно.
Если это ошибка кода, то вы её просто исправите, и в дальнейшем она не появится. Если что-то внешнее, например не доступен один из ресурсов, вы добавляете один try-catch в нужное место, где вместо результата будет сообщение «сервис не доступен», но при этом остальная часть страницы будет выводится нормально.
И это всего 2 try-catch, вместо тысячи проверок каждого вызова.

А в golang если вы пропустите проверку некоторых ф-ий, то может произойти непредсказуемая работа программы.

При использовании исключений если вы ее не поймате сразу же то может статься что её поймают где-то в глобальном catch всего приложения, и продолжить выполнение вы уже не сможете.
И это правильно, нельзя работать после ошибки, иначе будут непредсказуемые последствия, зачем пытаться читать из файла если не смог его открыть? Но если все же хочется, то это можно сделать.

а если вы сделали Reader… из куска оперативы, то там и обрабатывать нечего.
и тут приложение выпадает в crash, если вдруг произойдет чтение из чужой области памяти, а с try-catch, этот кусок кода остановился бы, и приложение продолжило работать с другими вещами, (просто как пример).
> достаточно одного try-catch
> при этом весь сервер будет работать стабильно.
это не так, вы же не hello world возвращаете, при работе много поточного приложения есть доступ к общим объектам, теже блокировки например, так вот если что-то пошло не так вы должно сделать Unlock, иначе у вас приложение не сможет дальше работать, так что 1 try/catch явно мало

> И это всего 2 try-catch, вместо тысячи проверок каждого вызова.
а почему не миллион? недавно была статья про рекомендации НАСА к кода, там не только результат проверяется, но и насколько результат соотвестует допустимым границам и так для каждого вызова (а в Go только для тех что error возвращают)

> И это правильно, нельзя работать после ошибки, иначе будут непредсказуемые последствия, зачем пытаться читать из файла если не смог его открыть? Но если все же хочется, то это можно сделать.
ошибки бывают разные, программы не только открывают файлы (см. внизу)

> и тут приложение выпадает в crash, если вдруг произойдет чтение из чужой области памяти
да упадет и в Go если вы сможете такое сделать это будет panic, так как вы допустили такое и после этого

еще раз напишу — в Go для действительно исключительных ситуаций есть panic — это считайте те же исключения, для того что предусмотрено и что должно обрабатываться в вашем коде используется error

если вы запрашиваете элемент 5 у массива с 2 элементами это panic — вы не правильно написали программу, она не работает, если вы не смогли скачать файл — это error потому что это нормальная ситуация, и вы должны принять решение что делать дальше, прямо в том месте где вы его скачивали, сохранить половину и попытаться еще раз, удалить и т.п., если вы не можете принять этого решения (например вы пишите библиотеку) — вы возвращаете этот error из своего метода, и тот кто будет пользоваться вашей библиотекой, в свою очередь опять примет решение, какое решение принять, это часть нормальной работы
НЛО прилетело и опубликовало эту надпись здесь
вот есть у вас список подключенных клиентов, вы захотели его сохранить на диск, заблочили его на запись, начали сохранять и не смогли записать на диск — место кончилось.
Никакие Rall тут не спасут, (в go кстати нет деструкторов в классическом виде, хотя можно повесить хук на удаление объета)
НЛО прилетело и опубликовало эту надпись здесь
При использовании исключений если вы ее не поймате сразу же то может статься что её поймают где-то в глобальном catch всего приложения, и продолжить выполнение вы уже не сможете.
Не рассматривайте ошибки и try-catch как что-то плохое, это просто удобный инструмент для передачи управления, как «крутой goto».

Например его можно и не стандартно использовать, например глубоко рекурсивная ф-ия делает поиск (в графе), при нахождении результата, вместо оформления возвратов через return, вы можете просто сделать один `raise Result(...)`, и там сверху, где была запущена рекурсия — поймать этот результат: `except Result`.
да я и не рассматриваю, но ваш пример с рекурсией — это не правильное использование исключений.
Тот же код на Python:
file = open('test.txt', 'r')
Тоже самое — гораздо проще просто вызвать open(), а обработкой ошибок заняться потом.

Не совсем так, что тут может произойти: файл либо «откроется», либо нет.
Если открылся, то идет дальнейшая с ним работа.
Если не открылся, то идет передача управления на верх (вот, обработка ошибки уже началась), а там может и не нужен перехват ошибки, например если вся работа программы крутится вокруг этого файла, при этом программа остановится и мы увидим «отчет» о проблеме с файлом.
С тестами согласен — удобно.

С ошибками — скорее нет. По поводу ошибок мне больше нравится подход erlang — чуть что не так — проблемная часть сразу падает и, если надо, перезапускается супервизором.
Общая система получается надёжной и код избавлен от рутинной обработки ошибок если ты их не ждешь.

Для примера тот же открытие файла
{ok, Handle} = file:open(...)


если всё ок — продолжаем работать.
Если не ок — сравнение не проходит и процесс падает целиком.

для игнорирования ошибки
{_, Handle} = file:open(...)


Для обработки ошибки
{Status, Handle} = file:open(...)
case Status of
...
end;

т.е. было бы прекрасно и в go писать что-то вроде:
file, nil := os.Open(...)

и если код ошибки вдруг не nil — падать в панику.

Аналогично для выражений в строку эти двойные возвраты мешаются, т.е. нельзя написать например
cInt := strconv.Atoi(a) + strconv.Atoi(b)


надо писать
aInt, err1 := strconv.Atoi(a)
if err1 != nil {
  panic(err1)
}
bInt, err2 := strconv.Atoi(b)
if err2 != nil {
panic(err2)
}
cInt := aInt+bInt


Если бы был способ при возврате ошибок или неожидаемого значения сразу падать — это значительно бы сократило и улучшило читаемость кода. Например удобно вот так:
   cInt := strconv.Atoi(a)(_, nil) + strconv.Atoi(b)(_, nil) 


символ _ может быть только один — он используется в качестве возвращаемого значения если остальные параметры совпадают.
вместо nil может быть константа для сравнения на равенство, выражение — для сравнения по значению или интерфейс для проверки соответствия интерфейсу.

Если что-то не совпало — сразу паника по поводу несовпадения условий.
можно в обоих случаях писать err вместо err1 и err2
делать панику из-за обыденной ситуации не правильно, error Это часть логики работы, это нормальное поведение panic — это не нормальное поведение, по сути свидетельствующее что программа написана не правильно, и так как без человека не понять где что-то пошло не так, нужно падать так далеко по стеку вызовов насколько позволят (recover)
вот как раз в erlang или в примере как было бы хорошо есть вариант выбора что является нормальным поведением, а что — нет.

Например если я получаю строку из каких-то внутренних ресурсов я рассчитываю что она будет содержать число и работаю так как будето это число. Если там оказалось что-то другое — это ошибка и надо падать.

В случае если это входной параметры через API например — там можно было бы уже проверить явно есть ли ошибка и если есть — выдать код ошибки что такой-то парметр не правильный ну или поправить (по ситуации).

Сейчас в golang возможности выбирать нет — нужно обратавать всё.
Два момента:
— если вы на 150% уверены, что не хотите проверять ошибки для Atoi() — можно а) использовать свой двухстрочный враппер б) использовать сторонние пакаджи, с врапперами для Atoi/ParseInt.
— в долгосрочной перспективе допущение «это пришло из внутренних ресурсов, поэтому можно не проверять» — плохое. За исключением комментариев, у вас нет возможности нигде это допущение проверить или сформулировать, и когда ваш код начнет развиваться/рефакториться и с ним будут работать другие люди — они не будут знать про это допущение, даже когда «внутренний инпут» заменится на «данные от клиента». а это источник багов. именно поэтому лучше приучить себя лишний раз проверять даже те ошибки, которые сейчас кажутся невозможными.
Согласен, что игнорирование плохо, но падения в этом случае — очень хорошо.
В любом случае моя обработка таких ошибок заключается в
panic(errors.New(«Can't parse variable to int: » + a)) и дальше если случится — по стеку понять где эта ошибка и разбираться почему она случилась.
Ровно так же могла бы упасть и синтаксическая конструкция, только короче.

Обертки — да, оберток уже много :)
НЛО прилетело и опубликовало эту надпись здесь
НЛО прилетело и опубликовало эту надпись здесь
А в чем революционность именно языка? В Java или в любом другом мейнстрим-языке можно написать функцию, которая будет возвращать Pair<ResultType, Exception> с тем же примерно эффектом.
НЛО прилетело и опубликовало эту надпись здесь
Ну легковесные потоки и GC не имеют отношения к языку как таковому, на мой взгляд. Думаю, такие вещи в принципе можно реализовать для любого языка.

В обработке ошибок (о чем говорит автор) тоже особенной революционности не видно. Предлагаются примерно такие же средства как и в других языках. Аналог связки throw и try-catch — это panic и recover, аналог finally — defer, аналог того, о чем пишет автор — это просто передача исключения как части результата функции.
Ну легковесные потоки и GC не имеют отношения к языку как таковому, на мой взгляд. Думаю, такие вещи в принципе можно реализовать для любого языка.

Про потоки — на вскидку я могу назвать только 2 языка в которых это сделано частью стандартного синтаксиса это Go и Erlang, тоесть чтобы запустить новый поток нужно написать go, spawn и всё.
про GC, на примере того же C/C++ я себе это слабо представляю

Обработка ошибок это то что по сути было в C++ — если что-то аномальное то исключение, если предусмотренный случай то это соответствующий код возврата.

При создании языка решали вполне конкретные проблемы, которые всплыли в процессе разработки в Google на разных языках и Go с этим справляется, что будет дальше покажет время
Про потоки — на вскидку я могу назвать только 2 языка в которых это сделано частью стандартного синтаксиса это Go и Erlang, тоесть что запустить новый поток нужно написать go, spawn и всё.


Ну, например, в JVM-мире существует Akka. Там работа с акторами, конечно, не встроена в язык. Ну а зачем, спрашивается, встраивать в синтаксис языка общего назначения то, что можно запросто выразить средствами объектно-ориентированного и функционального программирования (функциями, объектами)? С тем же успехом можно встроить в синтаксис работу с логами, сокетами и вообще чем угодно.

про GC, на примере того же C/C++ я себе это слабо представляю


Почему? Для Си нельзя написать виртуальную машину? Причем тут язык-то?

При создании языка решали вполне конкретные проблемы, которые всплыли в процессе разработки в Google на разных языках и Go с этим справляется, что будет дальше покажет время


У меня нет никаких сомнений, что справляется, потому что средства работы с исключениями Go аналогичны таковым в большинстве современных ЯП.
Из ответа vsb в другой ветке осознал про связь GC и языка. Действительно, если у вас в языке существуют указатели сделать сборку мусора проблематично. Но, опять-таки, полно языков без указателей. Есть ли у Go какие-нибудь фишки, которые радикально повышают качество сборки мусора по сравнению с этими другими языками?
у нормальных языков есть стандарт который не только описывает что можно делать в языке, а что нет, но и как это делать — чтобы программы одинаково работали при компиляции разными компиляторами.

Почему? Для Си нельзя написать виртуальную машину? Причем тут язык-то?
При чем здесь виртуальная машина и сборщик мусора?

У меня нет никаких сомнений, что справляется, потому что средства работы с исключениями Go аналогичны таковым в большинстве современных ЯП.
Это касалось не только (и не столько) исключений
При чем здесь виртуальная машина и сборщик мусора?


Да, согласен, с GC действительно не все так просто.

Действительно, если у вас в языке существуют указатели сделать сборку мусора проблематично. Но, опять-таки, полно языков без указателей. Есть ли у Go какие-нибудь фишки, которые радикально повышают качество сборки мусора по сравнению с этими другими языками?


Это касалось не только (и не столько) исключений


Ну конкретно меня интересует как раз революционность обработки исключений — ветка началась именно с этого.

ну революционности нет, взяли то что использовалось в C++ годами только вместо кода возврата, можно возвращать nil или объект который точно может вернуть описание того что пошло не так
ну и доп. информацию (как например выше кидали информацию про таймаут) при желании
А причем тут Akka? Бегло посмотрел, это вроде как лишь очередь сообщений.
А тут вместо
Thread th = new Thread() {
  public void run() {
    ourFunction();
  }
};

th.start();

Пишется просто
go ourFunction()

Плюс потоков много не поназапускаешь, а горутин можно очень много…
Уже отвечал в ветке чуть ниже. Akka — это не очередь сообщений, это реализация actor model со всякими плюшками.
Меня как человека, работающего и с erlang, и c go, очень раздражает, когда их пытаются ставить рядом.

Дело в том, что я точно знаю, что рантайм эрланга превосходит рантайм го в разы — настоящей изоляцией процессов, per-process gc, честным preemptive планировщиком, средствами интроспекции. Да, у него есть свои нюансы под высокой нагрузкой, но они есть везде. На мой взгляд, в крутизне рантайма с ним сейчас вообще ничто не может сравниться, тем более го, с его stop the world gc и планировщиком, недалеко ушедшим от примитивного gevent.
Дело в том, что я точно знаю, что рантайм эрланга превосходит рантайм го в разы — настоящей изоляцией процессов, per-process gc, честным preemptive планировщиком, средствами интроспекции

что такое «настоящая изоляция процессов»?
в нормальной программе на Go тоже вытесняющая многозадачность, хотя for {} не возьмет — тут что есть то есть
по GC — посмотрим что сделают в 1.5
в Go вообще-то есть интроспекция

> «настоящая изоляция процессов»
отсутствие shared memory да и shared state вообще. у каждого эрланговского процесса свой heap, посылка сообщения = копирование.
ок, будем честны, shared state есть — ets и refcounted binaries — но он абсолютно не такой, как в других языках и до определенного момента его наличие не имеет никакого значения. проблемы с ets легко выявляются, а эффекты от refc бинарей вы ощутите разве что в реальном хайлоаде, к моменту которого вы уже будете знать, как делать не надо.

> в нормальной программе на Go тоже вытесняющая многозадачность
там уже реализовали проверку на вытеснение при входе в функцию, как грозились?

> в Go вообще-то есть интроспекция
в erlang-е подцепиться к работающему приложению, хоть к продакшн серверу, и получить массу информации не только о состоянии системы в целом, но и о каждом отдельном процессе — потребление памяти, текущая функция, очередь сообщений, открытые порты — сокеты и файлы. просмотр ets-таблиц туда же. без остановок, на работающем сервере, в динамике.
я не знаю, что считается интроспекцией в go, но явно что-то другое.
Списки текущих горутин со стеками, память и профилировщик в go и так из коробки есть. Хоть по http получай «без остановок, на работающем сервере, в динамике».
Загруженность каналов только руками посчитать, да, готового не встречал.
levgem.livejournal.com/463349.html — хороший пост, из которого понятны возможности интроспекции в erlang-е.
НЛО прилетело и опубликовало эту надпись здесь
Да, про GC не для любого языка согласен.

В принципе всё это можно с той или иной многословностью сделать библиотечными средствами, но когда оно в языке, код получается лаконичней.


Проблема в том, что лаконичней получается только код, для которого подходит этот конкретный синтаксический сахар. А для какого-нибудь другого кейса (например, создание процесса ОС) — он уже не подходит и приходится писать по полной. Такая избирательность в синтаксисе делает язык неуниверсальным.

Поучительная иллюстрация этой мысли — встроенный XML-синтаксис в Scala. Авторы, когда его делали n лет назад, думали, что с такой мегафичей на уровне языка они всех порвут. Сейчас, когда XML уже не кажется таким актуальным, они думают, как бы выпилить эту монструозную фичу из языка и заменить чем-то более универсальным, чтобы одинакого просто было работать и с XML, и с JSon, и с форматами, которые могут стать востребованы с будущем.
НЛО прилетело и опубликовало эту надпись здесь
Ну кроме многопоточности есть, например, еще распределенность, которая тоже вряд ли куда-то денется. Как там у легковесных потоков со scale-out? Если плохо, то наверное скоро появится (или уже появилась) библиотека, которая умеет scale-up и scale-out одновременно. И горутины могут оказаться больше не нужны.
горутины это часть языка, и то что есть как минимум 2 языка в которых это (встроенная многопоточность) сделано уже должно наводить на мысль что это кому нибуть нужно
это в частности позволяет легко распараллеливание обработку и использовать их повсеместно, для многих задач. не ограничиваясь тем для чего вы это используете — для реализации асинхронного взаимодействия или вам нужно ускорить вычисления заняв несколько ядер процессора.

пример про Scala не показателен, я с тем же успехом я могу написать, классы не нужны, вон в скала xml добавили теперь выпилить не могут

Ну, следуя той же логике можно заключить, что классы — это крайне важная фича, которая есть в большинстве мейстримных языков. Встроенная многопоточность — маргинальная фича, которая есть в двух не самых мейнстримных языках (при всем моем глубоком уважении к Erlang и Go). Что-то на уровне Scala XML как раз…
Go и Erlang создавались чтобы быть многопоточными изначально, чтобы создавать сотни тысяч потоков и не убивать при этом сервер, чтобы это было очень легко делать из коробки, под все платформы и это именно повод к их созданию, потому что в других языках этого нет.
Я не очень понимаю против чего именно вы протестуете? что эти языки не нужны или что?
Я не то чтобы протестую, я скорее спрашиваю и удивляюсь.

В статье «Главное преимущество Go» половина текста про возврат исключения как части результата функции, вторая половина — про встроенную запускалку unit-test'ов. И это мол должно мотивировать писать «правильный» (в представлении автора) код. И это все главное преимущество что ли?

В каментах мы зацепились еще за 2 преимещества: наличие GC и сахар для горутин. Первое, прямо скажем, не очень специфично конкретно для Go. Второе — весьма сомнительная (для меня) заточка, которая делает язык нишевым, менее универсальным. А так ли он хорош в этой нише? Лучше Эрланга? Лучше привычных мне Scala + Akka? Вот это я пытаюсь выяснить.
а почему наличие встроенной многозадачности должно делать язык нишевым? Erlang нишевый не поэтому. А Go вообще не является нишевым (выше уже приводил пример docker)

про GC — а сколько вы знаете компилируемых языков со сборщиком мусора?

я правильно понимаю, что Akka это по сути брокер сообщений?
а почему наличие встроенной многозадачности должно делать язык нишевым?

Если отсылки к Go чаще всего всплывают в контексте легковесной многопоточности. Если она считается одной из основных фич языка. Если (на мой взгляд) сама по себе многопоточность — является скорее библиотечной функциональностью, чем элементом языка общего назначения. То можно сделать вывод, что Go заточен под многопоточность и берется ее обеспечивать просто и быстро. Это выглядит как некая ниша, которую Go стремится занять. Не утверждаю, что единственная.

про GC — а сколько вы знаете компилируемых языков со сборщиком мусора?

Много, например Scala. Вот, кстати, бенчмарк от Google (https://days2011.scala-lang.org/sites/days2011/files/ws3-1-Hundt.pdf), в котором Scala по всем параметрам кроме времени компиляции выглядит достаточно выигрышно на фоне Go.

я правильно понимаю, что Akka это по сути брокер сообщений?
Нет, Akka — это реализация actor model с поддержкой таких вещей как supervision, remoting, clustering. Можно запускать миллионы акторов на одной машине, можно — распределять по многим машинам.
компилируемый это значит на выходе получается машинный код, насколько я знаю Scala выполняется на JVM или .NET, то есть по сути является интерпретируемой.

> Нет, Akka — это реализация actor model с поддержкой таких вещей как supervision, remoting, clustering. Можно запускать миллионы акторов на одной машине, можно — распределять по многим машинам.

ну по сути это и есть брокер сообщений, просто он скрыт от вас за слоем абстракции
возвращаясь к тому с чего началось — никто не мешает сделать «Akka» для Go, никаких противоречий со встроенной много-поточностью тут нет, горутины и будут использоваться для для непосредственно выполнения кода акторов
> компилируемый это значит на выходе получается машинный код, насколько я знаю Scala выполняется на JVM, то есть по сути является интерпретируемой.

Это называется JIT-компиляция. Происходит в рантайме. На выходе получается вполне себе машинный код. Интерпретируемой «по сути» не является.

> ну по сути это и есть брокер сообщений

«По сути» это планировщик с кучей полезной обвязки. Такой же планировщик (только поменьше полезной обвязки) стоит и за горутинами.

> никто не мешает сделать «Akka» для Go, никаких противоречий со встроенной много-поточностью тут нет

Акку как любую стороннюю библиотеку можно подключить или отключить, можно без проблем заменить на другую. Горутины зачем-то встроены в язык. Захотите работать с потоками каким-то другим образом и весь сахар горутин окажется бесполезным, более того — вредным, раз нельзя полностью исключить его использование.
есть 2 типа программ компилируемые и интерпретируемые, так вот JIT это когда она у вас интерпретируется, она кусками превращается в машинный код, но менее интерпретируемой она от этого не становится и без JVM работать не будет.

по поводу планировщика:
планировщик в go ближе к тому что используется в ОС, он занимается выделением ресурсов процессора под конкретную нить выполнения, если это выполнение необходимо.

насколько я вижу отсюда, планировщик в Akka занимается несколько другим
doc.akka.io/docs/akka/snapshot/scala/scheduler.html
если есть про управление выделением ресурсов (именно когда у вас актор можно по середине бесконечного цикла остановить из вне) можете кинуть ссылку?

> Захотите работать с потоками каким-то другим образом и весь сахар горутин окажется бесполезным, более того — вредным, раз нельзя полностью исключить его использование.
вы можете расшифровать абстрактное «какие-то другим способом», что именно вы хотите делать?
> насколько я вижу отсюда, планировщик в Akka занимается несколько другим

Он там просто называется Dispatcher, и он там не один (http://doc.akka.io/docs/akka/snapshot/scala/dispatchers.html)

> именно когда у вас актор можно по середине бесконечного цикла остановить из вне

Единица планирования — сообщение. Т.е. остановить извне можно в любой момент, когда актор не обрабатывает сообщение (до того, как он получил хоть одно, между сообщениями и после того, как получил все). Если бесконечный цикл внутри обработки одного сообщения — остановить «по-хорошему» нельзя (да и незачем). Для таких «подвисающих» задач используются выделенные потоки ОС. Примерно так же делается и в Go, насколько я слышал.

> вы можете расшифровать абстрактное «какие-то другим способом», что именно вы хотите делать?

Допустим, вас не устраивает стандартный алгоритм планировщика. Вы его хотите заменить на другой, более быстрый/конфигурируемый/расширяемый/масштабируемый.
Для таких «подвисающих» задач используются выделенные потоки ОС. Примерно так же делается и в Go, насколько я слышал.

для этого как раз и используются горутины — чтобы создать параллельный поток выполнения, который можно независимо блокироваться (ну читаете вы что по сети например) и т.п.
так вот единица планировщика в go это горутина, тоесть когда ваш код должен продолжить выполняться (обрабатывать сообщение) планировщик дает вам процессорное время, при чем вы можете регулировать сколько процессов ОС можно максимально использовать (на уровне всей программы)
если вы ожидаете сообщение то планировщик не выделяет вам процессорное время.

В общем я всё еще не вижу в какой момент я должен буду понять, что нужно жить без горутин в Go. И в какой момент реализация анкоров пересекается с горутинами.

На всякий случай, вот пример чтения 2х файлов параллельно:
var content1, content2 []byte
var err error
var wg sync.WaitGroup
wg.Add(2)

go func () {
    content1, err = ioutil.ReadAll(file1)
    wg.Done()
}()

go func () {
    content2, err = ioutil.ReadAll(file2)
    wg.Done()
}()

wg.Wait()

if err != nil {
    log.Fatal(err)
}

// тут можно работать со считанным содержимым обоих файлов

Отвечаю на вопрос — возможно можно было придумать лучший заголовок для статьи, но мне хотелось раскрыть именно этот аспект — который безусловно является преимуществом языка — но который часто не попадает в списки «плюшек и преимуществ языка».
Речь о том, что дизайн языка способствует использованию «правильных» вещей в разработке ПО. «На другом языке тоже можно написать что-то похожее с похожим эффектом» — это совсем другое. Никто не будет «писать что-то похожее», если можно сделать проще («выбросить исключение» или проигнорировать ошибку). Go вынуждает программистов быть более внимательными к обработке ошибок — вне зависимости от их уровня мастерства и религиозных убеждений.
Речь о том, что дизайн языка способствует «простоте» в разработке ПО. «На go тоже можно написать просто» — это совсем другое. Никто не будет «писать просто», если придется делать «правильно» («написать тучу ифов» и таки обработать ошибку). %langname% вынуждает программистов писать более простые и читабельные программы — вне зависимости от их уровня мастерства и религиозных убеждений.
Полностью поддерживаю автора. Простота — мощный мотиватор. Когда правильные вещи делать просто, а неправильные сложно, общее качество написанного кода резко улучшается. Обработка ошибок — одна из вещей, которая задизайнена в Go с прицелом на то, чтобы написание надёжного кода было проще. У меня есть сервисы, которые ни разу (!) за всю свою историю не упали — ни во время разработки, ни в продакшне. Чего не скажешь о C++, Python, Java, Perl. Обработка ошибок всем скопом (try catch Exception в главном цикле) — тоже не решение, т.к. легко оставить утечку ресурсов.

Все языки предоставляют какие-то решения для обработки ошибок. Написать корректный код можно на любом языке. Проблема в том, что где-то это делать сложнее, что повышает вероятность что-то упустить, где-то проще.

Кстати, это всё не только ошибок касается. В Go производительный код писать проще, чем медленный. Тоже можно целую статью написать на эту тему.
Вкратце:
  1. Ошибки — суть не ошибки, а возникающие ситуации;
  2. Не надо создавать аксиомы в коде.


Подробнее:

Изучая LISP (место для начала холивара) можно найти такую интересную фичу, как «condition system». Рискну привести здесь цитату из викиучебника:
Common Lisp has an extremely advanced condition system. The condition system allows the program to deal with exceptional situations, or situations which are outside the normal operation of the program as defined by the programmer. A common example of an exceptional situation is an error, however the Common Lisp condition system encompasses much more than error handling.

The condition system can be broken into three parts, signalling or raising conditions, handling conditions, and providing recovery from conditions. Almost every modern programming language offers the first two protocols, but very few offer the last (or distinguish between the last two). This last protocol, providing restarts, or ways for the program to recover, is in some ways the most important aspect of Common Lisp condition handling.


Короче:
  1. все эти «ошибки» — есть частные случаи «ситуаций», которые возникают при выполнении функций программ, но есть и другие варианты;
  2. по протоколу, для обработки «ситуаций» нужно: её (1) обнаружить, (2) обработать и (3) предоставить возможность продолжить выполнение функции с места обнаружения или других заданных мест.


Очень немногие современные языки программирования предлагают возможность «recovery from conditions», а без неё сам протокол (читай обработка «ошибок») становится ущербным.

И получаются программы, как:
  • солдаты новобранцы — совершенно не понимающие, что делать, если что-то пошло не так (такие программы молча или громко падают),
  • либо как морские пехотинцы, которых учили несколько лет и которые могут и знают, как решать все проблемы самим, без чьей-либо помощи (такие программы упадут только когда их явно убьют, а до тех пор будут делать вид, что проблемы нет и не будут задавать интерактивных вопросов пользователю).


Когда решение должно быть на границе этих двух вариантов (зачастую это правда жизни — практически все программы выполняются в недетерминированной среде), тогда программисты пишут свои обработчики ошибок и механизмы возобновления выполнения программы.

В то время как поддержка recovery-механизма позволяет программисту функции не принимать решения о том, каким из вариантов разрешить возникшую ситуацию, а определить возможные варианты решения и предложить выбрать пользователю этой функции (и в конечном итоге пользователю программы).

Рассмотрим пример с
open('test.txt', 'r')

— что может случиться? — нет файла, нет прав, нет свободных дескрипторов, ошибка устройства...

— что можно поделать? — банально: прекратить выполнение программы, решить проблему: положить файл, дать права, закрыть неиспользуемые дескрипторы, перемонтировать устройство,… а еще: предложить другое имя файла, напечатать содержимое файла в консоли,… а зачем вообще этот файл открывать — мы же конфиг из него хотели прочитать, тогда: вернуть из текущей функции дефолтный конфиг, или напечатать в консоли конфиг, распарсить и вернуть его из функции,… ха, ну и конечно же попробовать выполнить функцию ещё раз!

Что, TL;DR? — слишком много кода писать для обработки ошибок? — да можно забить на обработку ошибок совсем (самый лёгкий путь) — тогда LISP, для лентяев, предложит вашим пользователям дефолтные рестарты:
error opening #P"test.txt":
  No such file or directory
   [Condition of type SB-INT:SIMPLE-FILE-ERROR]
 
Restarts:
 0: [TRY-DIFFERENT-FILE] TRY-DIFFERENT-FILE
 1: [RETRY] Retry SLIME REPL evaluation request.
 2: [ABORT] Return to SLIME's top level.
 3: [TERMINATE-THREAD] Terminate this thread (#<THREAD "repl-thread" RUNNING {BA80BC1}>

я категорически не согласен с автором этой статьи про то, что обработка ошибок по принципу "получил результат функции-вспомнил про совесть-не зассал проверить ошибку-тут же ее и обработал" есть хорошая практика. Привычный механизм Exeptions надает по рукам за необработку брошенного исключения и развернет весь стек до точки входа. Более того, механизм исключений крайне элегантен в алгоритмах, которые в исключених выбрасывают статус обработки данных, особенно когда статусов, например, 100 (м?) и особенно пробрасывать статус обычным return через 3 вызова мягко говоря некрасиво. Классические Exeptions, которых лишен Go, позволяют написать блок try так, как будто ошибок и вовсе не существует, а обработку отложить в блок(и) catch, вместо загрязнения кода ненужными проверками, бесконечным логированием и возвратами ошибок. Нельзя полагаться на то, что программист "не зассыт" проверить ошибку. Поэтому, на мой СКРОМНЫЙ взгляд, разработчики Go сознательно кастрировли его на одну сторону, выпилив исключения. Я молчу про дженерики.

Окей.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории