Главное преимущество Go

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



    Обработка ошибок


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

    Логично предположить, что это именно то, что отличает «хороших» программистов от «плохих» программистов, и доля правды тут, несомненно есть. Но есть одно но. Инструментарий — в данном случае это «язык программирования» — тоже решает. Если ваш язык позволяет делать «неправильно» намного проще, чем делать «правильно» — будьте уверены, никакое количество статей и книг «Как не нужно писать на [LANG]» не помогут — люди будут продолжать делать неправильно. Просто потому что это проще.

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

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

    Возьмем для примера простую вещь — открытие файла.
    Вот код на C++
    ifstream file;
    file.open ("test.txt");
    

    Это полностью рабочий код, и «правильно» обрабатывать ошибку было бы либо проверкой failbit флага, либо включив ifstream.exeptions() и завернув все в try{} catch{} блок. Но «неправильно» сделать намного проще — всего одна строчка, а «обработку ошибок можно потом добавить».

    Тот же код на Python:
    file = open('test.txt', 'r')
    

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

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

    А вот аналогичный пример на Go:
    file, err := os.Open("test.txt")
    

    И вот тут становится интересно — мы не можем просто так получить хендлер файла, «забыв» про возможную ошибку. Переменная типа error возвращается явно, и ее нельзя просто так оставить без внимания — неиспользованные переменные это ошибка на этапе компиляции в Go:
    ./main.go:8: err declared and not used
    

    Ее нужно либо заглушить, заменив на _, либо как-то проверить ошибку и среагировать, например:
    if err != nil {
    	log.Fatal("Aborting: ", err)
    }
    


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

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

    Тестирование


    Сейчас вряд ли кому-то нужно доказывать, что код покрытый тестами — это «правильный» путь, а код без тестов — это зло. Даже большее, чем глобальные переменные. «Хорошие» программисты — покрывают ~100% кода тестами, «плохие» — забивают. Но опять же — это не вина программистов, это инструментарий, который делает написание тестов сложной задачей.

    Кто совсем не знаком с состоянием дел в Go в плане тестирования — вот краткая история: чтобы написать тест для вашей функции не нужно никаких дополнительных библиотек или фреймворков. Все что нужно — создать файл mycode_test.go и добавить функцию, начинающуся с Test:
    import "testing"
    func TestMycode(t *testing.T) {
    }
    

    в которой пишете свои условия и проверки. Там практически нет никакого дополнительного синтаксиса — проверки осуществляются стандартными if-условиями. Это банально и примитивно, но это ужасно просто делать и это работает как часы.

    Все что вам нужно теперь, это запустить
    go test
    

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

    Так вот это game-changer. Программисты не любят писать тесты, не потому что они «плохие программисты», а потому что затраты времени и сил на то, чтобы «написать тесты» всегда высоки. Гораздо больше профита будет, если это же время потратить на написание нового кода. Go тихо и незаметно меняет правила игры.

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

    Я. к примеру, далеко не сразу начал осознавать важность тестирования кода, а когда начал — это было сложно и неудобно, и при первой же возможности не тестировать — я и не тестировал. С Go писать тесты стало не то что просто — стало стыдно «не писать». Я даже сам того не осознавая стал использовать TDD — просто потому что это стало чертовски просто и время потраченное на написание тестов стало минимальным.

    Вывод


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

    И в этом, по моему глубокому убеждению, одно из самых главных преимуществ Go.

    Статьи по теме


    Why Go gets exceptions right
    It's 2015. Why do we still write insecure software?
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

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

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

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

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


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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                              Один вопрос интересует — согласны ли вы с тем, что описанная ситуация (когда при возникновении ошибки остаток фрагмента кода просто не нужно выполнять, а обработка возникающих при этом ошибок однотипна) является наиболее частой?
                                              • 0
                                                вопрос на самом деле другой. Я уже много раз написал зачем нужны 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 вас не заставляет обрабатывать ошибки, всегда можно написать _ — так это защита от ошибки, а не от диверсии.
                                                • 0
                                                  большинство из них содержат код который выполняется если эта ошибка произошла
                                                  о, теперь наконец-то я вроде понял.
                                                  да, наверное вы правы. throw внутри catch и метастазы try по всему коду ничем не лучше.
                                                  • 0
                                                    Так вот условия с проверкой ошибок в этом файле встречается около 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.

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

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

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

                                                      > Меньше кода, больше защиты (надежность).
                                                      Тоесть чем меньше ошибок мы обрабатываем, тем больше надежность… ну ок
                                                      • +1
                                                        Ну и про память:
                                                        если это происходит значит вы неправильно написали программу и в этом случае в Go будет сгенерирована паника, которая работает в точности как исключения только с оглядкой на Go (будут вызваны defer при раскручивании стека вызовов)
                                                        то есть в Go разделена обработка тех ошибок которые должны происходить (например io) с теми что не должны происходить («чужая память», выход за границу массива)
                                                        потомучто когда у вас сеть упала программа должна это пережить, она должно отреагировать на это (начать по таймауту делать повторы и тп.), а когда у вас она начинает в «чужую память» лазить — то вы уже не можете просто подавить это исключение просто написав except Exception: log('Прочие ошибки') — вы не знаете что еще сломано и что вы сломаете если начнете дальше работать с этими данными и реакция по умолчанию это свалить всё приложение.
                                                        Как я уже писал это не противопоставление только исключением это та практика которая годами существует в C++ одновременное использование кода возврата и исключений, как раз для нормальной работы и не нормальной
                                                        • 0
                                                          я вам про то что внутри 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-объекте передавать позицию, и т.п.
                                                          • 0
                                                            Теперь давайте сравним с библиотекой html5lib которая тоже парсит html: github.com/html5lib/html5lib-python/blob/master/html5lib/html5parser.py
                                                            всего 2 try-except на 2700 строк, это 0.2% «лишнего кода» против 12% в исходнике выше.
                                                  • 0
                                                    > После каждого вызова писать if r.err — это не выход
                                                    не все вызовы могу содержать ошибку, в том и фишка вы обрабатываете только то что должно и будет содержать ошибку, вместо того чтобы забить на всё и падать до самой main
                                                    • 0
                                                      Если лишь 1 вызов из многих содержит ошибку, то конечно проблемы нет.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                                        Сделаем проще. Вот мааленький кусочек кода:
                                                            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.
                                                        • 0
                                                          для реализации паттерна в r сделайте свойство err в своих методах проверяете if r.err != nil { return }
                                                          и всё будет в точности как описано
                                                          • 0
                                                            для поля profile_idc, которое здесь не фигурирует, 0 не является приемлемым дефолтным значением. то есть, посреди кода будет торчать одна единственная проверка, выглядеть будет изрядно нелепо.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                                                  Вопрос снимается, извините ещё раз. В статье этот момент не подчёркнут, и меня сбила с толку аналогия с vbs/vba
                                                            • 0
                                                              вы передергиваете, писать обработку нужно только там где это необходимо, если не нужно обрабатывать делаете return этой ошибки, тоесть в момент написания кода вас подтолкнули принять решение, вернуть ошибку, обработать или забить (через _) и вы его приняли.
                                                              Если тот код который вы вызываете не возвращает error то тоже не нужно ничего обрабатывать.
                                                              • 0
                                                                Простите, но «обработать или забить» — это чушь, не выбор. Большинство функций при ошибке не вернут осмысленного результата (или я чего-то не понимаю). Попытка использовать этот результат без обработки приведёт к настолько плохо диагностируемым ошибкам, что уши завернутся.
                                                                • 0
                                                                  А, простите, я не сразу понял Вас. Вместо блоков try-catch предлагается использовать отдельные процедуры и вместо бросания исключения делать return? Ну тогда да, тоже подход
                                                        • 0
                                                          Однотипный код лучше выносить в отдельную функцию. Однотипность в данном случае — это операция открытия файла и обработка ошибки открытия.
                                                          • 0
                                                            Что в данном случае можно вынести и куда?
                                                            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
                                                            }
                                                            
                                                            • 0
                                                              Пользуясь ссылкой, приведённой выше, например, так (используя замыкание):

                                                              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
                                                              }
                                                              


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

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

                                                                Еще можно в вашем примере в замыкании возвращать func(), которая будет использоваться для defer в основной функции. Сейчас код не соберется изза defer file1.Close().
                                                                • 0
                                                                  Писал без проверки, а исправить уже поздно. Должно быть
                                                                  defer file.Close()
                                                                  • 0
                                                                    Еще хуже — file закроется при выходе из замыкания :)
                                                                    • 0
                                                                      Эээ… почему? Разве не при выходе из diff?
                                                                      • 0
                                                                        Всё-все, понял… Согласен, нужно другое решение.
                                                                        • 0
                                                                          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.


                                                                          анонимная функция в данном случае ничем не отличается от обычной функции
                                                                  • 0
                                                                    это не равноценный код, у вас файл будет закрыт при выходе из анонимной функции
                                                              • 0
                                                                Этот код имхо не скомпилируется: операция := не просто присваивание, а объявление переменной совмещенное с инициализацией, а так получается что 'err' объявляется два раза.
                                                                Впрочем, если бы переменные file1, file2 и err были объявлены выше, а здесь использовалось бы присваивание, то да — первая ошибка потеряется.
                                                                Наверное, решение в этом случае — иммутабельность переменных по умолчанию, хоть мне, стороннику низкоуровневости, это и не нравится.
                                                              • 0
                                                                Удалил комментарий, так как выше такой же уже есть.
                                                              • +14
                                                                Проблема в том, что обработка ошибок «разбавляет» простой и ясный код, делая его менее понятным и менее читабельным. С точки зрения программиста, который потом будет читать код, пытаясь понять логику работы, мириады обработчиков ошибок — это своего рода мусор.
                                                                А кодеры стремятся к красоте кода. Что толку в правильно работающем коде, если код этот уродлив? И 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 подключить, чтобы эти «окончания» строчек выделялись бледным шрифтом и не слишком бросались в глаза.
                                                                • +3
                                                                  Вот — это оно.

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

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

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

                                                                    всё что вы написали псевдокодом с кущей констант уже есть:
                                                                    > 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
                                                                    }
                                                                    

                                                                    • +4
                                                                      Вот если бы какой-либо язык позволял надёжно отделить обработку ошибок от основного функционала программы…
                                                                      Вы таки будете смеяться, но этот невероятно читабельный язык… 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;
                                                                      
                                                                      • –2
                                                                        Выше коммент про perl, но perl — это просто страшно :)
                                                                        Но точно так же (or blablabla and die) можно написать на Ruby, что уже лучше.
                                                                      • +2
                                                                        В защиту Exception стоит вспомнить, что в Java сигнатура метода должна содержать перечень исключений, который в этом методе могут быть выброшены. И на этапе компиляции проверяется, что все исключения попадают либо в catch блок, либо бросаются вызывающему методу. Это заставляет программиста проверять все исключения вызываемых методов.
                                                                        • +1
                                                                          Что интересно, в соседнем посте говорят об избавлении C++ от exception specification как о благе — реализовано было странно, работало только в рантайме. Каждый язык своей дорогой идёт.
                                                                          • +1
                                                                            Не должна, а может содержать. В Java поддерживаются как checked- так и unchecked-исключения.
                                                                            Некоторые считают, что это добавило (ещё больше) проблем и геммороя в Java-мире: blog.informatech.cr/2014/04/04/jdk-8-interface-pollution/
                                                                            Хотя инициатива была, конечно, из благих побуждений.
                                                                          • +4
                                                                            Думаю, не стоит так оголтело разделять людей на лагеря. Лучше подумать о том, что у разных языков и у инструментов разные задачи, и что исключения это удобный инструмент для своих задач. Никто же не говорит, что фрезерный станок хуже, чем электролобзик, потому что им можно отпилить сразу все пальцы. В целом, люди действительно по-разному относятся к обработке ошибок, но, боюсь, в этом вопросе эстетический компонент это не основная движущая программистом сила.
                                                                            • 0
                                                                              Ну, я сравнивал языки, которые претендуют на примерно одну и ту же нишу.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                                                                                Получается, логер в глобальной переменной модуля в Питоне можно? Или есть еще какие-то причины так не делать?
                                                                                                • 0
                                                                                                  По-моему с логгером только так и нужно делать. Встречал я передачу работу с логгером как self.logger — неудобнее трудно себе представить. ценность логгера уровня модуля в том, что он может выдавать файл и строку вызова в лог.
                                                                                                  А вообще — при обоснованном использовании глобальных переменных, проблема в основном в четких соглашениях и дисциплине, ИМХО.
                                                                                        • +2
                                                                                          Кажется, Роб Пайк на какой-то конференции упоминал об этой особенности языка. Что-то в этом есть. Не полагаться на то, что программа всегда отрабатывает как надо и завершается успешно, а считать, что 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 подход, но более жёсткий.
                                                                                          • +1
                                                                                            Ну и try!() для проброса ошибки наверх.
                                                                                          • +6
                                                                                            Мне не понравилась обработка ошибок в Go. Слишком многословно.

                                                                                            Я почти все ошибки обрабатываю одинаково – пользователю отдаю страницу 500, записываю сообщение об ошибках в лог и продолжаю работать дальше (речь про веб-сервер). Очень неудобно захламлять код кучей избыточных проверок, которые мало того, что ничего не делают, так ещё и теряют место ошибки.

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

                                                                                            В Java сделано на порядок удобней, если забыть про checked exceptions, которые, к счастью, из стандартной библиотеки потихоньку искореняются.

                                                                                            Go для меня это очень мощный рантайм (горутины), очень интересные идеи по мультипоточному взаимодействию (каналы, средства для работы с ними) и всё это портит отвратительная обработка ошибок в стандартной библиотеке и отсутствие шаблонов (generics).
                                                                                            • +2
                                                                                              которые мало того, что ничего не делают, так ещё и теряют место ошибки.
                                                                                              runtime.Caller
                                                                                              • 0
                                                                                                Это надо самому запоминать при создании ошибки? Вариант, конечно, но лучше бы ошибка сама это запоминала. Я имел в виду обработку ошибок «по умолчанию». Самому-то можно сделать всё, как нравится.
                                                                                            • +1
                                                                                              Хочется сделать одно замечание. Касается оно обработки ошибок в программах написанных на Python. В частности и примера, приведенного в тексет статьи.
                                                                                              Так вот, мой опыт показывает, что в 90% случаев исключения можно не обрабатывать, а в 70% процентах обработанных в программах на питоне исключений, хочется убить программистов, которые их обработали.
                                                                                              Поясняю. Если исключение на обрабатывать (например в случае невозможности открытия файла) интерпетатор напечатает стек вызовов и завершится. Из стека будет довольно таки ясно, в каком месте и что случилось.
                                                                                              В случае же обработки, 50% процентов программистов, или пишут не мудрствуя лукаво pass, либо выводят в лог, stderr или даже в stdio сообщение, из которого вообще, фиг чего поймешь.
                                                                                              • +1
                                                                                                Про обработку ошибок кстати нарыл забавную статью от Роба Пайка, прочтение которой уберегло бы например меня от задавания глупых вопросов в комментариях.
                                                                                                • 0
                                                                                                  Да, хорошая статья, она тут уже в комментариях проскакивала. )
                                                                                                • +2
                                                                                                  file, err := os.Open("test.txt")
                                                                                                  Но зачем возвращать два значения, и файл и ошибку? Ведь если открылся файл, то ошибки нет, а если есть ошибка, то файл не открылся. Нелогично. При таком подходе можно обратится к переменной file, даже если произошла ошибка. Нужно возвращать либо файл либо ошибку.
                                                                                                  • 0
                                                                                                    Собственно об этом и статья. Ошибку можно спрятать, в некое подобие union и давать читать либо переменную, либо ошибку, а можно выбрасывать исключение. Но в Go принято ошибку возвращать *явно* — от этого «не заметить» ошибку становится невозможно — что порождает стимул всегда обрабатывать ошибки, где они могут возникать. В этом и поинт.
                                                                                                    • +3
                                                                                                      Вот пример из Rust:
                                                                                                      let file = File::open(&Path::new("foo.txt"));
                                                                                                      match file {
                                                                                                          Ok(f) => {
                                                                                                              // здесь код, который может работать с файлом с помощью переменной f, к переменной e в этой ветке доступа нет
                                                                                                          }
                                                                                                          Err(e) => {
                                                                                                              // здесь можно вывести сообщение об ошибке ипользуя переменную e, к переменной f в этой ветке доступа нет
                                                                                                          }
                                                                                                      }
                                                                                                      
                                                                                                      Тут возвращается как раз либо файл либо ошибка и «не заметить» этого нельзя.
                                                                                                      • –4
                                                                                                        Да, этот пример тут проскакивал уже.
                                                                                                        Rust-овский Result — по сути дела лишь union, в котором может храниться либо значение, либо ошибка. Это выглядит симпатично, хотя «явность наличия ошибки» тут заключается, если я правильно понимаю, лишь в том, что File::open() возвращает IoResult, в котором явно указан взаимоисключащий тип IoError. Если убрать «плюс» union в экономии места, то это ничем не отличается от возврата структуры с двумя полями, вместо двух значений, как в Go. И вот здесь «возврат двух значений» как раз явнее, потому что со вторым значением что-то таки нужно делать, а поле в структуре (точнее в union) можно молча «забыть».

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

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

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

                                                                                                            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());
                                                                                                            
                                                                                                      • +2
                                                                                                        результаты и ошибки тоже разные бывают, например если это Read() ([]byte, error) то []byte может содержать байты прочитанные до возникновения ошибки
                                                                                                        а про file -обратится можно, только там nil будет

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

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

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

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

                                                                                                        PS: В разных языках try-catch может иметь разные нюансы, я написал взляд из python.