Идея по обработке ошибок в Go

image

В последнее (и не только последнее) время ломают много копий по поводу неудобства обработки ошибок в Go.

Суть претензий сводится к тому, что прямое использование:

customVar, err := call()
if err != nil {
	doSomething(err)
	return err
}

на больших количествах повторений гораздо менее удобно, чем классическое:

try {
	String customVar = call();
} catch (BadException e) {
	doSomething(e);
	sendException();
}

Можно долго спорить как по самому предмету претензий, так и по поводу обходных манёвров, однако же логика в «пакетном» подходе действительно имеется.

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

Выглядело бы это так:

try err {
	customVar1, err := call1()
	customVar2, err := call2()
	customVar3, err := call3()
} catch {
	doSomething(err)
	return err
}

Общая идея такова: сразу после try объявляется переменная (в нашем случае err) типа error, область видимости которой — весь блок try...catch. Далее, при каждом присвоении переменной нового значения внутри блока try, компилятор проверяет его на nil, и если вернулась ошибка — следует переход в блок catch. Теоретически это не слишком затратная операция, производительность не должна пострадать.

Также возможно назначение нескольких переменных, типа:

try errIo, errNet {
	customVarIo1, errIo := callIo1()
	customVarIo2, errIo := callIo2()
	customVarNet1, errNet := callNet1()
	customVarNet2, errNet := callNet2()
} catch {
	if errIo != nil {
		doSomething(errIo)
		return errIo
	} else {
		doSomething(errNet)
		return errNet
	}
}

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

switch err {
	case net.Error1:
		doSomethingWithNetError()
	case net.Error2:
		doSomethingWithNetError()
	case io.Error1:
		doSomethingWithIoError()
	case io.Error2:
		doSomethingWithIoError()
}

В общем, у меня всё. Ругайте.
Share post

Comments 44

    +4
    как мне кажется, уж лучше использовать наработанные стереотипы от других языков. Вместо
    try errIo, errNet {
    // ..........
    } catch {
    // ..........
    }
    

    лучше использовать форму
    try {
    // ..........
    } catch  errIo, errNet {
    // ..........
    }
    
      +4
      Ага, и вместо

      switch err {
      	case net.Error1:
      		doSomethingWithNetError()
      	case net.Error2:
      		doSomethingWithNetError()
      	case io.Error1:
      		doSomethingWithIoError()
      	case io.Error2:
      		doSomethingWithIoError()
      }
      

      использовать обычный подход:

      try {
      // ..........
      } catch errNet {
      // ..........
      } catch errIo {
      // ..........
      }
      

      И, внезапно, получаем стандартную инструкцию try/catch. Потому что она уже продумана и испытана во многих языках. Но, по какой-то причине, это не go-way.
        +1
        Не по какой-то, а по вполне определённой — создатели языка верят, что использование исключений приводит к запутанному коду.
        0
        как мне кажется, уж лучше использовать наработанные стереотипы от других языков

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

        Ну это, конечно, если не ломать сам подход к обработке ошибок. Если ломать — придётся устроить большой холивар на тему «в какую сторону» и «нужно ли». И мне почему-то кажется, что за «ломать» будут в первую очередь те, кто на Go не пишут, а большинство среди тех, кто будет «против» — использует этот язык ежедневно.
        +5
        Я возможно не совсем понял, но какой смысл в этой заметке, если это не работает? :)
        Или это какой-то совет разработчикам языка?
          0
          Это заметка о «фиче» которая не помешала бы лично мне. Как минимум ещё 9 человек со мной согласились — день прожит не зря.
          +3
          При необходимости на Go легко реализуется поведение, подобное try/catch, при чем без лишнего оверхеда по сравнению с теми языками, где это выделено в отдельную сущность. Но, по причине отсутствия явной поддержки исключений в языке, в Go это реализуется в более многословном виде без синтаксического сиропчика. Но в случае уместности такого подхода это вполне идиоматично и именно это и нужно делать и именно это и делается, в том числе в стандартной библиотеке языка.

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

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

            0
            Очень интересно было бы посмотреть на реализацию try/catch в Go руками. Пишу без сарказма, действительно интересно.
            Мне на ум приходит только panic/defer/recovery, но этот подход требует создания оберток функций для блоков try/catch, что сильно усложняет код и портит его обозримость. Не говоря уж о том, что это неправильный подход для Go.
              0
                +1
                Главный вопрос в ловле panic — производительность. Мне кажется, она тут страдает.
                  0
                  Почему она не страдает в случае try…catch, но страдает в случае panic…defer?
                    +1
                    > Почему она не страдает в случае try…catch

                    Очень даже страдает. В той же Java, например.
                      +1
                      Немного потеоритезирую, но в случае имплементации моей версии try...catch компилятор после каждого присвоения нового значения указанным после try переменным типа error — сразу после того, как записал их значение (коим является 64-битный адрес объекта ошибки) в память, но до того, как вытеснил это значение из регистра — просто смотрит, является ли значение в регистре отличным от нуля. Если да — следует простой переход в блок catch, а все нужные значения при этом уже находятся на своих местах. Никаких других накладных расходов, вроде, не предвидится. Не думаю, что в данном варианте логики производительность сильно пострадает. Всего лишь одна проверка регистра на 0.

                      Полноценное исключение, насколько я его понимаю, является гораздо более масштабным мероприятием.
                        0
                        > try err {
                        > go func() {
                        > err = MyCoolFunc()
                        > } ()
                        > } catch {
                        >
                        > }
                        как будет работать такой код?
                          0
                          Хороший вопрос.
                          Я бы не ловил в данном случае. То есть catch ловит только внутри текущей goroutine.
                            +1
                            ну то есть мало проверить на nil надо ещё тогда проверить кто его туда записал, значит надо записать куда-то кто его туда записал и так далее
                              0
                              Не надо ничего проверять. Это делает компилятор — для кода, относящегося к текущей goroutine, расставляет в местах инструкций для процессора переход по условию на точку входа в блок catch. Чуть выше я написал как именно он это делает.
                              0
                              А как-же замыкания?
                          0
                          Вы в контексте мою фразу прочитайте.
                0
                Блоки try/cath выглядят весьма уродливо. Они запутывают структуру кода и смешивают обработку ошибок с нормальной обработкой. Что мешает сделать как то так?
                if err := client.Set("Fatal", " ", 0).Err(); err != nil {
                			log.Print(err)
                }
                
                  0
                  с одной стороны да, но пример из статьи так не записать без явного обявления переменных customVar и err
                    –1
                    т.е. вот это менее уродливо?
                    client.Set(«Fatal», " ", 0).Err()
                      –1
                      это redis либа если что. Где
                      client — индикаторный соединения
                      Fatal — имя ключа
                      " " — содержание.

                      Ну а если не нравиться можно написать свою либу. Благодаря Go я это могу сделать за 30 минут просто изучив команды редиса.
                        0
                        менее уродливо чем что?
                          0
                          try/cath я полагаю
                            0
                            Чем try/catch, в качестве альтернативы которому был приведён этот код.
                            Т.е. вызов метода Err() у результата void-метода Set(), по мнению, vGrabko99, меньше "запутывает структуру кода" и не "смешивает обработку ошибок с нормальной обработкой".
                              0
                              согласен! Пример не удачен. Вот обработка ошибок бд (не кривая либа с гитхаба, а коробочная)
                              if rows, err := DB.Query("SELECT * FROM users WHERE login=?", login);err !=nil {
                              log.Fatal(err)
                              }
                              


                              Вы тупо вместо трех этажных конструкций всё делаете сразу в if.
                                0
                                В данном конкретном случае областью видимости rows будет блок if/else. Не всегда это удобно.
                                  +1
                                  Все равно удобнее try/cath (я видел очень много этих try/cath и хочу сказать что даже передача rows в глобальную переменную для функции в else блоке будет намного более читаемо и понятно)
                                +1
                                ну это не корректно с try/catch тут только if,err надо сравнивать, остальное дизайн библиотеки (конкретно эта возвращает чуть больше чем err)
                                то есть
                                > if err := client.Set(«Fatal», " ", 0); err != nil {
                                > log.Print(err)
                                > }

                                vs

                                > try {
                                > client.Set(«Fatal», " ", 0)
                                > } catch (Exception e){
                                > log.Println(e.getMessage())
                                > }
                                в этом случае вариант с if явно короче.

                                по поводу ошибки как значения против исключений можно холиварить много, но не надо тащить в Go практики работы с исключениями.
                                  –2
                                  Адско плюсую. Правда кармы не хватает))
                                    +3
                                    Вся суть обработки исключений в том, что не надо проверять результат после каждого библиотечного вызова.
                                    По моему опыту, большинство функций вообще обходятся без try. Обработка ошибок не видна и не засоряет код.
                                    В try заворачивается весь main или вся транзакция, или обработка взаимодействия с одним подключением (отвалилось — залогировали и забыли). В тех случаях, когда ошибки ожидаются, нужно вызывать фунции со статусом-результатом (TryParse вместо Parse, TryGetValue вместо обращения к Dictionary по индексу). Да, методов в библиотеках становится больше, зато писать код быстрее.

                                    Go не даёт выбора.
                                      –1
                                      > По моему опыту, большинство функций вообще обходятся без try. Обработка ошибок не видна и не засоряет код.
                                      Вот это именно то к чему склоняют исключения — генерировать ошибки, но не обрабатывать ошибки, а просто поймать их где-то в общем для всего приложения месте.

                                      > Go не даёт выбора.
                                      а ещё выбора не дает Rust
                                        0
                                        Вот это именно то к чему склоняют исключения

                                        Они склоняют, а мы не поддаёмся. Исключения не должны быть единственным способом передачи ошибки, как я писал выше. Программист должен взвешенно выбрать, что использовать.
                                        Конструкции типа try { client.Set(«Fatal», " ", 0); } catch (Exception e) это антипаттерн, не надо их приводить в пример.
                                          0
                                          Я не его приводил в пример, а просто написал эквивалентный вариант по Go коду.
                                          В Rust и Go на уровне дизайна разделены случаи ошибок и исключительных ситуаций, зачем пытаться делать ещё одну сущность и собрать их вместе?
                                          Как мне писать в этом случае код работайщий со сторонним кодом, когда у меня одна функция возвращает error, а вторая исключения, stack trace будет формироваться в обоих случаях? почему в с# это сделано только для некоторых классов но не для всех (уж не потому, что иметь 2х методов это пзцд)?
                                          Почему в статьях про Rust — http://habrahabr.ru/post/242269/ большинство говорят, что да это классно, а в статьях про Go — как там могли придумать вообще, верните мои исключения
                                            0
                                            Я не его приводил в пример, а просто написал эквивалентный вариант по Go коду.

                                            В принципе плохой пример. Ошибка логируется, а программа продолжает как ни в чём не бывало и падает на следующей строке из-за того, что предыдущий оператор не выполнился.
                                            Хороший пример — это вернуть err в вызывающую ф-цию.

                                            Тогда аналог на c#/java вообще бы не содержал бы никаких try/catch, ни одного лишнего символа в коде на обработку ошибок. Исключение прозрачно ушло бы наверх.

                                            Как мне писать в этом случае код работайщий со сторонним кодом, когда у меня одна функция возвращает error, а вторая исключения, stack trace будет формироваться в обоих случаях?

                                            На нагрузку на стек никак не влияет наличие try/catch, ведь trace надо будет собрать в любом случае, если где-то в глубине дерева вызовов случится panic.
                                              –3
                                              Падает? Та вы мисье почитайте о panic log.Fatal и т.д. ))
                                        0
                                        Go не даёт выбора.

                                        Для указанного случая — как раз таки даёт «из коробки». Только вместо try в main или транзакцию снизу кидается panic, которую наверху ловят через defer/recover.
                                          0
                                          Так надо вручную после каждого вызова кидать panic, если есть ошибка.
                              0
                              Получился оператор COMEFROM.
                                0
                                Что то мне это напоминает, ах да это же java а именно try-with-resources
                                    try (Statement stmt = con.createStatement()) {
                                        ResultSet rs = stmt.executeQuery(query);
                                        while (rs.next()) {
                                            System.out.println(---);
                                        }
                                    } catch (SQLException e) {
                                        JDBCTutorialUtilities.printSQLException(e);
                                    }
                                


                                Ваш подход очень похож. И не так уж плох.
                                • UFO just landed and posted this here
                                    0
                                    Этим можно пользоваться, если все нижележащие функции возвращают ошибку.
                                    Как только мы написали foo!, наша функция начинает кидать исключение и к ней этот подход перестаёт работать.

                                    Конечно, можно ловить исключение и преобразовывать его в ошибку, но программист никогда не будет уверен, то с чем он сейчас работает, не кидает ли скрытые исключения, просаживая производительность.
                                    • UFO just landed and posted this here

                                  Only users with full accounts can post comments. Log in, please.