Пользователь
0,0
рейтинг
26 января 2015 в 18:53

Разработка → Главное преимущество 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?
divan0 @divan0
карма
135,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (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 на каждой второй строчке.
                • 0
                  • +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
        Скомпилируется ":=" работает пока есть хотябы одна новая переменная слева от него
        play.golang.org/p/RRwRPhrH5j
    • 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.
    • 0
      Т.е. try-catch по сути делает тоже самое, только дефолтное поведение — это передача ошибки наверх, как отписались выше это то что нужно в 90% случаев.

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

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

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

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

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

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

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

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

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

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

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

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

          если вы запрашиваете элемент 5 у массива с 2 элементами это panic — вы не правильно написали программу, она не работает, если вы не смогли скачать файл — это error потому что это нормальная ситуация, и вы должны принять решение что делать дальше, прямо в том месте где вы его скачивали, сохранить половину и попытаться еще раз, удалить и т.п., если вы не можете принять этого решения (например вы пишите библиотеку) — вы возвращаете этот error из своего метода, и тот кто будет пользоваться вашей библиотекой, в свою очередь опять примет решение, какое решение принять, это часть нормальной работы
          • 0
            теже блокировки например, так вот если что-то пошло не так вы должно сделать Unlock, иначе у вас приложение не сможет дальше работать, так что 1 try/catch явно мало

            Почему? RAII и аналогичные идиомы в других языках позаботятся о том, чтобы при выходе из скоупа всё, что нужно, разлочилось, закрылось и роллбэкнулось.
            • 0
              вот есть у вас список подключенных клиентов, вы захотели его сохранить на диск, заблочили его на запись, начали сохранять и не смогли записать на диск — место кончилось.
              Никакие Rall тут не спасут, (в go кстати нет деструкторов в классическом виде, хотя можно повесить хук на удаление объета)
              • 0
                Если у меня место на диске закончилось, то, боюсь, я уже никак не смогу его освободить, хоть с RAII, хоть с ручным управлением ошибками.

                Впрочем, это несколько ортогонально проблеме (не)автоматического освобождения выделенных и захваченных в функции ресурсов при выходе из неё по исключению.
      • 0
        При использовании исключений если вы ее не поймате сразу же то может статься что её поймают где-то в глобальном catch всего приложения, и продолжить выполнение вы уже не сможете.
        Не рассматривайте ошибки и try-catch как что-то плохое, это просто удобный инструмент для передачи управления, как «крутой goto».

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

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

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

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


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

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


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

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

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

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


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


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


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

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

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

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

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

            Обертки — да, оберток уже много :)
    • +1
      Прямо идеальный случай для монад. Вот, например, Maybe взять хаскелевскую, и пусть какая-нибудь там strToInt возвращает Maybe Int. Тогда в do-нотации:
      addNumbers s1 s2 = do
          n1 <- strToInt s1
          n2 <- strToInt s2
          return $ n1 + n2
      


      Тип addNumbers — тоже Maybe Int, если какая-нибудь из strToInt зафейлится, весь последующий код выполняться не будет, и сразу вернётся Nothing.

      Чуть-чуть по-другому, без do-нотации и в одну строчку:
      addNumbers s1 s2 = liftM2 (+) (strToInt s1) (strToInt s2)
      


      Можно где-нибудь написать заранее оператор суммирования для монадических значений, и тогда всё вообще красиво будет:
      (|+|) = liftM2 (+)
      addNumbers s1 s2 = strToInt s1 |+| strToInt s2
      


      Заодно такой поднятый плюс можно и где-нибудь ещё применять.

      Аналогично можно взять Either a b, которая чем-то похожа на пару из некоторых типов a и b. Тогда для некоторого конкретного типа ошибки ErrorType (часто просто String берут) Either ErrorType тоже будет монадой, только ещё вдобавок, возможно, хранящей информацию об ошибке.
      • 0
        Вместо пары для Either имеется ввиду этакий union, конечно же — либо один тип, либо другой.
  • 0
    А в чем революционность именно языка? В Java или в любом другом мейнстрим-языке можно написать функцию, которая будет возвращать Pair<ResultType, Exception> с тем же примерно эффектом.
    • +2
      В Go хорошая реализация легковесных потоков, позволяющая писать код без коллбэков, запускающий миллионы «потоков» на каждый чих и всё это нормально работает. И интересные механизмы коммуникации между потоками с помощью каналов. Плюс хороший Garbage Collector. И всё это хорошо реализовано, быстро и надёжно работает. Революционно нового ничего нет, просто хорошие идеи с хорошей реализацией и поддержкой мегакорпорации.
      • 0
        Ну легковесные потоки и GC не имеют отношения к языку как таковому, на мой взгляд. Думаю, такие вещи в принципе можно реализовать для любого языка.

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

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

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

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


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

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


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

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


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

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

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


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

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


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


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

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

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

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

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

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

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

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

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

          Не уверен. Для C вы не сделаете ни того ни другого качественно. Можно сделать половинчатое решение. Для качественного GC в языке должны быть определенные ограничения, например не должно быть арифметики указателей. Для легковесных потоков не скажу сходу, что должно быть, наверное можно написать компилятор C, в котором будет поддержка легковесных потоков, а может и нельзя.

          Общение между потоками через каналы сделано именно посредством примитивов языка. Также есть примитив для мультиплексирования каналов (если я правильный термин использую). Запуск потока одним словом go, без всяких дополнительных классов. В принципе всё это можно с той или иной многословностью сделать библиотечными средствами, но когда оно в языке, код получается лаконичней.
          • 0
            Да, про GC не для любого языка согласен.

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                  На всякий случай, вот пример чтения 2х файлов параллельно:
                                  var content1, content2 []byte
                                  var err error
                                  var wg sync.WaitGroup
                                  wg.Add(2)
                                  
                                  go func () {
                                      content1, err = ioutil.ReadAll(file1)
                                      wg.Done()
                                  }()
                                  
                                  go func () {
                                      content2, err = ioutil.ReadAll(file2)
                                      wg.Done()
                                  }()
                                  
                                  wg.Wait()
                                  
                                  if err != nil {
                                      log.Fatal(err)
                                  }
                                  
                                  // тут можно работать со считанным содержимым обоих файлов
                                  
                                  
                    • 0
                      Отвечаю на вопрос — возможно можно было придумать лучший заголовок для статьи, но мне хотелось раскрыть именно этот аспект — который безусловно является преимуществом языка — но который часто не попадает в списки «плюшек и преимуществ языка».
    • +2
      Речь о том, что дизайн языка способствует использованию «правильных» вещей в разработке ПО. «На другом языке тоже можно написать что-то похожее с похожим эффектом» — это совсем другое. Никто не будет «писать что-то похожее», если можно сделать проще («выбросить исключение» или проигнорировать ошибку). Go вынуждает программистов быть более внимательными к обработке ошибок — вне зависимости от их уровня мастерства и религиозных убеждений.
      • –2
        Речь о том, что дизайн языка способствует «простоте» в разработке ПО. «На go тоже можно написать просто» — это совсем другое. Никто не будет «писать просто», если придется делать «правильно» («написать тучу ифов» и таки обработать ошибку). %langname% вынуждает программистов писать более простые и читабельные программы — вне зависимости от их уровня мастерства и религиозных убеждений.
  • +1
    Полностью поддерживаю автора. Простота — мощный мотиватор. Когда правильные вещи делать просто, а неправильные сложно, общее качество написанного кода резко улучшается. Обработка ошибок — одна из вещей, которая задизайнена в Go с прицелом на то, чтобы написание надёжного кода было проще. У меня есть сервисы, которые ни разу (!) за всю свою историю не упали — ни во время разработки, ни в продакшне. Чего не скажешь о C++, Python, Java, Perl. Обработка ошибок всем скопом (try catch Exception в главном цикле) — тоже не решение, т.к. легко оставить утечку ресурсов.

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

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


    Подробнее:

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

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


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


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

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


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

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

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

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

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

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

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.