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()
}

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

Комментарии (44)


  1. s_kozlov
    03.11.2015 12:17
    +4

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

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

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


    1. Core2Duo
      03.11.2015 12:45
      +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. psylosss
        04.11.2015 13:50
        +1

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


    1. OlegFX
      03.11.2015 22:48

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

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

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


  1. shoomyst
    03.11.2015 13:24
    +5

    Я возможно не совсем понял, но какой смысл в этой заметке, если это не работает? :)
    Или это какой-то совет разработчикам языка?


    1. OlegFX
      03.11.2015 22:51

      Это заметка о «фиче» которая не помешала бы лично мне. Как минимум ещё 9 человек со мной согласились — день прожит не зря.


  1. Blooderst
    03.11.2015 13:31
    +3

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

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

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


    1. s_kozlov
      03.11.2015 15:48

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


      1. divan0
        03.11.2015 17:08

        1. OlegFX
          03.11.2015 22:54
          +1

          Главный вопрос в ловле panic — производительность. Мне кажется, она тут страдает.


          1. bolk
            04.11.2015 04:43

            Почему она не страдает в случае try…catch, но страдает в случае panic…defer?


            1. isden
              04.11.2015 11:18
              +1

              > Почему она не страдает в случае try…catch

              Очень даже страдает. В той же Java, например.


              1. OlegFX
                04.11.2015 12:10
                +1

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

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


                1. neolink
                  04.11.2015 12:16

                  > try err {
                  > go func() {
                  > err = MyCoolFunc()
                  > } ()
                  > } catch {
                  >
                  > }
                  как будет работать такой код?


                  1. OlegFX
                    04.11.2015 12:20

                    Хороший вопрос.
                    Я бы не ловил в данном случае. То есть catch ловит только внутри текущей goroutine.


                    1. neolink
                      04.11.2015 13:57
                      +1

                      ну то есть мало проверить на nil надо ещё тогда проверить кто его туда записал, значит надо записать куда-то кто его туда записал и так далее


                      1. OlegFX
                        04.11.2015 21:34

                        Не надо ничего проверять. Это делает компилятор — для кода, относящегося к текущей goroutine, расставляет в местах инструкций для процессора переход по условию на точку входа в блок catch. Чуть выше я написал как именно он это делает.


                    1. mirrr
                      05.11.2015 19:29

                      А как-же замыкания?


              1. bolk
                04.11.2015 18:16

                Вы в контексте мою фразу прочитайте.


  1. vGrabko99
    03.11.2015 18:21

    Блоки try/cath выглядят весьма уродливо. Они запутывают структуру кода и смешивают обработку ошибок с нормальной обработкой. Что мешает сделать как то так?

    if err := client.Set("Fatal", " ", 0).Err(); err != nil {
    			log.Print(err)
    }
    


    1. neolink
      03.11.2015 18:39

      с одной стороны да, но пример из статьи так не записать без явного обявления переменных customVar и err


    1. justmara
      03.11.2015 21:26
      -1

      т.е. вот это менее уродливо?

      client.Set(«Fatal», " ", 0).Err()


      1. vGrabko99
        03.11.2015 21:29
        -1

        это redis либа если что. Где
        client — индикаторный соединения
        Fatal — имя ключа
        " " — содержание.

        Ну а если не нравиться можно написать свою либу. Благодаря Go я это могу сделать за 30 минут просто изучив команды редиса.


      1. neolink
        03.11.2015 21:49

        менее уродливо чем что?


        1. vGrabko99
          03.11.2015 22:21

          try/cath я полагаю


        1. justmara
          03.11.2015 22:31

          Чем try/catch, в качестве альтернативы которому был приведён этот код.
          Т.е. вызов метода Err() у результата void-метода Set(), по мнению, vGrabko99, меньше "запутывает структуру кода" и не "смешивает обработку ошибок с нормальной обработкой".


          1. vGrabko99
            03.11.2015 22:48

            согласен! Пример не удачен. Вот обработка ошибок бд (не кривая либа с гитхаба, а коробочная)

            if rows, err := DB.Query("SELECT * FROM users WHERE login=?", login);err !=nil {
            log.Fatal(err)
            }
            


            Вы тупо вместо трех этажных конструкций всё делаете сразу в if.


            1. OlegFX
              03.11.2015 22:57

              В данном конкретном случае областью видимости rows будет блок if/else. Не всегда это удобно.


              1. vGrabko99
                03.11.2015 23:14
                +1

                Все равно удобнее try/cath (я видел очень много этих try/cath и хочу сказать что даже передача rows в глобальную переменную для функции в else блоке будет намного более читаемо и понятно)


          1. neolink
            03.11.2015 23:16
            +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 практики работы с исключениями.


            1. vGrabko99
              03.11.2015 23:25
              -2

              Адско плюсую. Правда кармы не хватает))


            1. qw1
              04.11.2015 11:04
              +3

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

              Go не даёт выбора.


              1. neolink
                04.11.2015 11:47
                -1

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

                > Go не даёт выбора.
                а ещё выбора не дает Rust


                1. qw1
                  04.11.2015 13:32

                  Вот это именно то к чему склоняют исключения

                  Они склоняют, а мы не поддаёмся. Исключения не должны быть единственным способом передачи ошибки, как я писал выше. Программист должен взвешенно выбрать, что использовать.
                  Конструкции типа try { client.Set(«Fatal», " ", 0); } catch (Exception e) это антипаттерн, не надо их приводить в пример.


                  1. neolink
                    04.11.2015 13:47

                    Я не его приводил в пример, а просто написал эквивалентный вариант по Go коду.
                    В Rust и Go на уровне дизайна разделены случаи ошибок и исключительных ситуаций, зачем пытаться делать ещё одну сущность и собрать их вместе?
                    Как мне писать в этом случае код работайщий со сторонним кодом, когда у меня одна функция возвращает error, а вторая исключения, stack trace будет формироваться в обоих случаях? почему в с# это сделано только для некоторых классов но не для всех (уж не потому, что иметь 2х методов это пзцд)?
                    Почему в статьях про Rust — http://habrahabr.ru/post/242269/ большинство говорят, что да это классно, а в статьях про Go — как там могли придумать вообще, верните мои исключения


                    1. qw1
                      04.11.2015 17:11

                      Я не его приводил в пример, а просто написал эквивалентный вариант по Go коду.

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

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

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

                      На нагрузку на стек никак не влияет наличие try/catch, ведь trace надо будет собрать в любом случае, если где-то в глубине дерева вызовов случится panic.


                      1. vGrabko99
                        04.11.2015 17:17
                        -3

                        Падает? Та вы мисье почитайте о panic log.Fatal и т.д. ))


              1. OlegFX
                04.11.2015 11:54

                Go не даёт выбора.

                Для указанного случая — как раз таки даёт «из коробки». Только вместо try в main или транзакцию снизу кидается panic, которую наверху ловят через defer/recover.


                1. qw1
                  04.11.2015 13:24

                  Так надо вручную после каждого вызова кидать panic, если есть ошибка.


  1. printf
    04.11.2015 10:47

    Получился оператор COMEFROM.


  1. FanKiLL
    04.11.2015 17:47

    Что то мне это напоминает, ах да это же 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);
        }
    


    Ваш подход очень похож. И не так уж плох.


  1. guai
    07.11.2015 15:25
    +1

    было бы прикольно на уровне языка иметь возможность вызывать метод так или этак, с возвратом ошибки или с выбросом исключения.
    Например: result, err = foo() и result,! = foo()
    второй вариант это типа получить ошибку и сразу выбросить ее как исключение.
    ну или так: foo() и foo!()


    1. qw1
      07.11.2015 16:14

      Этим можно пользоваться, если все нижележащие функции возвращают ошибку.
      Как только мы написали foo!, наша функция начинает кидать исключение и к ней этот подход перестаёт работать.

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


      1. guai
        07.11.2015 16:37

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