Go: Предложение: встроенная функция проверки ошибок Go, "попробуй"

Созданный на 5 июн. 2019  ·  808Комментарии  ·  Источник: golang/go

Предложение: встроенная функция проверки ошибок Go, try

Это предложение было закрыто .

Прежде чем комментировать, пожалуйста, прочтите подробный проектный документ и просмотрите сводку обсуждения от 6 июня , сводку от 10 июня и, что наиболее важно, совет о том, как оставаться сосредоточенным _. Возможно, на ваш вопрос или предложение уже был дан ответ или сделано. Спасибо.

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

[Текст ниже был отредактирован, чтобы более точно отражать дизайн-документ.]

Встроенная функция try принимает в качестве аргумента одно выражение. Выражение должно оцениваться как n+1 значений (где n может быть равно нулю), где последнее значение должно иметь тип error . Он возвращает первые n значений (если есть), если (последний) аргумент ошибки равен нулю, в противном случае он возвращается из объемлющей функции с этой ошибкой. Например, такой код, как

f, err := os.Open(filename)
if err != nil {
    return …, err  // zero values for other results, if any
}

можно упростить до

f := try(os.Open(filename))

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

Это предложение сводит первоначальный эскизный проект, представленный на прошлогоднем GopherCon, к его сути. Если требуется увеличение или обертывание ошибок, есть два подхода: придерживаться проверенного оператора if или, в качестве альтернативы, «объявить» обработчик ошибок с помощью оператора defer :

defer func() {
    if err != nil { // no error may have occurred - check for it
        err = … // wrap/augment error
    }
}()

Здесь err — это имя результата ошибки объемлющей функции. На практике подходящие вспомогательные функции сократят объявление обработчика ошибок до однострочного. Например

defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)

(где fmt.HandleErrorf украшает *err ) хорошо читается и может быть реализован без необходимости в новых функциях языка.

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

Таким образом, try на первый взгляд может показаться необычным, но это просто синтаксический сахар, созданный специально для одной конкретной задачи, обработки ошибок с меньшим количеством шаблонов и достаточно хорошо справляющийся с этой задачей. Таким образом, он прекрасно вписывается в философию Go. try не предназначен для решения _всех_ ситуаций обработки ошибок; он разработан, чтобы хорошо обрабатывать _наиболее распространенные_ случаи, чтобы дизайн был простым и понятным.

Кредиты

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

Детальный проектный документ

https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

Инструмент tryhard для изучения влияния try

https://github.com/griesemer/трихард

Go2 LanguageChange Proposal error-handling

Самый полезный комментарий

Всем привет,

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

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

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

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

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

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

Что касается более широкой темы о том, что можно улучшить в обработке ошибок Go, мы были бы очень рады увидеть отчеты об опыте о том, какие аспекты обработки ошибок в Go являются для вас наиболее проблематичными в ваших собственных кодовых базах и рабочих средах, и какое влияние окажет хорошее решение. иметь в своем развитии. Если вы напишете такой отчет, разместите ссылку на странице Go2ErrorHandlingFeedback .

Спасибо всем, кто принял участие в этом обсуждении, здесь и в других местах. Как ранее указывал Расс Кокс, обсуждения в масштабах сообщества, подобные этому, в лучшем случае являются открытым исходным кодом . Мы очень благодарны всем за помощь в изучении этого конкретного предложения и, в более общем плане, в обсуждении лучших способов улучшить состояние обработки ошибок в Go.

Роберт Гриземер, для Комитета по рассмотрению предложений.

Все 808 Комментарий

Я согласен, что это лучший путь вперед: исправить наиболее распространенную проблему с помощью простого дизайна.

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

Предложение gophercon цитирует ? в рассмотренных идеях и приводит три причины, по которым оно было отвергнуто: первое («передачи потока управления, как правило, сопровождаются ключевыми словами») и третье («обработчики определены более естественно с ключевым словом, поэтому проверки тоже должны быть") больше не применяются. Второй — стилистический: в нем говорится, что, хотя постфиксный оператор лучше работает для цепочки, он все же может хуже читаться в некоторых случаях, например:

check io.Copy(w, check newReader(foo))

скорее, чем:

io.Copy(w, newReader(foo)?)?

но теперь у нас было бы:

try(io.Copy(w, try(newReader(foo))))

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

Итак, суть моего комментария заключается в том, что все три причины, указанные в предложении gophercon для отказа от использования ? , не относятся к этому предложению try ; ? является кратким, очень читабельным, он не затеняет структуру оператора (с его внутренней иерархией вызовов функций) и может быть объединен в цепочку. Он удаляет еще больше беспорядка из представления, не затеняя поток управления больше, чем уже делает предложенный try() .

Чтобы уточнить:

Делает

func f() (n int, err error) {
  n = 7
  try(errors.New("x"))
  // ...
}

вернуть (0, "х") или (7, "х")? Я бы предположил последнее.

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

Ваш пример возвращает 7, errors.New("x") . Это должно быть понятно из полного документа, который скоро будет представлен (https://golang.org/cl/180557).

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

Я действительно недоволен встроенной функцией _function_, влияющей на поток управления вызывающей стороны. Я ценю невозможность добавления новых ключевых слов в Go 1, но решение этой проблемы с помощью магических встроенных функций кажется мне неправильным. Затенение других встроенных функций не приводит к таким непредсказуемым результатам, как изменение потока управления.

Мне не нравится, как выглядит постфикс ? , но я думаю, что он все же лучше try() .

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

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

@dominikh В подробном предложении это подробно обсуждается, но обратите внимание, что panic и recover — это две встроенные функции, которые также влияют на поток управления.

Одно уточнение/предложение по улучшению:

if the last argument supplied to try, of type error, is not nil, the enclosing function’s error result variable (...) is set to that non-nil error value before the enclosing function returns

Можно ли вместо этого сказать is set to that non-nil error value and the enclosing function returns ? (с/до/и)

При первом чтении казалось, что before the enclosing function returns _в конце концов_ установит значение ошибки в какой-то момент в будущем прямо перед возвратом функции - возможно, в более поздней строке. Правильная интерпретация заключается в том, что try может привести к возврату текущей функции. Это удивительное поведение для текущего языка, поэтому приветствуется более четкий текст.

Я думаю, что это просто сахар, и небольшое количество ярых противников дразнили golang по поводу многократного использования ввода if err != nil ... , и кто-то воспринял это всерьез. Я не думаю, что это проблема. Не хватает только этих двух встроенных модулей:

https://github.com/purpleidea/mgmt/blob/a235b760dc3047a0d66bb0b9d63c25bc746ed274/util/errwrap/errwrap.go#L26

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

try(foobar())

Если foobar вернул (error, error)

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

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

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

Как и @dominikh , я также не согласен с необходимостью упрощенной обработки ошибок.

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

Однако, если бы мне пришлось выбирать между предложениями по упрощению обработки ошибок, это было бы моим предпочтительным предложением.

Было бы полезно, если бы это могло сопровождаться (на каком-то этапе приемлемости) инструментом для преобразования кода Go для использования try в некотором подмножестве функций, возвращающих ошибку, где такое преобразование может быть легко выполнено без изменение семантики. Мне приходят в голову три преимущества:

  • При оценке этого предложения люди могли бы быстро понять, как try можно использовать в их кодовой базе.
  • Если try появится в будущей версии Go, люди, скорее всего, захотят изменить свой код, чтобы использовать его. Наличие инструмента для автоматизации простых дел очень поможет.
  • Имея возможность быстро преобразовать большую кодовую базу для использования try , вы сможете легко изучить влияние реализации в масштабе. (Скажем, правильность, производительность и размер кода.) Однако реализация может быть достаточно простой, чтобы этим можно было пренебречь.

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

Я чувствую, что могу работать с try , если достаточно привыкну, но я также чувствую, что нам потребуется дополнительная поддержка IDE (или что-то в этом роде), чтобы выделить try для эффективного распознавания неявного потока в обзорах кода. /сеансы отладки

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

Я думаю, что общая проблема обработки ошибок, на которую жалуется сообщество, представляет собой комбинацию шаблона if err != nil И добавления контекста к ошибкам. В FAQ четко указано, что последнее намеренно опущено как отдельная проблема, но я чувствую, что тогда это становится неполным решением, но я готов дать ему шанс, подумав об этих двух вещах:

  1. Объявите err в начале функции.
    Это работает? Я вспоминаю проблемы с отложенными и неназванными результатами. Если это не так, предложение должно рассмотреть это.
func sample() (string, error) {
  var err error
  defer fmt.HandleErrorf(&err, "whatever")
  s := try(f())
  return s, nil
}
  1. Назначайте значения так же, как мы делали это в прошлом, но используйте вспомогательную функцию wrapf с шаблоном if err != nil .
func sample() (string, error) {
  s, err := f()
  try(wrapf(err, "whatever"))
  return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
  if err != nil {
    // err = wrapped error
  }
  return err
}

Если что-то работает, я могу с этим справиться.

func sample() (string, error) {
  var err error
  defer fmt.HandleErrorf(&err, "whatever")
  s := try(f())
  return s, nil
}

Так не пойдет. Отсрочка обновит локальную переменную err , которая не связана с возвращаемым значением.

func sample() (string, error) {
  s, err := f()
  try(wrapf(err, "whatever"))
  return s, nil
}
func wrapf(err error, format string, ...v interface{}) error {
  if err != nil {
    // err = wrapped error
  }
  return err
}

Это должно сработать. Однако он будет вызывать wrapf даже при нулевой ошибке.
Это также будет (продолжать) работать, и ИМО намного понятнее:

func sample() (string, error) {
  s, err := f()
  if err != nil {
      return "", wrap(err)
  }
  return s, nil
}

Никто не заставит вас использовать try .

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

try(foobar())

Если foobar вернул (error, error)

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

Не могли бы вы уточнить на примере?

@cespare : кто-то должен иметь возможность написать go fix , который переписывает существующий код, подходящий для try , так что он использует try . Может быть полезно понять, как можно упростить существующий код. Мы не ожидаем каких-либо существенных изменений в размере кода или производительности, поскольку try — это просто синтаксический сахар, заменяющий общий шаблон более коротким фрагментом исходного кода, который производит практически такой же код на выходе. Также обратите внимание, что код, который использует try , будет привязан к версии Go, по крайней мере той версии, в которой был введен try .

@lestrrat : Согласен, что нужно будет узнать, что try может изменить поток управления. Мы подозреваем, что IDE может достаточно легко выделить это.

@Goodwine : Как уже указал @randall77 , ваше первое предложение не сработает. Один вариант, о котором мы подумали (но не обсуждался в документе), — это возможность иметь некоторую заранее объявленную переменную, которая обозначает результат error (если он присутствует в первую очередь). Это устранило бы необходимость называть этот результат только для того, чтобы его можно было использовать в defer . Но это было бы еще большим волшебством; это не кажется оправданным. Проблема с присвоением имени возвращаемому результату, по сути, косметическая, и самое главное — это автоматически сгенерированные API, обслуживаемые go doc и друзьями. Было бы легко решить эту проблему в этих инструментах (см. также FAQ подробного проектного документа по этому вопросу).

@nictuku : Что касается вашего предложения по разъяснению (s/before/and/): я думаю, что код непосредственно перед абзацем, на который вы ссылаетесь, ясно дает понять, что именно происходит, но я понимаю вашу точку зрения, s/before/and/ может сделать прозу более понятной. Я внесу изменения.

См. CL 180637 .

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

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

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    if err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    defer r.Close()

    w, err := os.Create(dst)
    if err != nil {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    if _, err := io.Copy(w, r); err != nil {
        w.Close()
        os.Remove(dst)
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    if err := w.Close(); err != nil {
        os.Remove(dst)
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
}

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

func CopyFile(src, dst string) error {
    defer func() {
        err = fmt.Errorf("copy %s %s: %v", src, dst, err)
    }()
    r, err := try(os.Open(src))
    defer r.Close()

    w, err := try(os.Create(dst))

    defer w.Close()
    defer os.Remove(dst)
    try(io.Copy(w, r))
    try(w.Close())

    return nil
}

Вы можете посмотреть на это с первого взгляда и подумать, что он выглядит лучше, потому что в нем гораздо меньше повторяющегося кода. Однако было очень легко определить все места, которые функция вернула в первом примере. Все они были с отступом и начинались с return , за которым следовал пробел. Это связано с тем, что все условные возвраты _должны_ находиться внутри условных блоков, таким образом, они имеют отступ в соответствии со стандартами gofmt . return также, как было сказано ранее, является единственным способом выйти из функции, не говоря о том, что произошла катастрофическая ошибка. Во втором примере есть только один return , поэтому похоже, что единственное, что функция _ever_ должна вернуть, это nil . Последние два вызова try легко увидеть, но первые два немного сложнее, и было бы еще сложнее, если бы они были где-то вложенными, т.е. что-то вроде proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1])))) .

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

Кто-то это уже реализовал 5 лет назад. Если вы заинтересованы, вы можете
попробуй эту функцию

https://news.ycombinator.com/item?id=20101417

Я реализовал try() в Go пять лет назад с препроцессором AST и использовал его в реальных проектах, это было довольно приятно: https://github.com/lunixbochs/og

Вот несколько примеров того, как я использую его в функциях с тяжелой проверкой ошибок: https://github.com/lunixbochs/poxd/blob/master/tls.go#L13 .

Я ценю усилия, затраченные на это. Я думаю, что это самое удачное решение, которое я когда-либо видел. Но я думаю, что это вводит кучу работы при отладке. Распаковывать try и добавлять блок if каждый раз, когда я отлаживаю, и перепаковывать его, когда я закончу, утомительно. И у меня также есть некоторое передергивание по поводу волшебной переменной err, которую мне нужно учитывать. Меня никогда не беспокоила явная проверка ошибок, так что, возможно, я не тот человек, которого нужно спрашивать. Мне всегда казалось, что он «готов к отладке».

@griesemer
Моя проблема с предложенным вами использованием отсрочки в качестве способа обработки переноса ошибок заключается в том, что поведение из фрагмента, который я показал (повторяется ниже), не очень распространено, AFAICT, и поскольку это очень редко, я могу представить, что люди пишут это, думая, что это работает когда это не так.

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

var err error
defer fmt.HandleErrorf(err);

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

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

@deanveloper Это правда, что это предложение (и, если уж на то пошло, любое предложение, пытающееся сделать то же самое) удалит явно видимые операторы return из исходного кода - в конце концов, в этом весь смысл предложения, не так ли? Чтобы удалить шаблон из операторов if и returns , которые все одинаковы. Если вы хотите сохранить return , не используйте try .

Мы привыкли сразу распознавать операторы returnpanic ), потому что именно так этот тип потока управления выражается в Go (и многих других языках). Кажется невероятным, что мы также распознаем try как изменяющийся поток управления после некоторого привыкания к нему, точно так же, как мы это делаем для return . Не сомневаюсь, что и в этом поможет хорошая поддержка IDE.

У меня две проблемы:

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

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

Более мелкая стилистическая проблема заключается в том, что, к сожалению, много строк кода теперь будет заключено в try(actualThing()) . Я могу себе представить, что большинство строк кодовой базы завернуты в try() . Это прискорбно.

Я думаю, что эти проблемы будут решены с помощью настройки:

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

check() будет вести себя так же, как try() , но в общем откажется от передачи значений, возвращаемых функцией, и вместо этого предоставит возможность добавлять контекст. Это все равно вызовет возврат.

Это сохранит многие преимущества try() :

  • это встроенный
  • следует за существующим потоком управления WRT, чтобы отложить
  • это хорошо согласуется с существующей практикой добавления контекста к ошибкам
  • он согласуется с текущими предложениями и библиотеками для переноса ошибок, такими как errors.Wrap(err, "context message")
  • это приводит к чистому сайту вызова: в строке a, b, err := myFunc() нет шаблона
  • описание ошибок с помощью defer fmt.HandleError(&err, "msg") по-прежнему возможно, но это не нужно поощрять.
  • сигнатура check немного проще, потому что ей не нужно возвращать произвольное количество аргументов из функции, которую она обертывает.

@ s4n-gt Спасибо за эту ссылку. Я не знал об этом.

@Goodwine Точка взята. Причина, по которой не предоставляется более прямая поддержка обработки ошибок, подробно обсуждается в проектной документации. Также фактом является то, что в течение года или около того (с тех пор, как эскизы проектов были опубликованы на прошлогоднем Gophercon) не было найдено удовлетворительного решения для явной обработки ошибок. Вот почему в этом предложении это намеренно опущено (и вместо этого предлагается использовать defer ). Это предложение по-прежнему оставляет возможность для будущих улучшений в этом отношении.

В предложении упоминается изменение пакетного тестирования, позволяющее тестам и эталонным тестам возвращать ошибку. Хотя это не будет «скромным изменением библиотеки», мы могли бы рассмотреть возможность принятия func main() error . Это сделало бы написание небольших сценариев намного приятнее. Семантика будет эквивалентна:

func main() {
  if err := newmain(); err != nil {
    println(err.Error())
    os.Exit(1)
  }
}

Последняя критика. На самом деле это не критика самого предложения, а критика общего ответа на контраргумент «функция управления потоком».

Ответом на «Мне не нравится, что функция управляет потоком» будет то, что « panic также управляет потоком программы!». Однако есть несколько причин, по которым panic может сделать это, но они не относятся к try .

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

  2. panic — это имя, которое легко увидеть. Это вызывает беспокойство, и это необходимо. Если кто-то видит panic в кодовой базе, он должен немедленно подумать о том, как _избежать_ паники, даже если это тривиально.

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

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

Функция try не удовлетворяет ни одному из этих пунктов.

  1. Невозможно догадаться, что делает try , не посмотрев на него документацию. Многие языки используют это ключевое слово по-разному, что затрудняет понимание того, что оно означает в Go.

  2. try не бросается в глаза, особенно когда это функция. _Особенно_ когда подсветка синтаксиса выделяет его как функцию. _ОСОБЕННО_ после разработки на таком языке, как Java, где try рассматривается как ненужный шаблон (из-за проверенных исключений).

  3. try можно использовать в аргументе вызова функции, как в моем примере в моем предыдущем комментарии proc := try(os.FindProcess(try(strconv.Atoi(os.Args[1])))) . Это еще больше усложняет обнаружение.

Мои глаза игнорируют функции try , даже когда я специально их ищу. Мои глаза увидят их, но сразу перейдут к вызовам os.FindProcess или strconv.Atoi . try — это условный возврат. И поток управления, и возврат в Go поддерживаются на пьедестале. Весь поток управления внутри функции имеет отступ, а все возвращаемые значения начинаются с return . Смешивание обеих этих концепций вместе в вызове функции, который легко пропустить, кажется немного странным.


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

@buchanae интересно. Однако, как написано, форматирование в стиле fmt перемещается из пакета в сам язык, что открывает банку червей.

Однако, как написано, форматирование в стиле fmt перемещается из пакета в сам язык, что открывает банку червей.

Хорошая точка зрения. Более простой пример:

a, b, err := myFunc()
check(err, "calling myFunc")

@buchanae Мы решили сделать явную обработку ошибок более непосредственно связанной с try — см. подробный проектный документ , в частности раздел, посвященный итерациям проектирования. Ваше конкретное предложение check позволит увеличить количество ошибок только с помощью чего-то вроде fmt.Errorf API (как часть check ), если я правильно понимаю. В общем, люди могут захотеть делать все что угодно с ошибками, а не просто создавать новый, который ссылается на исходный через его строку ошибки.

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

a, b, c, ... err := try(someFunctionCall())
if err != nil {
   return ..., err
}

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

Я не следую этой линии:

defer fmt.HandleErrorf(&err, “foobar”)

Он сбрасывает входящую ошибку на пол, что необычно. Это предназначено для использования в чем-то более подобном?

defer fmt.HandleErrorf(&err, “foobar: %v”, err)

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

Я разделяю две озабоченности, поднятые @buchanae , относительно именованных возвратов и контекстных ошибок.

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

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

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

Существующий код:

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        err := dbfile.RunMigrations(db, dbMigrations)
        if err != nil {
                return nil, err
        }
        t := &Thing{
                thingy: thingy,
        }
        t.scanner, err = newScanner(thingy, db, client)
        if err != nil {
                return nil, err
        }
        t.initOtherThing()
        return t, nil
}

С try :

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        try(dbfile.RunMigrations(db, dbMigrations))
        t := &Thing{
                thingy:  thingy,
                scanner: try(newScanner(thingy, db, client)),
        }
        t.initOtherThing()
        return t, nil
}

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

@josharian Что касается main , возвращающего error : мне кажется, что ваша маленькая вспомогательная функция - это все, что нужно для получения того же эффекта. Я не уверен, что изменение подписи main оправдано.

Что касается примера «foobar»: это просто плохой пример. Я, вероятно, должен изменить его. Спасибо, что подняли это.

defer fmt.HandleErrorf(&err, “foobar: %v”, err)

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

РЕДАКТИРОВАТЬ: эта ошибка ранней оценки присутствует в примере
ближе к концу документа:

defer fmt.HandleErrorf(&err, "copy %s %s: %v", src, dst, err)

@adg Да, try можно использовать, как вы используете его в своем примере. Я оставлю ваши комментарии относительно именованных возвратов такими, какие они есть.

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

try не пытается обрабатывать все виды вещей, которые люди хотят делать с ошибками, а только те, которые мы можем найти практическим способом значительно упростить. Я считаю, что мой пример check идет по тому же пути.

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

Но, может быть, другие предложения добавят трассировку стека ко всем ошибкам? Я потерял след.

В примере, который дал @adg , есть две потенциальные ошибки, но нет контекста. Если newScanner и RunMigrations сами по себе не предоставляют сообщений, которые подсказывают вам, что из них пошло не так, вам остается только гадать.

В примере, который дал @adg , есть две потенциальные ошибки, но нет контекста. Если newScanner и RunMigrations сами по себе не предоставляют сообщений, которые подсказывают вам, что из них пошло не так, вам остается только гадать.

Это верно, и это выбор дизайна, который мы сделали в этом конкретном фрагменте кода. Мы часто переносим ошибки в другие части кода.

Я разделяю опасения @deanveloper и других, что это может усложнить отладку. Это правда, что мы можем не использовать его, но стили сторонних зависимостей не находятся под нашим контролем.
Если менее повторяющиеся if err := ... { return err } являются основной точкой, мне интересно, будет ли достаточно «условного возврата», как предложено https://github.com/golang/go/issues/27794 .

        return nil, err if f, err := os.Open(...)
        return nil, err if _, err := os.Write(...)

Я думаю, что ? будет лучше, чем try , и постоянное преследование defer за ошибку также будет непростой задачей.

Это также навсегда закрывает ворота для исключений, использующих try/catch .

Это также навсегда закрывает ворота для исключений с использованием try/catch.

Я более чем согласен с этим.

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

Как говорит @griesemer :

Опять же, это предложение не пытается решить все ситуации обработки ошибок. Я подозреваю, что в большинстве случаев try имеет смысл для кода, который сейчас выглядит примерно так:
a, b, c, ... err := try(someFunctionCall())
если ошибка != ноль {
вернуться ..., ошибиться
}
Существует очень много кода, который выглядит так. И не каждый фрагмент кода, выглядящий как этот, нуждается в дополнительной обработке ошибок. И там, где отсрочка неверна, можно использовать оператор if.

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

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

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

Это пример реального кода, который у меня есть -

func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    if err != nil {
        return err
    }
    var res int64
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("insert table: %w", err)
    }

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    if err != nil {
        tx.Rollback()
        return fmt.Errorf("insert table2: %w", err)
    }
    return tx.Commit()
}

По предложению:

Если требуется дополнение или перенос ошибки, есть два подхода: придерживаться проверенного оператора if или, альтернативно, «объявить» обработчик ошибок с помощью оператора defer:

Я думаю, что это попадет в категорию «придерживаться проверенного оператора if». Я надеюсь, что предложение может быть улучшено, чтобы решить и эту проблему.

Я настоятельно рекомендую команде Go отдать приоритет дженерикам , так как именно здесь Go слышит больше всего критики, и дождаться обработки ошибок. Сегодняшняя техника не такая болезненная (хотя go fmt должна стоять на одной строке).

Концепция try() имеет все проблемы check из проверки/дескриптора:

  1. Это не читается как Go. Людям нужен синтаксис присваивания без последующего нулевого теста, так как это похоже на Go. Это предполагалось в тринадцати отдельных ответах на проверку/обработку; см. _Повторяющиеся темы_ здесь:
    https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -темы

    f, #      := os.Open(...) // return on error
    f, #panic := os.Open(...) // panic on error
    f, #hname := os.Open(...) // invoke named handler on error
    // # is any available symbol or unambiguous pair
    
  2. Вложенность вызовов функций, которые возвращают ошибки, скрывает порядок операций и затрудняет отладку. Положение дел при возникновении ошибки, а значит и последовательность вызова, должно быть понятно, а здесь нет:
    try(step4(try(step1()), try(step3(try(step2())))))
    Теперь вспомним, что язык запрещает:
    f(t ? a : b) и f(a++)

  3. Было бы тривиально возвращать ошибки без контекста. Ключевым обоснованием проверки/обработки было поощрение контекстуализации.

  4. Он привязан к типу error и последнему возвращаемому значению. Если нам нужно проверить другие возвращаемые значения/типы на наличие исключительного состояния, мы вернемся к: if errno := f(); errno != 0 { ... }

  5. Он не предлагает несколько путей. Код, вызывающий API-интерфейсы хранилища или сети, обрабатывает такие ошибки иначе, чем те, которые возникают из-за неправильного ввода или неожиданного внутреннего состояния. Мой код делает одно из этого гораздо чаще, чем return err :

    • лог.Фатальный()
    • panic() для ошибок, которые никогда не должны возникать
    • запишите сообщение и повторите попытку

@gopherbot добавить Go2, LanguageChange

Как насчет использования только ? для развертки результата, как rust

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

func doSomething() (int, %error) {
  f := try(foo())
  ...
}
  • Мы не можем использовать try(), если doSomething не имеет %error в возвращаемых значениях.
  • Мы не можем использовать try(), если foo() не имеет ошибки в последнем из возвращаемых значений.

Сложно добавить новые требования/функции к существующему синтаксису.

Честно говоря, я думаю, что foo() также должен иметь %error.

Добавить еще 1 правило

  • %error может быть только один в списке возвращаемых значений функции.

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

handler := func(err error) error {
        return fmt.Errorf("foo failed: %v", err)  // wrap error
}

f := try(os.Open(filename), handler)  

или еще лучше вот так:

f := try(os.Open(filename), func(err error) error {
        return fmt.Errorf("foo failed: %v", err)  // wrap error
})  

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

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

РЕДАКТИРОВАТЬ: В-третьих, в идеале, язык go должен сначала получить дженерики, где важным вариантом использования будет возможность реализовать эту функцию try как дженерик, чтобы можно было закончить байкшединг, и каждый мог получить обработку ошибок, которую они предпочитают сами.

Хакерские новости имеют определенный смысл: try не ведет себя как обычная функция (она может возвращать значение), поэтому нехорошо придавать ей синтаксис, подобный функции. Синтаксис return или defer был бы более подходящим:

func CopyFile(src, dst string) (err error) {
        r := try os.Open(src)
        defer r.Close()

        w := try os.Create(dst)
        defer func() {
                w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        try io.Copy(w, r)
        try w.Close()
        return nil
}

@sheerun общий контраргумент этому заключается в том, что panic также является встроенной функцией изменения потока управления. Лично я с этим не согласен, но это правильно.

  1. Вторя @deanveloper выше , а также аналогичные комментарии других, я очень боюсь, что мы недооцениваем затраты на добавление нового, несколько тонкого и, особенно когда он встроен в другие вызовы функций, легко упускаемого из виду ключевого слова, которое управляет управлением стеком вызовов. поток. panic(...) — относительно явное исключение (каламбур) из правила, согласно которому return — единственный выход из функции. Я не думаю, что мы должны использовать его существование как оправдание для добавления третьего.
  2. Это предложение канонизировало бы возврат необработанной ошибки как поведение по умолчанию, а ошибки упаковки считали бы чем-то, на что вы должны согласиться, с дополнительной церемонией. Но, по моему опыту, это прямо противоположно хорошей практике. Я надеюсь, что предложение в этой области упростит или, по крайней мере, не усложнит добавление контекстной информации к ошибкам на месте ошибки.

возможно, мы можем добавить вариант с дополнительной функцией увеличения, что-то вроде tryf с этой семантикой:

func tryf(t1 T1, t1 T2, … tn Tn, te error, fn func(error) error) (T1, T2, … Tn)

переводит это

x1, x2, … xn = tryf(f(), func(err error) { return fmt.Errorf("foobar: %q", err) })

в это

t1, … tn, te := f()
if te != nil {
    if fn != nil {
        te = fn(te)
    }
    err = te
    return
}

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

Я обеспокоен тем, что try заменит традиционную обработку ошибок и в результате затруднит аннотирование путей ошибок.

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

a, b, err := doWork()
if err != nil {
  updateCounters()
  writeLogs()
  return err
}

Go — чрезвычайно социальный язык с общими идиомами, усиленными инструментами (fmt, lint и т. д.). Пожалуйста, имейте в виду социальные последствия этой идеи - будет тенденция использовать ее повсюду.

@politician , извините, но слово, которое вы ищете, не _социальный_, а _самоуверенный_. Go — самоуверенный язык программирования. В остальном я в основном согласен с тем, к чему вы клоните.

Инструменты сообщества @beoran , такие как Godep и различные линтеры, демонстрируют, что Go является одновременно самоуверенным и социальным, и многие драмы с языком проистекают из этой комбинации. Надеюсь, мы оба согласимся, что try не должна стать следующей дорамой.

@politician Спасибо за разъяснение, я не так понял. Я, конечно, могу согласиться с тем, что мы должны стараться избегать драмы.

Я в замешательстве.

Из блога: Ошибки — это ценности , с моей точки зрения, они предназначены для того, чтобы их ценили, а не игнорировали.

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

Мы не должны рассматривать error как exception , это похоже на импорт сложности не только для мышления, но и для кодирования, если мы это делаем.

«Используйте язык, чтобы упростить обработку ошибок». -- Роб Пайк

И еще, мы можем просмотреть этот слайд

image

Одна из ситуаций, когда проверка ошибок с помощью if кажется мне особенно неудобной, — это закрытие файлов (например, в NFS). Я думаю, в настоящее время мы должны написать следующее, возможны ли возвраты ошибок из .Close() ?

r, err := os.Open(src)
if err != nil {
    return err
}
defer func() {
    // maybe check whether a previous error occured?
    return r.Close()
}()

Может ли defer try(r.Close()) быть хорошим способом иметь управляемый синтаксис для обработки таких ошибок? По крайней мере, имело бы смысл как-то скорректировать пример CopyFile() в предложении, чтобы не игнорировать ошибки из r.Close() и w.Close() .

@seehuhn Ваш пример не скомпилируется, потому что отложенная функция не имеет возвращаемого типа.

func doWork() (err error) {
  r, err := os.Open(src)
  if err != nil {
    return err
  }
  defer func() {
    err = r.Close()  // overwrite the return value
  }()
}

Будет работать так, как вы ожидаете. Ключ — это именованное возвращаемое значение.

Мне нравится это предложение, но я думаю, что следует рассмотреть и пример @seehuhn :

defer try(w.Close())

вернет ошибку из Close() только в том случае, если ошибка еще не была установлена.
Этот шаблон используется так часто...

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

Первое, о чем я подумал, это заменить fmt.HandleErrorf tryf , которая ставит перед ошибкой дополнительный контекст.

func tryf(t1 T1, t1 T2, … tn Tn, te error, ts string) (T1, T2, … Tn)

Например (из реального кода, который у меня есть):

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil {
        return nil, errors.WithMessage(err, "load config dir")
    }
    b := bytes.NewBuffer(nil)
    if err = templates.ExecuteTemplate(b, "main", c); err != nil {
        return nil, errors.WithMessage(err, "execute main template")
    }
    buf, err := format.Source(b.Bytes())
    if err != nil {
        return nil, errors.WithMessage(err, "format main template")
    }
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    if err := ioutil.WriteFile(target, buf, 0644); err != nil {
        return nil, errors.WithMessagef(err, "write file %s", target)
    }
    // ...
}

Можно изменить на что-то вроде:

func (c *Config) Build() error {
    pkgPath := tryf(c.load(), "load config dir")
    b := bytes.NewBuffer(nil)
    tryf(emplates.ExecuteTemplate(b, "main", c), "execute main template")
    buf := tryf(format.Source(b.Bytes()), "format main template")
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    tryf(ioutil.WriteFile(target, buf, 0644), fmt.Sprintf("write file %s", target))
    // ...
}

Или, если я возьму пример @agnivade :

func (p *pgStore) DoWork() (err error) {
    tx := tryf(p.handle.Begin(), "begin transaction")
        defer func() {
        if err != nil {
            tx.Rollback()
        }
    }()
    var res int64
    tryf(tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res), "insert table")
    _, = tryf(tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res), "insert table2")
    return tryf(tx.Commit(), "commit transaction")
}

Однако @josharian поднял хороший вопрос, который заставляет меня сомневаться в этом решении:

Однако, как написано, форматирование в стиле fmt перемещается из пакета в сам язык, что открывает банку червей.

Я полностью согласен с этим предложением и вижу его преимущества на ряде примеров.

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

Для меня я бы предпочел, чтобы встроенная функция называлась pass . Я чувствую, что это дает лучшее представление о том, что происходит. В конце концов, вы не обрабатываете ошибку, а передаете ее обратно вызывающей стороне. try создает впечатление, что ошибка была обработана.

Я не согласен с этим, главным образом потому, что проблема, которую он призван решать («шаблонные операторы if, обычно связанные с обработкой ошибок»), просто не является проблемой для меня. Если бы все проверки ошибок были просто if err != nil { return err } , то я мог бы увидеть некоторую ценность в добавлении синтаксического сахара для этого (хотя Go по своей склонности является относительно свободным от сахара языком).

На самом деле то, что я хочу сделать в случае ненулевой ошибки, довольно сильно варьируется от одной ситуации к другой. Может быть, я хочу t.Fatal(err) . Может быть, я хочу добавить украшающее сообщение return fmt.Sprintf("oh no: %v", err) . Может быть, я просто регистрирую ошибку и продолжаю. Может быть, я устанавливаю флаг ошибки на своем объекте SafeWriter и продолжаю, проверяя флаг в конце какой-то последовательности операций. Возможно, мне нужно предпринять какие-то другие действия. Ни один из них нельзя автоматизировать с помощью try . Поэтому, если аргумент в пользу try заключается в том, что он удалит все блоки if err != nil , этот аргумент не имеет силы.

Устранит ли это _some_ из них? Конечно. Это привлекательное предложение для меня? Мех. Я искренне не беспокоюсь. Для меня if err != nil — это просто часть Go, как фигурные скобки или defer . Я понимаю, что это выглядит многословным и повторяющимся для людей, которые плохо знакомы с Go, но люди, которые плохо знакомы с Go, не в лучшем положении, чтобы вносить кардинальные изменения в язык по целому ряду причин.

Планка для существенных изменений в Go традиционно заключалась в том, что предлагаемое изменение должно решить проблему, которая (A) значительна, (B) затрагивает множество людей и (C) хорошо решается предложением. Я не убежден ни по одному из этих трех критериев. Я вполне доволен обработкой ошибок Go как таковой.

Повторяя @peterbourgon и @deanveloper , одна из моих любимых особенностей Go заключается в том, что поток кода ясен, а panic() не рассматривается как стандартный механизм управления потоком, как в Python.

Что касается дебатов о панике, panic() почти всегда появляется в строке сам по себе, потому что он не имеет значения. Вы не можете fmt.Println(panic("oops")) . Это значительно увеличивает его видимость и делает его гораздо менее сравнимым с try() , чем люди думают.

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

Один из примеров в предложении решает проблему для меня:

func printSum(a, b string) error {
        fmt.Println(
                "result:",
                try(strconv.Atoi(a)) + try(strconv.Atoi(b)),
        )
        return nil
}

Поток управления действительно становится менее очевидным и очень скрытым.

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

Хотя реакция на это может быть «тогда не используйте это», проблема в том, что другие библиотеки будут использовать это, и их отладка, чтение и использование становятся более проблематичными. Это побудит мою компанию никогда не переходить на go 2 и начать использовать только библиотеки, которые не используют try . Если я не одинок с этим, это может привести к разделению а-ля питон 2/3.

Кроме того, имя try автоматически подразумевает, что в конце концов catch появится в синтаксисе, и мы снова станем Java.

Итак, из-за всего этого я _категорически_ против этого предложения.

Мне не нравится имя try . Это подразумевает _попытку_ сделать что-то с высоким риском неудачи (у меня может быть культурное предубеждение против _попытки_, поскольку я не являюсь носителем английского языка), в то время как вместо этого будет использоваться try в случае, если мы ожидаем редких неудач. (мотивация желания уменьшить многословность обработки ошибок) и настроены оптимистично. Кроме того, try в этом предложении фактически _перехватывает_ ошибку, чтобы вернуть ее раньше. Мне нравится предложение pass от @HiImJC.

Помимо имени, я нахожу неудобным иметь return -подобный оператор, теперь спрятанный в середине выражений. Это нарушает стиль потока Go. Это усложнит проверку кода.

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

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

Мне нравится это предложение в целом. Взаимодействие с defer кажется достаточным, чтобы обеспечить эргономичный способ возврата ошибки, а также добавить дополнительный контекст. Хотя было бы неплохо решить проблему, на которую @josharian указал, как включить исходную ошибку в завернутое сообщение об ошибке.

Чего не хватает, так это эргономичного способа взаимодействия с предложениями по проверке ошибок на столе. Я считаю, что API должны очень внимательно относиться к тому, какие типы ошибок они возвращают, и по умолчанию, вероятно, должно быть «возвращаемые ошибки никоим образом не поддаются проверке». Затем должно быть легко перейти в состояние, в котором ошибки могут быть проверены точным образом, как задокументировано сигнатурой функции («Она сообщает об ошибке вида X в обстоятельстве A и ошибке вида Y в обстоятельстве B»).

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

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

Другим решением было бы перепрофилировать (отброшенную) идею наличия необязательного второго аргумента в try для определения/внесения в белый список типов ошибок, которые могут быть возвращены с этого сайта. Это немного проблематично, потому что у нас есть два разных способа определения «типа ошибки»: либо по значению ( io.EOF и т. д.), либо по типу ( *os.PathError , *exec.ExitError ). Легко указать типы ошибок, которые являются значениями, в качестве аргументов функции, но сложнее указать типы. Не уверен, как с этим справиться, но выбрасываю идею.

Проблему, на которую указал @josharian , можно избежать, отложив оценку ошибки:

defer func() { fmt.HandleErrorf(&err, "oops: %v", err) }()

Выглядит не очень, но должно работать. Однако я бы предпочел, чтобы это можно было решить, добавив новый глагол/флаг форматирования для указателей ошибок или, может быть, для указателей в целом, который печатает разыменованное значение, как с обычным %v . Для примера назовем его %*v :

defer fmt.HandleErrorf(&err, "oops: %*v", &err)

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

Редактировать:

Другой подход заключается в заключении указателя ошибки в структуру, реализующую Stringer :

type wraperr struct{ err *error }
func (w wraperr) String() string { return (*w.err).Error() }

...

defer handleErrorf(&err, "oops: %v", wraperr{&err})

Пара вещей с моей точки зрения. Почему мы так озабочены сохранением нескольких строк кода? Я рассматриваю это в том же духе, что и малые функции, которые считаются вредными .

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

Предложение, как указано, также имеет ряд неясных поведений, так что это уже проблематично, чем _explicit_ дополнительные ~3 строки, которые более понятны.

В настоящее время мы экономно используем шаблон отсрочки дома. Здесь есть статья, которая вызвала столь же неоднозначную реакцию, когда мы ее писали — https://bet365techblog.com/better-error-handling-in-go .

Однако мы использовали его в ожидании продвижения предложения check / handle .

Check/handle был гораздо более комплексным подходом к упрощению обработки ошибок. Его блок handle сохранил ту же область действия функции, что и та, в которой он был определен, в то время как любые операторы defer представляют собой новые контексты с некоторым объемом накладных расходов. Это, казалось, больше соответствовало идиомам go, в том смысле, что если вы хотите, чтобы поведение «просто возвращало ошибку, когда она возникает», вы могли объявить это явно как handle { return err } .

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

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

Если обработка ошибок на основе отсрочки будет A Thing, то, вероятно, следует добавить что-то вроде этого в пакет ошибок:

        f := try(os.Create(filename))
        defer errors.Deferred(&err, f.Close)

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

Встроенная функция, которая возвращает результат, сложнее продать, чем ключевое слово, которое делает то же самое.
Я бы хотел, чтобы это было ключевое слово, как в Zig[1].

  1. https://ziglang.org/documentation/master/#try

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

Мы привыкли сразу распознавать операторы возврата (и паники), потому что именно так этот тип потока управления выражается в Go (и многих других языках). Кажется невероятным, что мы также будем распознавать try как изменение потока управления после некоторого привыкания к нему, точно так же, как мы это делаем для return. Не сомневаюсь, что и в этом поможет хорошая поддержка IDE.

Я думаю, что это довольно надумано. В коде, созданном с помощью gofmt, return всегда соответствует /^\t*return / — это очень простой шаблон, который можно обнаружить на глаз без посторонней помощи. try , с другой стороны, может встречаться в любом месте кода, вложенного произвольно глубоко в вызовы функций. Никакое обучение не позволит нам сразу определить весь поток управления в функции без помощи инструментов.

Кроме того, функция, которая зависит от "хорошей поддержки IDE", будет иметь недостатки во всех средах, где нет хорошей поддержки IDE. Сразу приходят на ум инструменты проверки кода — Геррит выделит для меня все попытки? А что насчет людей, которые по разным причинам решили не использовать IDE или причудливую подсветку кода? Начнет ли acme выделять try ?

Языковая функция должна быть проста для понимания сама по себе, а не зависеть от поддержки редактора.

@kungfusheep Мне нравится эта статья. Забота о переносе в один только отложенный код уже немного повышает читабельность без try .

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

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

Хотя семантика вполне ясна. Каждый раз, когда вы видите try , он либо следует счастливому пути, либо возвращается. Я действительно не могу быть проще, чем это.

Это похоже на специальный макрос.

@dominikh try всегда соответствует /try\(/ , поэтому я не знаю, о чем вы на самом деле говорите. Он одинаково доступен для поиска, и каждый редактор, о котором я когда-либо слышал, имеет функцию поиска.

@qrpnxz Я думаю, что смысл, который он пытался донести, не в том, что вы не можете искать его программно, а в том, что его сложнее искать глазами. Регулярное выражение было просто аналогией с акцентом на /^\t* , что означает, что все возвращаемые значения явно выделяются тем, что находятся в начале строки (игнорируя начальные пробелы).

Если подумать, должно быть несколько общих вспомогательных функций. Возможно, они должны быть в пакете под названием «отложенный».

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

func Format(err error, message string, args ...interface{}) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf(...)
}

Это можно использовать без именованного возврата, например:

func foo(s string) (int, error) {
    n, err := strconv.Atoi(s)
    try(deferred.Format(err, "bad string %q", s))
    return n, nil
}

Вместо этого предлагаемый fmt.HandleError можно поместить в отложенный пакет, а мою вспомогательную функцию error.Defer можно назвать deferred.Exec , и может быть условный exec для выполнения процедур, только если ошибка не равна нулю.

Собрав это вместе, вы получите что-то вроде

func CopyFile(src, dst string) (err error) {
    defer deferred.Annotate(&err, "copy %s %s", src, dst)

    r := try(os.Open(src))
    defer deferred.Exec(&err, r.Close)

    w := try(os.Create(dst))
    defer deferred.Exec(&err, r.Close)

    defer deferred.Cond(&err, func(){ os.Remove(dst) })
    try(io.Copy(w, r))

    return nil
}

Другой пример:

func (p *pgStore) DoWork() (err error) {
    tx := try(p.handle.Begin())

    defer deferred.Cond(&err, func(){ tx.Rollback() })

    var res int64 
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    try(deferred.Format(err, "insert table")

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    try(deferred.Format(err, "insert table2"))

    return tx.Commit()
}

Это предложение ведет нас от того, чтобы везде иметь if err != nil , к тому, чтобы везде было try . Он сдвигает предложенную проблему и не решает ее.

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

Кроме того, я бы сказал, что if err != nil на самом деле более удобочитаем, чем try , потому что он не загромождает строку языка бизнес-логики, а находится прямо под ней:

file := try(os.OpenFile("thing")) // less readable than, 

file, err := os.OpenFile("thing")
if err != nil {

}

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

func getString() (string, error) { ... }

func caller() {
  defer func() {
    if err != nil { ... } // whether `err` must be defined or not is not shown in this example. 
  }

  // would call try internally, because a user is not 
  // assigning an error value. Also, it can add a compile error
  // for "defined and not used err value" if the user does not 
  // handle the error. 
  str := getString()
}

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

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

Например, в VSCode есть фрагмент под названием iferr , если вы наберете его и нажмете Enter, он расширится до полной инструкции по обработке ошибок... поэтому написание его никогда не кажется мне утомительным, а чтение позже лучше .

@джошариан

Хотя это не будет «скромным изменением в библиотеке», мы могли бы принять и ошибку func main().

Проблема в том, что не все платформы имеют четкую семантику того, что это значит. Ваше переписывание хорошо работает в «традиционных» программах Go, работающих в полной операционной системе, но как только вы пишете прошивку микроконтроллера или даже просто WebAssembly, не очень ясно, что будет означать os.Exit(1) . В настоящее время os.Exit является библиотечным вызовом, поэтому реализации Go бесплатны только для того, чтобы не предоставлять его. Однако форма main связана с языком.


Вопрос о предложении, на который, вероятно, лучше всего ответить «нет»: как try взаимодействует с вариативными аргументами? Это первый случай функции с переменным числом переменных, у которой нет своих переменных в последнем аргументе. Разрешено ли это:

var e []error
try(e...)

Не говоря уже о том, почему ты это сделал. Я подозреваю, что ответ будет «нет» (иначе продолжением будет «что, если длина расширенного фрагмента равна 0»). Просто упомянул об этом, чтобы в конечном итоге иметь в виду при формулировке спецификации.

  • Некоторые из замечательных особенностей Go заключаются в том, что текущие встроенные функции обеспечивают четкий поток управления, явная и поощряемая обработка ошибок, а разработчикам настоятельно не рекомендуется писать «волшебный» код. Предложение try не согласуется с этими основными принципами, поскольку оно будет способствовать сокращению за счет удобочитаемости потока управления.
  • Если это предложение будет принято, то, возможно, стоит подумать о том, чтобы сделать встроенный оператор try вместо функции . Затем он более совместим с другими операторами потока управления, такими как if . Кроме того, удаление вложенных скобок незначительно улучшает читаемость.
  • Опять же, если предложение будет принято, то, возможно, реализовать его без использования defer или чего-то подобного. Его уже нельзя реализовать в чистом виде (как указывали другие), поэтому он может также использовать более эффективную реализацию под капотом.

Я вижу в этом две проблемы:

  1. Он помещает МНОГО кода, вложенного в функции. Это добавляет много дополнительной когнитивной нагрузки, пытаясь разобрать код у себя в голове.
  1. Это дает нам места, где код может выйти из середины оператора.

Номер 2, я думаю, намного хуже. Все приведенные здесь примеры — это простые вызовы, которые возвращают ошибку, но вот что гораздо коварнее:

func doit(abc string) error {
    a := fmt.Sprintf("value of something: %s\n", try(getValue(abc)))
    log.Println(a)
    return nil
}

Этот код может выйти в середине этого sprintf, и будет СУПЕР легко пропустить этот факт.

Мой голос - нет. Это не улучшит код go. Это не облегчит чтение. Это не сделает его крепче.

Я говорил это раньше, и это предложение иллюстрирует это — я чувствую, что 90% жалоб на Go — это «я не хочу писать оператор if или цикл». Это убирает некоторые очень простые операторы if, но добавляет когнитивную нагрузку и позволяет легко пропустить точки выхода для функции.

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

func main() {
    f := try(os.Open("foo.txt"))
    defer f.Close()
}

Я не уверен, что попытка паники в main будет приемлемой.

Кроме того, это не было бы особенно полезно в тестах ( func TestFoo(t* testing.T) ), что, к сожалению, :(

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

Я бы предпочел что-то вроде try/catch, которое может выглядеть как

Предполагая, что foo() определяется как

func foo() (int, error) {}

Затем вы могли бы сделать

n := try(foo()) {
    case FirstError:
        // do something based on FirstError
    case OtherError:
        // do something based on OtherError
    default:
        // default behavior for any other error
}

Что переводится как

n, err := foo()
if errors.Is(err, FirstError) {
    // do something based on FirstError
if errors.Is(err, OtherError) {
    // do something based on OtherError
} else {
    // default behavior for any other error
}

Для меня обработка ошибок — одна из самых важных частей кодовой базы.
Уже слишком много кода go if err != nil { return err } , который возвращает ошибку из глубины стека без добавления дополнительного контекста или даже (возможно) хуже, добавляя контекст, маскируя основную ошибку оберткой fmt.Errorf .

Предоставление нового ключевого слова, которое является своего рода волшебством, которое ничего не делает, кроме замены if err != nil { return err } , кажется опасным путем.
Теперь весь код будет просто обернут вызовом try. Это несколько нормально (хотя читабельность отстой) для кода, который имеет дело только с ошибками внутри пакета, такими как:

func foo() error {
  /// stuff
  try(bar())
  // more stuff
}

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

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

*Примечание: на самом деле это не уменьшает количество повторений, а просто изменяет то, что повторяется, при этом делая код менее читаемым, потому что все заключено в try() .

И последнее замечание: чтение предложения сначала кажется приятным, затем вы начинаете разбираться со всеми подводными камнями (по крайней мере, перечисленными), и это просто как «ок, да, это слишком».


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

Поскольку контекст ошибки кажется повторяющейся темой...

Гипотеза: большинство функций Go возвращают (T, error) , а не (T1, T2, T3, error)

Что, если вместо определения try как try(T1, T2, T3, error) (T1, T2, T3) мы определили его как
try(func (args) (T1, T2, T3, error))(T1, T2, T3) ? (это приблизительно)

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

Затем, подобно make , открывается дверь к форме вызова с двумя аргументами, где второй аргумент является контекстом попытки (например, фиксированная строка, строка с %v ).

Это по-прежнему позволяет создавать цепочки для случая (T, error) , но вы больше не можете связывать несколько возвратов, что обычно не требуется IMO.

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

if err != nil { return err } — это то же самое, что «мы исправим это позже», как и try , за исключением того, что это более раздражает при прототипировании.

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

Было бы неплохо, если бы вы указали на некоторые из этих конкретных «ошибок», которые беспокоили вас, поскольку это тема.

Читабельность кажется проблемой, но как насчет того, чтобы go fmt представила try() так, чтобы он выделялся, например:

f := try(
    os.Open("file.txt")
)

@MrTravisB

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

Я не согласен. Предполагается, что вы хотите делать это достаточно часто, чтобы гарантировать сокращение именно для этого. Если вы этого не сделаете, это не помешает явно обрабатывать ошибки.

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

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

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

@boomlinde Совершенно верно . Это предложение пытается решить отдельный вариант использования, а не предоставляет инструмент для решения более крупной проблемы обработки ошибок. Я думаю, что основной вопрос, если именно то, что вы указали.

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

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

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

defer fmt.HandleErrorf(&err, “foobar”)

n := try(foo())

x : try(foo2())

Что, если мне нужна другая обработка ошибок, которые могут быть возвращены из foo() против foo2() ?

@MrTravisB

Что, если мне нужна другая обработка ошибок, которые могут быть возвращены из foo() по сравнению с foo2()?

Тогда вы используете что-то еще. Это то, к чему стремился @boomlinde .

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

@qrpnxz

f := try(os.Open("/foo"))
data := try(ioutil.ReadAll(f))
try(send(data))

(да я понимаю там ReadFile и что конкретно этот пример не лучший способ копировать данные куда-то, не в этом дело)

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

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

Управление потоком на основе выражений

panic может быть еще одной функцией управления потоком, но она не возвращает значения, что фактически делает ее оператором. Сравните это с выражением try , которое может встречаться где угодно.

recover имеет значение и влияет на управление потоком, но должно встречаться в операторе defer . Эти defer обычно являются функциональными литералами, recover вызывается только один раз, поэтому recover также эффективно используется как инструкция. Опять же, сравните это с try , которое может произойти где угодно.

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


Другое предложение

Разрешить такие заявления, как

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

для форматирования одной строки на gofmt , когда блок содержит только оператор return , и этот оператор не содержит новых строк. Например:

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

Обоснование

  • Не требует изменений языка
  • Правило форматирования простое и понятное
  • Правило может быть разработано таким образом, чтобы в нем gofmt сохраняло новые строки, если они уже существуют (например, структурные литералы). Выбор также позволяет писателю сделать акцент на некоторых обработках ошибок.
  • Если это не так, код может быть автоматически перенесен в новый стиль с помощью вызова gofmt
  • Это только для операторов return , так что это не будет злоупотреблять кодом для гольфа без необходимости.
  • Хорошо взаимодействует с комментариями, описывающими, почему могут возникать некоторые ошибки и почему они возвращаются. Использование множества вложенных выражений try плохо справляется с этой задачей.
  • Это уменьшает вертикальное пространство обработки ошибок на 66%.
  • Поток управления без выражений
  • Код читается гораздо чаще, чем пишется, поэтому его следует оптимизировать для читателя. Повторяющийся код, занимающий меньше места, полезен для читателя, тогда как try больше ориентирован на автора.
  • Люди уже предлагали try , существующие на нескольких линиях. Например, этот комментарий или этот комментарий , который вводит такой стиль, как
f, err := os.Open(file)
try(maybeWrap(err))
  • Стиль "попробовать на своей строке" устраняет любую двусмысленность в отношении того, какое значение err возвращается. Поэтому я подозреваю, что эта форма будет широко использоваться. Разрешение одного выровненного блока if - это почти то же самое, за исключением того, что в нем также явно указано, какие возвращаемые значения
  • Это не способствует использованию именованных возвратов или неясной упаковки на основе defer . Оба повышают барьер для ошибок переноса, а первый может потребовать изменений godoc
  • Нет необходимости обсуждать, когда использовать try по сравнению с традиционной обработкой ошибок.
  • Не исключает выполнения try или чего-то еще в будущем. Изменение может быть положительным, даже если try принимается
  • Нет отрицательного взаимодействия с библиотекой testing или функциями main . На самом деле, если предложение допускает любой однострочный оператор вместо простого возврата, это может уменьшить использование библиотек, основанных на утверждениях. Рассмотреть возможность
value, err := something()
if err != nil { t.Fatal(err) }
  • Нет негативного взаимодействия с проверкой конкретных ошибок. Рассмотреть возможность
n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }

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


Некоторые портированные примеры

Из https://github.com/golang/go/issues/32437#issuecomment-498941435

с попыткой

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        try(dbfile.RunMigrations(db, dbMigrations))
        t := &Thing{
                thingy:  thingy,
                scanner: try(newScanner(thingy, db, client)),
        }
        t.initOtherThing()
        return t, nil
}

С этим

func NewThing(thingy *foo.Thingy, db *sql.DB, client pb.Client) (*Thing, error) {
        err := dbfile.RunMigrations(db, dbMigrations))
        if err != nil { return nil, fmt.Errorf("running migrations: %v", err) }

        t := &Thing{thingy: thingy}
        t.scanner, err = newScanner(thingy, db, client)
        if err != nil { return nil, fmt.Errorf("creating scanner: %v", err) }

        t.initOtherThing()
        return t, nil
}

Он конкурентоспособен в использовании пространства, но при этом позволяет добавлять контекст к ошибкам.

Из https://github.com/golang/go/issues/32437#issuecomment-499007288

с попыткой

func (c *Config) Build() error {
    pkgPath := try(c.load())
    b := bytes.NewBuffer(nil)
    try(emplates.ExecuteTemplate(b, "main", c))
    buf := try(format.Source(b.Bytes()))
    target := fmt.Sprintf("%s.go", filename(pkgPath))
    try(ioutil.WriteFile(target, buf, 0644))
    // ...
}

С этим

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil { return nil, errors.WithMessage(err, "load config dir") }

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err != nil { return nil, errors.WithMessage(err, "execute main template") }

    buf, err := format.Source(b.Bytes())
    if err != nil { return nil, errors.WithMessage(err, "format main template") }

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
    // ...
}

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

@cpuguy83
Для меня это более читабельно с try . В этом примере я прочитал «открыть файл, прочитать все байты, отправить данные». При обычной обработке ошибок я бы прочитал «откройте файл, проверьте, была ли ошибка, обработка ошибок делает это, затем прочитайте все байты, теперь проверьте, не произошло ли что-то ...» Я знаю, что вы можете просмотреть err != nil s, но для меня try просто проще, потому что, когда я вижу это, я сразу понимаю поведение: возвращает if err != nil. Если у вас есть филиал, я должен посмотреть, что он делает. Оно могло сделать что угодно.

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

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

@zeebo Да, я в этом заинтересован.
В статье @kungfusheep использовалась такая проверка на ошибку в одну строку, и мне захотелось попробовать ее. Затем, как только я сохранился, gofmt расширил его на три строки, что было грустно. Многие функции в stdlib определены в одной строке, поэтому меня удивило, что gofmt расширяет это.

@qrpnxz

Мне довелось прочитать много кода go. Одна из лучших вещей в этом языке — это простота, которая исходит от того, что большая часть кода следует определенному стилю (спасибо gofmt).
Я не хочу читать кучу кода, завернутого в try(f()) .
Это означает, что будут либо расхождения в стиле/практике кода, либо линтеры вроде «о, вы должны были использовать здесь try() » (что, опять же, мне даже не нравится, и в этом смысл моих и других комментариев по этому предложению).

Объективно это не лучше, чем if err != nil { return err } , просто меньше печатать.


Последняя вещь:

Если вы прочтете предложение, то увидите, что ничто не мешает вам

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

@cpuguy83
Мой плохой парень с процессором. Я не это имел в виду.

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

Конечно, это не объективно лучше. Я говорил, что так мне удобнее читать. Я тщательно это сформулировал.

Еще раз извините за такой тон. Хотя это аргумент, я не хотел вас раздражать.

https://github.com/golang/go/issues/32437#issuecomment-498908380

Никто не заставит вас использовать try.

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

Конечно, мне не нужно его использовать. Но любой, с кем я пишу код, может использовать его и заставить меня попытаться расшифровать try(try(try(to()).parse().this)).easily()) . Это как сказать

Никто не заставит вас использовать пустой интерфейс{}.

В любом случае, Go довольно строго относится к простоте: gofmt делает весь код одинаковым. Счастливый путь остается левым, и все, что может быть дорогим или неожиданным, является явным . try , как предлагается, это поворот на 180 градусов от этого. Простота! = лаконичность.

По крайней мере, try должно быть ключевым словом с lvalue.

Это не _объективно_ лучше, чем if err != nil { return err } , просто меньше печатать.

Между ними есть одно объективное различие: try(Foo()) — это выражение. Для некоторых эта разница является недостатком (критика try(strconv.Atoi(x))+try(strconv.Atoi(y)) ). Для других эта разница является преимуществом по той же причине. Все еще объективно не лучше или хуже, но я также не думаю, что разницу следует заметать под ковер, и утверждение, что «просто меньше печатать», не оправдывает предложение.

@elagergren-spideroak трудно сказать, что try раздражает видеть на одном дыхании, а затем сказать, что это неявно на следующем. Ты должен выбрать один.

Обычно аргументы функций сначала помещаются во временные переменные. Я уверен, что было бы чаще видеть

this := try(to()).parse().this
that := try(this.easily())

чем ваш пример.

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

@josharian Что касается вашего комментария в https://github.com/golang/go/issues/32437#issuecomment -498941854, я не думаю, что здесь есть ошибка ранней оценки.

отложить fmt.HandleErrorf(&err, «foobar: %v», ошибка)

Неизмененное значение err передается в HandleErrorf и передается указатель на err . Мы проверяем, является ли err nil (с помощью указателя). Если нет, мы форматируем строку, используя немодифицированное значение err . Затем мы устанавливаем err в форматированное значение ошибки, используя указатель.

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

@ianlancetaylor , я думаю, что @josharian прав: «немодифицированное» значение err — это значение в момент помещения defer в стек, а не (предположительно предполагаемое) значение err устанавливается try перед возвратом.

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

this := try(to()).parse().this
that := try(this.easily())

^^ даже это просто ужасно. Первая строка, я должен прыгать туда-сюда, сопоставляя парен в голове. Даже вторую строчку, которая на самом деле довольно проста... действительно трудно читать.
Вложенные функции трудно читать.

parser, err := to()
if err != nil {
    return err
}
this := parser.parse().this
that, err := this.easily()
if err != nil {
    return err
}

^^ Это намного проще и лучше, ИМО. Это супер просто и понятно. да, это намного больше строк кода, мне все равно. Это очень очевидно.

@bcmills @josharian А, конечно, спасибо. Так что это должно быть

defer func() { fmt.HandleErrorf(&err, “foobar: %v”, err) }()

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

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

@ianlancetaylor , если fmt.HandleErrorf отправляет ошибку в качестве первого аргумента после формата, тогда реализация будет лучше, и пользователь всегда сможет ссылаться на нее с помощью %[1]v .

@natefinch Абсолютно согласен.

Интересно, будет ли подход в стиле ржавчины более приемлемым?
Обратите внимание, что это не предложение, просто обдумать его...

this := to()?.parse().this
that := this.easily()?

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


конечно, ржавчина также имеет try() почти так же, но... другой стиль ржавчины.

Это не _объективно_ лучше, чем if err != nil { return err } , просто меньше печатать.

Между ними есть одно объективное различие: try(Foo()) — это выражение. Для некоторых эта разница является недостатком (критика try(strconv.Atoi(x))+try(strconv.Atoi(y)) ). Для других эта разница является преимуществом по той же причине. Все еще объективно не лучше или хуже, но я также не думаю, что разницу следует заметать под ковер, и утверждение, что «просто меньше печатать», не оправдывает предложение.

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

@MrTravisB

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

Что конкретно я сказал, что именно это ваша точка зрения? Мне скорее кажется, что вы в корне неправильно поняли мою мысль, если думаете, что мы согласны.

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

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

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

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

Что делать, если мне нужна другая обработка ошибок для ошибок, которые могут быть возвращены из foo() по сравнению с foo2()

Опять же, тогда вы не используете try . Тогда вы ничего не получите от try , но и ничего не потеряете.

@cpuguy83

Интересно, будет ли подход в стиле ржавчины более приемлемым?

В предложении представлен аргумент против этого.

На данный момент я думаю, что try{}catch{} более читабельно :upside_down_face:

  1. Использование именованного импорта для обхода угловых случаев defer не только ужасно для таких вещей, как godoc, но, что наиболее важно, очень подвержено ошибкам. Мне все равно, я могу обернуть все это еще одним func() , чтобы обойти проблему, просто мне нужно помнить о других вещах, я думаю, что это поощряет «плохую практику».
  2. Никто не заставит вас использовать try.

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

  3. Я думаю, что такие примеры, как try(try(try(to()).parse().this)).easily()) , нереалистичны, это уже можно было бы сделать с другими функциями, и я думаю, что было бы справедливо, если бы те, кто просматривает код, попросили его разделить.
  4. Что, если у меня есть 3 места, которые могут выдать ошибку, и я хочу обернуть каждое место отдельно? try() делает это очень сложным, на самом деле try() уже препятствует ошибкам переноса, учитывая его сложность, но вот пример того, что я имею в виду:

    func before() error {
      x, err := foo()
      if err != nil {
        wrap(err, "error on foo")
      }
      y, err := bar(x)
      if err != nil {
        wrapf(err, "error on bar with x=%v", x)
      }
      fmt.Println(y)
      return nil
    }
    
    func after() (err error) {
      defer fmt.HandleErrorf(&err, "something failed but I don't know where: %v", err)
      x := try(foo())
      y := try(bar(x))
      fmt.Println(y)
      return nil
    }
    
  5. Опять же, тогда вы не используете try . Тогда вы ничего не получите от try , но и ничего не потеряете.

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

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

@boomlinde Мы согласны с тем, что это предложение пытается решить второстепенный вариант использования, и тот факт, что «если вам это не нужно, не используйте это», является основным аргументом в пользу этого. Как заявил @elagergren-spideroak, этот аргумент не работает, потому что, даже если я не хочу его использовать, другие захотят, что вынудит меня использовать его. По логике вашего аргумента в Go также должен быть тернарный оператор. И если вам не нравятся троичные операторы, не используйте их.

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

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

func exists(filename string) bool {
  _, err := os.Stat(filename)
  return err == nil
}

чтобы иметь возможность написать if exists(...) { ... } , хотя этот код молча игнорирует некоторые возможные ошибки. Если бы у меня было try , я бы, вероятно, не стал этого делать и просто вернул бы (bool, error) .

Будучи здесь хаотичным, я подкину идею добавления второй встроенной функции с именем catch , которая будет получать функцию, которая принимает ошибку и возвращает перезаписанную ошибку, а затем, если последующая catch вызывается, он перезапишет обработчик. Например:

func catch(handler func(err error) error) {
  // .. impl ..
}

Теперь эта встроенная функция также будет макроподобной функцией, которая будет обрабатывать следующую ошибку, которая будет возвращена try следующим образом:

func wrapf(format string, ...values interface{}) func(err error) error {
  // user defined
  return func(err error) error {
    return fmt.Errorf(format + ": %v", ...append(values, err))
  }
}
func sample() {
  catch(wrapf("something failed in foo"))
  try(foo()) // "something failed in foo: <error>"
  x := try(foo2()) // "something failed in foo: <error>"
  // Subsequent calls for catch overwrite the handler
  catch(wrapf("something failed in bar with x=%v", x))
  try(bar(x)) // "something failed in bar with x=-1: <error>"
}

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

func foo(a, b string) (int64, error) {
  return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func withContext(a, b string) (int64, error) {
  catch(func (err error) error {
    return fmt.Errorf("can't parse a: %s, b: %s, err: %v", a, b, err)
  })
  return try(strconv.Atoi(a)) + try(strconv.Atoi(b))
}
func moreExplicitContext(a, b string) (int64, error) {
  catch(func (err error) error {
    return fmt.Errorf("can't parse a: %s, err: %v", a, err)
  })
  x := try(strconv.Atoi(a))
  catch(func (err error) error {
    return fmt.Errorf("can't parse b: %s, err: %v", b, err)
  })
  y := try(strconv.Atoi(b))
  return x + y
}
func withHelperWrapf(a, b string) (int64, error) {
  catch(wrapf("can't parse a: %s", a))
  x := try(strconv.Atoi(a))
  catch(wrapf("can't parse b: %s", b))
  y := try(strconv.Atoi(b))
  return x + y
}
func before(a, b string) (int64, error) {
  x, err := strconv.Atoi(a)
  if err != nil {
    return 0,  fmt.Errorf("can't parse a: %s, err: %v", a, err)
  }
  y, err := strconv.Atoi(b)
  if err != nil {
    return 0,  fmt.Errorf("can't parse b: %s, err: %v", b, err)
  }
  return x + y
}

И еще о хаотичном настроении (чтобы помочь вам сопереживать) Если вам не нравится catch , вы можете не использовать его.

Теперь... Я не имею в виду последнее предложение, но мне кажется, что оно бесполезно для обсуждения, очень агрессивно, ИМО.
Тем не менее, если бы мы пошли по этому пути, я думаю, что вместо этого мы могли бы также иметь try{}catch(error err){} :stuck_out_tongue:

См. также #27519 — модель ошибки #id/catch.

Никто не заставит вас использовать try.

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

Извините, бойкость не была моей целью.

Я пытаюсь сказать, что try не является 100% решением. Существуют различные парадигмы обработки ошибок, которые плохо обрабатываются try . Например, если вам нужно добавить к ошибке контекст, зависящий от места вызова. Вы всегда можете вернуться к использованию if err != nil { для обработки этих более сложных случаев.

Это, безусловно, правильный аргумент, что try не может обрабатывать X для различных экземпляров X. Но часто обработка случая X означает усложнение механизма. Здесь есть компромисс: обработка X с одной стороны и усложнение механизма для всего остального. Все, что мы делаем, зависит от того, насколько распространен X и насколько сложно будет справиться с X.

Итак, говоря «Никто не заставит вас использовать try», я имею в виду, что я думаю, что рассматриваемый пример относится к 10%, а не к 90%. Это утверждение, безусловно, подлежит обсуждению, и я рад услышать контраргументы. Но в конце концов нам придется где-то подвести черту и сказать: «Да, try не справится с этим случаем. Вам придется использовать обработку ошибок в старом стиле. Извините».

Проблема не в том, что «try не может обработать этот конкретный случай обработки ошибок», а в том, что «try побуждает вас не оборачивать свои ошибки». Идея check-handle заставляла вас писать оператор возврата, поэтому написание обёртки ошибок было довольно тривиальным.

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

Идея check-handle заставляла вас писать оператор возврата, поэтому написание обёртки ошибок было довольно тривиальным.

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

Опираясь на озорную точку @Goodwine , вам действительно не нужны отдельные функции, такие как HandleErrorf , если у вас есть одна функция моста, например

func handler(err *error, handle func(error) error) {
  // nil handle is treated as the identity
  if *err != nil && handle != nil {
    *err = handle(*err)
  }
}

который вы бы использовали как

defer handler(&err, func(err error) error {
  if errors.Is(err, io.EOF) {
    return nil
  }
  return fmt.Errorf("oops: %w", err)
})

Вы можете сделать handler полумагической встроенной функцией, такой как try .

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

defer handler(func(err error) error {
  if errors.Is(err, io.EOF) {
    return nil
  }
  return fmt.Errorf("oops: %w", err)
})

Если бы это было волшебством, это должно было бы быть ошибкой времени компиляции, если бы оно использовалось где-либо, кроме как в качестве аргумента для defer . Вы могли бы пойти еще дальше и сделать его неявно отложенным, но defer handler читается довольно хорошо.

Поскольку он использует defer , он может вызывать свою функцию handle всякий раз, когда возвращается ненулевая ошибка, что делает ее полезной даже без try , поскольку вы можете добавить

defer handler(wrapErrWithPackageName)

вверху до fmt.Errorf("mypkg: %w", err) всего.

Это дает вам многое из старого предложения check / handle , но работает с отложенным естественным образом (и явно), избавляясь в большинстве случаев от необходимости явно указывать имя err возврат. Как и try , это относительно простой макрос, который (я полагаю) можно полностью реализовать во внешнем интерфейсе.

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

Мой плохой, ты прав.

Я имею в виду, что я думаю, что рассматриваемый пример относится к 10%, а не к 90%. Это утверждение, безусловно, подлежит обсуждению, и я рад услышать контраргументы. Но в конце концов нам придется где-то провести черту и сказать: «Да, попытка не справится с этим случаем. Вам придется использовать обработку ошибок в старом стиле. Извините».

Согласен, мое мнение таково, что эта линия должна быть нарисована при проверке EOF или аналогичного, а не при переносе. Но, возможно, если бы ошибки имели больше контекста, это больше не было бы проблемой.

Может ли try() автоматически переносить ошибки с полезным контекстом для отладки? Например, если xerrors становится errors , ошибки должны иметь что-то похожее на трассировку стека, которую может добавить try() , не так ли? Если да, то этого будет достаточно 🤔

Если цели (читая https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md):

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

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

вместо предложенного:

func printSum(a, b string) error {
        defer fmt.HandleErrorf(&err, "sum %s %s: %v", a,b, err) 
        x := try(strconv.Atoi(a))
        y := try(strconv.Atoi(b))
        fmt.Println("result:", x + y)
        return nil
}

Мы можем:

func printSum(a, b string) error {
        var err ErrHandler{HandleFunc : twoStringsErr("printSum",a,b)} 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        return nil
}

Что бы мы выиграли?
twoStringsErr может быть встроен в printSum или в общий обработчик, который знает, как перехватывать ошибки (в данном случае с двумя строковыми параметрами), поэтому, если у меня есть одинаковые повторяющиеся сигнатуры func, используемые во многих моих функциях, мне не нужно переписывать обработчик каждый раз. время
таким же образом я могу расширить тип ErrHandler следующим образом:

type ioErrHandler ErrHandler
func (i ErrHandler) Handle() ...{

}

или

type parseErrHandler ErrHandler
func (p parseErrHandler) Handle() ...{

}

или

type str2IntErrHandler ErrHandler
func (s str2IntErrHandler) Handle() ...{

}

и используйте это все вокруг моего кода:

func printSum(a, b string) error {
        var pErr str2IntErrHandler 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        return nil
}

Таким образом, на самом деле необходимо разработать триггер, когда для err.Error установлено значение не nil.
С помощью этого метода мы также можем:

func (s str2IntErrHandler) Handle() bool{
   **return false**
}

Что сообщит вызывающей функции о продолжении вместо возврата

И используйте разные обработчики ошибок в одной и той же функции:

func printSum(a, b string) error {
        var pErr str2IntErrHandler 
        var oErr overflowError 
        x, err.Error := strconv.Atoi(a)
        y,err.Error := strconv.Atoi(b)
        fmt.Println("result:", x + y)
        totalAsByte,oErr := sumBytes(x,y)
        sunAsByte,oErr := subtractBytes(x,y)
        return nil
}

и т.п.

Еще раз пробежимся по целям

  • убрать шаблон - готово
  • минимальные языковые изменения - сделано
  • охватывающий «наиболее распространенные сценарии» - больше, чем предлагаемый ИМО
  • добавление очень небольшой сложности к языку - сон
    Плюс - более легкий перенос кода из
x, err := strconv.Atoi(a)

к

x, err.Error := strconv.Atoi(a)

и на самом деле - лучшая читаемость (ИМО, опять же)

@guybrand , ты последний приверженец этой повторяющейся темы (которая мне нравится).

См. https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -themes

@guybrand Это похоже на совершенно другое предложение; Я думаю, вам следует оформить это как отдельную проблему, чтобы можно было сосредоточиться на обсуждении предложения @griesemer .

@natefinch согласен. Я думаю, что это больше направлено на улучшение опыта при написании Go, а не на оптимизацию для чтения. Интересно, могут ли макросы или фрагменты IDE решить проблему, не становясь при этом особенностью языка.

@Гудвин

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

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

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

Вместо того, чтобы просто сказать «ну, если вам это не нравится, не используйте это и заткнитесь» (так это читается), я думаю, было бы лучше попытаться исправить то, что многие пользователи считают недостатком. в дизайне.

Если это так, я извиняюсь. Я не хочу сказать, что вы должны делать вид, что этого не существует, если вам это не нравится. Это то, что очевидно, что есть случаи, когда try будет бесполезным, и что вы не должны использовать его в таких случаях, что для этого предложения, я считаю, обеспечивает хороший баланс между KISS и общей полезностью. Я не думал, что я был неясен в этом вопросе.

Спасибо всем за плодотворную обратную связь; это очень информативно.
Вот моя попытка сделать первоначальное резюме, чтобы лучше понять обратную связь. Заранее приношу извинения всем, кого я пропустил или исказил; Надеюсь, я правильно уловил общую суть.

0) С положительной стороны, @rasky , @adg , @eandre , @dpinela и другие явно выражали удовлетворение по поводу упрощения кода, которое обеспечивает try .

1) Наиболее важной проблемой является то, что try не поощряет хороший стиль обработки ошибок, а вместо этого продвигает «быстрый выход». ( @agnivade , @peterbourgon , @politician , @a8m , @eandre , @prologic , @kungfusheep , @cpuguy и другие выразили свою озабоченность по этому поводу.)

2) Многим людям не нравится идея встроенного или связанный с ним синтаксис функции, потому что он скрывает return . Лучше использовать ключевое слово. ( @sheerun , @Redundancy , @dolmen , @komuw , @RobertGrantEllis , @elagergren-spideroak). try также можно легко не заметить (@peterbourgon), особенно потому, что он может появляться в выражениях, которые могут быть произвольно вложенными. @natefinch обеспокоен тем, что try позволяет «слишком легко сбрасывать слишком много в одну строку», чего мы обычно стараемся избегать в Go. Кроме того, поддержка IDE для try может оказаться недостаточной (@dominikh); try должен «стоять сам по себе».

3) Для некоторых статус-кво явных операторов if не является проблемой, они им довольны ( @bitfield , @marwan-at-work, @natefinch). Лучше иметь только один способ делать что-либо (@gbbr); а явные операторы if лучше, чем неявные операторы return ( @DavexPro , @hmage , @prologic , @natefinch).
В том же духе @mattn обеспокоен «неявной привязкой» результата ошибки к try — связь явно не видна в коде.

4) Использование try усложнит отладку кода; например, может потребоваться переписать выражение try обратно в оператор if только для того, чтобы можно было вставить операторы отладки ( @deanveloper , @typeless , @networkimprov , другие).

5) Есть некоторые опасения по поводу использования именованных возвратов ( @buchanae , @adg).

Несколько человек внесли предложения по улучшению или изменению предложения:

6) Некоторые подхватили идею дополнительного обработчика ошибок (@beoran) или строки формата, предоставляемой try ( @unexge , @a8m , @eandre , @gotwarlost ), чтобы способствовать хорошей обработке ошибок.

7) @pierrec предположил, что gofmt может соответствующим образом форматировать выражения try , чтобы сделать их более заметными.
В качестве альтернативы можно сделать существующий код более компактным, разрешив gofmt форматировать операторы if , проверяющие наличие ошибок в одной строке (@zeebo).

8) @marwan-at-work утверждает, что try просто переносит обработку ошибок с операторов if на выражения try . Вместо этого, если мы действительно хотим решить проблему, Go должен «владеть» обработкой ошибок, сделав ее действительно неявной. Цель должна состоять в том, чтобы упростить (правильную) обработку ошибок и повысить продуктивность разработчиков (@cpuguy).

9) Наконец, некоторым людям не нравится имя try ( @beoran , @HiImJC , @dolmen ) или они предпочитают такой символ, как ? ( @twisted1919 , @leaxoy , другие) .

Некоторые комментарии к этому отзыву (соответственно пронумерованные):

0) Спасибо за положительный отзыв! :-)

1) Было бы хорошо узнать больше об этой проблеме. Текущий стиль кодирования с использованием операторов if для проверки на наличие ошибок настолько явный, насколько это возможно. Очень легко добавить дополнительную информацию к ошибке на индивидуальной основе (для каждого if ). Часто имеет смысл обрабатывать все обнаруженные в функции ошибки единым образом, что можно сделать с помощью defer — это уже сейчас возможно. Тот факт, что у нас уже есть все инструменты для хорошей обработки ошибок в языке, и проблема конструкции обработчика, не являющейся ортогональной defer , заставила нас отказаться от нового механизма исключительно для увеличения ошибок. .

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

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

4) Точка отладки является обоснованной проблемой. Если есть необходимость добавить код между обнаружением ошибки и return , необходимость переписать выражение try в оператор if может раздражать.

5) Именованные возвращаемые значения: Подробный документ обсуждает это подробно. Если это главная проблема в этом предложении, то, я думаю, мы находимся в хорошем положении.

6) Необязательный аргумент обработчика для try : это также обсуждается в подробном документе. См. раздел «Итерации проекта».

7) Использование gofmt для форматирования выражений try таким образом, чтобы они были более заметными, безусловно, является вариантом. Но это лишит некоторых преимуществ try при использовании в выражении.

8) Мы рассматривали проблему с точки зрения обработки ошибок ( handle ), а не с точки зрения тестирования ошибок ( try ). В частности, мы вкратце рассмотрели только введение понятия обработчика ошибок (по аналогии с первоначальным проектом, представленным на прошлогоднем Gophercon). Мысль заключалась в том, что если (и только если) объявлен обработчик, в присваиваниях с несколькими значениями, где последнее значение имеет тип error , это значение может быть просто опущено в присваивании. Компилятор неявно проверит, не равно ли оно нулю, и если да, то перейдет к обработчику. Это привело бы к полному исчезновению явной обработки ошибок и побудило бы всех вместо этого написать обработчик. Это казалось экстремальным подходом, потому что он был бы полностью неявным — факт проверки был бы невидимым.

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

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

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

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

func try(t1 T1, t1 T2, … tn Tn, te error) (T1, T2, … Tn)

должно быть:

func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)

Стоит ли анализировать общедоступный код Go для операторов проверки ошибок, чтобы попытаться выяснить, действительно ли большинство проверок ошибок повторяются или в большинстве случаев несколько проверок в одной и той же функции добавляют разную контекстуальную информацию? Предложение имело бы большой смысл в первом случае, но не помогло бы во втором. В последнем случае люди либо продолжат использовать if err != nil , либо откажутся от добавления дополнительного контекста, будут использовать try() и прибегнут к добавлению общего контекста ошибок для каждой функции, что, по мнению IMO, было бы вредным. Я думаю, что с будущими функциями значений ошибок мы ожидаем, что люди будут чаще оборачивать ошибки дополнительной информацией. Возможно, я неправильно понял предложение, но AFAIU, это помогает уменьшить шаблон только тогда, когда все ошибки одной функции должны быть обернуты ровно одним способом, и не помогает, если функция обрабатывает пять ошибок, которые, возможно, нужно обернуть по-разному. Не уверен, насколько распространены такие случаи в дикой природе (довольно распространены в большинстве моих проектов), но я обеспокоен тем, что try() может побудить людей использовать общие оболочки для каждой функции, даже если имеет смысл оборачивать разные ошибки. по-другому.

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

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

Если это основная проблема, решаемая этим предложением, я обнаружил, что этот «шаблон» составляет только ~ 1,4% моего кода в десятках общедоступных проектов с открытым исходным кодом на общую сумму ~ 60 000 SLOC.

Интересно, есть ли у кого-нибудь еще такая же статистика?

В гораздо большей кодовой базе, такой как сам Go, в общей сложности около ~ 1,6 млн SLOC, это составляет около ~ 0,5% кодовой базы, содержащей строки вроде if err != nil .

Действительно ли это самая серьезная проблема, которую нужно решить с помощью Go 2?

Большое спасибо @griesemer за то, что нашли время, чтобы рассмотреть все идеи и явно изложить мысли. Я думаю, что это действительно помогает с восприятием того, что сообщество услышано в процессе.

  1. @pierrec предположил, что gofmt может соответствующим образом отформатировать выражения try, чтобы сделать их более заметными.
    В качестве альтернативы можно сделать существующий код более компактным, разрешив gofmt форматировать операторы if, проверяющие наличие ошибок в одной строке (@zeebo).
  1. Использование gofmt для форматирования выражений try таким образом, чтобы они были более заметными, безусловно, является вариантом. Но это лишит некоторых преимуществ try при использовании в выражении.

Это ценные мысли о том, что gofmt требуется для форматирования try , но мне интересно, есть ли какие-либо мысли, в частности, о том, что gofmt разрешает проверку операторов if ошибка в одну строку. Предложение было объединено с форматированием try , но я думаю, что это совершенно ортогональная вещь. Спасибо.

@griesemer спасибо за невероятную работу по просмотру всех комментариев и ответов на большинство, если не на все отзывы 🎉

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

Например, с появлением нового LSP ( gopls ) кажется, что это идеальное место для анализа подписи функции и заботы о шаблоне обработки ошибок для разработчика, а также с надлежащей оберткой и проверкой.

@griesemer Я уверен, что это не очень хорошо продумано, но я попытался изменить ваше предложение ближе к тому, что мне было бы удобно здесь: https://www.reddit.com/r/golang/comments/bwvyhe /proposal_a_builtin_go_error_check_function_try/eq22bqa?utm_source=share&utm_medium=web2x

@zeebo Было бы легко сделать gofmt формате if err != nil { return ...., err } в одной строке. Предположительно, это будет только для этого конкретного типа шаблона if , а не для всех «коротких» операторов if ?

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

Текущий стиль:

a, b, c, ... err := BusinessLogic(...)
if err != nil {
   return ..., err
}

Однострочный if :

a, b, c, ... err := BusinessLogic(...)
if err != nil { return ..., err }

try в отдельной строке (!):

a, b, c, ... err := BusinessLogic(...)
try(err)

try как предложено:

a, b, c := try(BusinessLogic(...))

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

@ marwan-at-work Я не уверен, что вы предлагаете делать инструменты для вас. Вы предлагаете как-то скрыть обработку ошибок?

@дпинела

@guybrand Это похоже на совершенно другое предложение; Я думаю, вам следует оформить это как отдельную проблему, чтобы можно было сосредоточиться на обсуждении предложения @griesemer .

ИМО мое предложение отличается только синтаксисом, что означает:

  • Цели схожи по содержанию и приоритету.
  • Идея захвата каждой ошибки в своей строке и, соответственно (если не ноль), выхода из функции при прохождении через функцию обработчика аналогична (псевдо asm - это "jnz" и "call").
  • Это даже означает, что количество строк в теле функции (без отсрочки) и поток будут выглядеть точно так же (и, соответственно, AST, вероятно, тоже получится таким же).

поэтому основное различие заключается в том, оборачиваем ли мы исходный вызов функции с помощью try(func()), который всегда будет анализировать последнюю переменную для jnz вызова, или использовать для этого фактическое возвращаемое значение.

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

Однако я серьезно думаю о написании предложения, спасибо за идею.

@griesemer

Я не уверен, что вы предлагаете, что инструменты делают для вас. Вы предлагаете как-то скрыть обработку ошибок?

Наоборот: я предлагаю gopls написать для вас шаблон обработки ошибок.

Как вы упомянули в своем последнем комментарии:

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

Итак, суть проблемы в том, что программисту приходится писать много шаблонного кода. Так что проблема в том, чтобы писать, а не читать. Поэтому я предлагаю: пусть компьютер (tooling/gopls) напишет за программиста, проанализировав сигнатуру функции и поместив соответствующие пункты обработки ошибок.

Например:

// user begins to write this function: 
func openFile(path string) ([]byte, error) {
  file, err := os.Open(path)
  defer file.Close()
  bts, err := ioutil.ReadAll(file)
  return bts, nil
}

Затем пользователь запускает инструмент, возможно, просто сохраняя файл (аналогично тому, как обычно работает gofmt/goimports), и gopls просматривает эту функцию, анализирует возвращаемую сигнатуру и дополняет код следующим образом:

// user has triggered the tool (by saving the file, or code action)
func openFile(path string) ([]byte, error) {
  file, err := os.Open(path)
  if err != nil {
    return nil, fmt.Errorf("openFile: %w", err)
  }
  defer file.Close()
  bts, err := ioutil.ReadAll(file)
  if err != nil {
    return nil, fmt.Errorf("openFile: %w", err)
  }
  return bts, nil
}

Таким образом, мы получаем лучшее из обоих миров: мы получаем удобочитаемость/ясность текущей системы обработки ошибок, а программист не пишет никаких шаблонов для обработки ошибок. Более того, пользователь может позже модифицировать блоки обработки ошибок, чтобы добиться другого поведения: gopls может понять, что блок существует, и не будет его изменять.

Как инструмент узнает, что я намерен обработать err позже в функции, а не возвращаться раньше? Хоть и редко, но тем не менее код я написал.

Прошу прощения, если этот вопрос уже поднимался, но я не нашел упоминания об этом.

try(DoSomething()) мне хорошо читается и имеет смысл: код пытается что-то сделать. try(err) , OTOH, с точки зрения семантики кажется немного странным: как можно попробовать ошибку? На мой взгляд, можно было бы _проверить_ или _проверить_ ошибку, но _попробовать_ не кажется правильным.

Я понимаю, что разрешение try(err) важно из соображений согласованности: я полагаю, было бы странно, если бы try(DoSomething()) работало, а err := DoSomething(); try(err) — нет. Тем не менее, кажется, что try(err) выглядит немного неуклюже на странице. Я не могу придумать какие-либо другие встроенные функции, которые можно было бы так легко сделать такими странными.

У меня нет конкретных предложений по этому поводу, но я все же хотел сделать это замечание.

@griesemer Спасибо. Действительно, предложение должно было быть только для return , но я подозреваю, что было бы хорошо, если бы любой отдельный оператор был одной строкой. Например, в тесте можно было бы без изменений в библиотеке тестирования иметь

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

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

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

Мне больше нечего добавить из того, что, как мне кажется, еще не было сказано.


Я не видел, как ответить на этот вопрос в дизайн-документе. Что делает этот код:

func foo() (err error) {
    src := try(getReader())
    if src != nil {
        n, err := src.Read(nil)
        if err == io.EOF {
            return nil
        }
        try(err)
        println(n)
    }
    return nil
}

Насколько я понимаю, это будет обессахариваться в

func foo() (err error) {
    tsrc, te := getReader()
    if err != nil {
        err = te
        return
    }
    src := tsrc

    if src != nil {
        n, err := src.Read(nil)
        if err == io.EOF {
            return nil
        }

        terr := err
        if terr != nil {
            err = terr
            return
        }

        println(n)
    }
    return nil
}

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

@marwan-на-работе

Как вы упомянули в своем последнем комментарии:

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

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

Я думаю, что на самом деле все наоборот - меня больше всего раздражает текущий шаблон обработки ошибок не столько в том, что его нужно печатать, сколько в том, как он разбрасывает счастливый путь функции по вертикали по экрану, что затрудняет понимание в взгляд. Эффект особенно заметен в коде с большим количеством операций ввода-вывода, где между каждыми двумя операциями обычно есть блок шаблона. Даже упрощенная версия CopyFile занимает примерно 20 строк, хотя на самом деле она выполняет всего пять шагов: открыть исходный код, отложить закрытие исходного кода, открыть место назначения, скопировать источник -> место назначения, закрыть место назначения.

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

Мне нравится try в отдельной строке.
И я надеюсь, что он сможет указать функцию handler самостоятельно.

func try(error, optional func(error)error)
func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    try(err)

    handle := func(err error) error {
        tx.Rollback()
        return err
    }

    var res int64
    _, err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    try(err, handle)

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    try(err, handle)

    return tx.Commit()
}

@zeebo : примеры , которые я привел, - это переводы 1: 1. Первый (традиционный if ) не обработал ошибку, как и остальные. Если бы первый обрабатывал ошибку, и если бы это было единственное место, где проверяется ошибка в функции, первый пример (с использованием if ) мог бы быть подходящим выбором для написания кода. Если есть несколько проверок ошибок, каждая из которых использует одну и ту же обработку ошибок (оболочку), скажем, потому что все они добавляют информацию о текущей функции, можно использовать оператор defer для обработки всех ошибок в одном месте. При желании можно переписать if в try (или оставить их в покое). Если есть несколько ошибок, которые нужно проверить, и все они обрабатывают ошибки по-разному (что может быть признаком того, что проблема функции слишком широка и, возможно, ее необходимо разделить), использование if способ пойти. Да, одно и то же можно сделать несколькими способами, и правильный выбор зависит как от кода, так и от личного вкуса. Хотя мы стремимся в Go к «одному способу сделать одну вещь», это, конечно, уже не так, особенно для общих конструкций. Например, когда последовательность if - else - if становится слишком длинной, иногда более подходящей может быть последовательность switch . Иногда объявление переменной var x int выражает намерение лучше, чем x := 0 и так далее (хотя не всем это нравится).

Что касается вашего вопроса о «перезаписи»: нет, ошибки компиляции не будет. Обратите внимание, что перезапись происходит внутри (и может быть более эффективной, чем предполагает шаблон кода), и компилятору не нужно жаловаться на теневой возврат. В вашем примере вы объявили локальную переменную err во вложенной области. Конечно, try по-прежнему будет иметь прямой доступ к результирующей переменной err . Под обложкой переписывание может выглядеть так .

[отредактировано] PS: Лучшим ответом было бы: try не является голым возвратом (даже если переписывание выглядит так). В конце концов, один явно дает try аргумент, который содержит (или является) ошибкой, которая возвращается, если не nil . Теневая ошибка для голых возвратов - это ошибка в источнике (а не в базовом переводе источника. Компилятору не нужна ошибка.

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

Это сделает встроенный более универсальным (например, удовлетворит мою озабоченность в # 32219)

@pjebs Это было рассмотрено и отклонено. Пожалуйста, ознакомьтесь с подробным проектным документом (который явно относится к вашей проблеме по этому вопросу).

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

func doSomething() (error, error, error, error, error) {
   ...
}
try(try(try(try(try(doSomething)))))

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

Я подчеркиваю тонкое различие:

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

Если у него нет окончательного возвращаемого типа error => паника
При использовании try для объявлений переменных уровня пакета => паника (устраняет необходимость в соглашении MustXXX( ) )

Для модульных тестов небольшое изменение языка.

@mattn , я очень сомневаюсь, что какое-либо значительное количество людей будет писать такой код.

@pjebs , эта семантика - паника, если в текущей функции нет ошибки - это именно то, что обсуждается в проектном документе на https://github.com/golang/proposal/blob/master/design/32437-try-builtin. md#обсуждение.

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

Тем не менее, контекстная зависимость try считалась чреватой: например, поведение функции, содержащей вызовы try, могло незаметно измениться (с возможной паники на отсутствие паники и наоборот), если результат ошибки был добавлен или удален из подписи. Это казалось слишком опасным свойством. Очевидным решением было бы разделить функциональность try на две отдельные функции, must и try (очень похоже на то, что предлагается в issue #31442). Но для этого потребовались бы две новые встроенные функции, и только попытка try напрямую связана с неотложной потребностью в лучшей поддержке обработки ошибок.

@pjebs Это именно то, что мы рассмотрели в предыдущем предложении (см. Подробный документ, раздел «Итерации дизайна», 4-й абзац):

Кроме того, в попытке сделать try полезным не только внутри функций с результатом ошибки, семантика try зависела от контекста: если try использовался на уровне пакета или если он вызывался внутри функции без результата ошибки, try будет паниковать при обнаружении ошибки. (Кроме того, из-за этого свойства встроенная функция была вызвана must, а не try в этом предложении.)

Консенсус (внутренний Go Team) заключался в том, что try будет сбивать с толку, если он будет зависеть от контекста и действовать по-разному. Например, добавление результата ошибки к функции (или его удаление) может незаметно изменить поведение функции с паники на отсутствие паники (или наоборот).

@griesemer Спасибо за разъяснения по поводу перезаписи. Я рад, что он скомпилируется.

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

Что касается необходимости обрабатывать ошибки по-разному, я не согласен, что это признак того, что проблема функции слишком широка. Я переводил некоторые примеры заявленного реального кода из комментариев и помещал их в раскрывающийся список внизу моего исходного комментария , а пример в https://github.com/golang/go/issues/32437#issuecomment - 499007288 Я думаю, хорошо демонстрирует распространенный случай:

func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err != nil { return nil, errors.WithMessage(err, "load config dir") }

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err != nil { return nil, errors.WithMessage(err, "execute main template") }

    buf, err := format.Source(b.Bytes())
    if err != nil { return nil, errors.WithMessage(err, "format main template") }

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err != nil { return nil, errors.WithMessagef(err, "write file %s", target) }
    // ...
}

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

Я думаю, что это также сигнал о том, насколько незаметными были ошибки defer wrap(&err, "message: %v", err) и как они сбивали с толку даже опытных программистов Go.


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

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

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

Я хотел бы еще раз поднять идею обработчика в качестве второго аргумента для try , но с добавлением того, что аргумент обработчика должен быть _required_, но невозможным. Это делает обработку ошибки по умолчанию, а не исключение. В тех случаях, когда вы действительно хотите передать ошибку без изменений, просто укажите значение nil в обработчике, и try будет вести себя так же, как в исходном предложении, но аргумент nil будет действовать как визуальный сигнал о том, что ошибка ошибка не обрабатывается. Это будет легче поймать во время проверки кода.

file := try(os.Open("my_file.txt"), nil)

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

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

Что, если обработчик вызывается с ошибкой, отличной от нуля, а затем возвращает нулевой результат? Означает ли это, что ошибка «сброшена»? Или объемлющая функция должна вернуться с нулевой ошибкой?

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

func getOrCreateObject(obj *object) error {
    defaultObjectHandler := func(err error) error {
        if err == ObjectDoesNotExistErr {
            *obj = object{}
            return nil
        }
        return fmt.Errorf("getting or creating object: %v", err)
    }

    *obj = try(db.getObject(), defaultObjectHandler)
}

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

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

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

@velovix Мне нравится эта идея, но зачем нужен обработчик ошибок? Разве это не может быть nil по умолчанию? Зачем нужна «визуальная подсказка»?

@griesemer Что, если идея @velovix будет принята, но с builtin , содержащей предопределенную функцию, которая преобразует ошибку в панику, И мы удалим требование, чтобы всеобъемлющая функция имела возвращаемое значение ошибки?

Идея состоит в том, что если всеобъемлющая функция не возвращает ошибку, использование try без обработчика ошибок является ошибкой времени компиляции.

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

@pjebs

зачем нужен обработчик ошибок? Разве это не может быть nil по умолчанию? Зачем нужна «визуальная подсказка»?

Это необходимо для устранения опасений, что

  1. Предложение try в его нынешнем виде может отбить у людей охоту предоставлять контекст своим ошибкам, поскольку сделать это не так просто.

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

  1. Проблема из исходного документа предложения. Я процитировал это в своем первом комментарии:

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

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

Думая дальше об условном возврате, кратко упомянутом на https://github.com/golang/go/issues/32437#issuecomment -498947603.
Похоже на то
return if f, err := os.Open("/my/file/path"); err != nil
будет более соответствовать тому, как выглядит существующий if в Go.

Если мы добавим правило для оператора return if , которое
когда последнее выражение условия (например, err != nil ) отсутствует,и последняя переменная объявления в операторе return if имеет тип error ,тогда значение последней переменной будет автоматически сравниваться с nil как неявное условие.

Тогда оператор return if можно сократить до:
return if f, err := os.Open("my/file/path")

Что очень близко к соотношению сигнал-шум, которое обеспечивает try .
Если мы изменим return if на try , получится
try f, err := os.Open("my/file/path")
Это снова становится похожим на другие предложенные варианты try в этой теме, по крайней мере, синтаксически.
Лично я все еще предпочитаю return if try в этом случае, потому что это делает точки выхода функции очень явными. Например, при отладке я часто выделяю ключевое слово return в редакторе, чтобы определить все точки выхода большой функции.

К сожалению, похоже, это также недостаточно помогает с неудобствами вставки журнала отладки.
Если мы также не разрешим блок body для return if , например
Оригинал:

        return if f, err := os.Open("my/path") 

При отладке:

-       return if f, err := os.Open("my/path") 
+       return if f, err := os.Open("my/path") {
+               fmt.Printf("DEBUG: os.Open: %s\n", err)
+       }

Я полагаю, что смысл блока тела return if очевиден. Он будет выполнен до defer и завершится.

Тем не менее, у меня нет претензий к существующему подходу к обработке ошибок в Go.
Меня больше беспокоит то, как добавление новой обработки ошибок повлияет на нынешнее совершенство Go.

@velovix Нам очень понравилась идея try с явной функцией обработчика в качестве второго аргумента. Но было слишком много вопросов, на которые не было очевидных ответов, как говорится в дизайн-документе. На некоторые из них вы ответили так, как вам кажется разумным. Вполне вероятно (и это был наш опыт в Go Team), что кто-то другой думает, что правильный ответ совсем другой. Например, вы заявляете, что аргумент обработчика всегда должен предоставляться, но он может быть nil , чтобы сделать его явным, мы не заботимся об обработке ошибки. Что произойдет, если вы предоставите значение функции (а не литерал nil ), и это значение функции (хранящееся в переменной) окажется равным нулю? По аналогии с явным значением nil никакой обработки не требуется. Но другие могут возразить, что это ошибка в коде. Или, в качестве альтернативы, можно разрешить аргументы обработчика с нулевым значением, но тогда функция может непоследовательно обрабатывать ошибки в некоторых случаях, а не в других, и это не обязательно очевидно из кода, который делает, потому что кажется, что обработчик всегда присутствует . Другим аргументом было то, что лучше иметь объявление обработчика ошибок на верхнем уровне, потому что это ясно показывает, что функция действительно обрабатывает ошибки. Отсюда defer . Там, вероятно, больше.

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

@griesemer - IIUC, вы говорите, что для контекстов ошибок, зависящих от места вызова, текущий оператор if подходит. Принимая во внимание, что эта новая функция try полезна в случаях, когда полезна обработка нескольких ошибок в одном месте.

Я полагаю, что проблема заключалась в том, что, хотя простое выполнение if err != nil { return err} в некоторых случаях может быть уместным, обычно рекомендуется декорировать ошибку перед возвратом. И это предложение, кажется, касается предыдущего и мало что делает для последнего. По сути, это означает, что людям будет предложено использовать шаблон легкого возврата.

@agnivade Вы правы, это предложение никак не помогает с оформлением ошибок (но рекомендует использовать defer ). Одна из причин заключается в том, что языковые механизмы для этого уже существуют. Как только требуется декорирование ошибок, особенно для отдельных ошибок, дополнительный объем исходного текста для кода оформления делает if менее обременительным по сравнению с ним. Это случаи, когда украшение не требуется или украшение всегда одно и то же, когда шаблон становится видимой неприятностью и затем отвлекает от важного кода.

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

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

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

Я думаю, мы также можем добавить функцию catch , которая будет хорошей парой, поэтому:

func a() int {
  x := randInt()
  // let's assume that this is what recruiters should "fix" for us
  // or this happens in 3rd-party package.
  if x % 1337 != 0 {
    panic("not l33t enough")
  }
  return x
}

func b() error {
  // if a() panics, then x = 0, err = error{"not l33t enough"}
  x, err := catch(a())
  if err != nil {
    return err
  }
  sendSomewhereElse(x)
  return nil
}

// which could be simplified even further

func c() error {
  x := try(catch(a()))
  sendSomewhereElse(x)
  return nil
}

в этом примере catch() будет recover() паникой и return ..., panicValue .
конечно, у нас есть очевидный крайний случай, когда у нас есть функция, которая также возвращает ошибку. в этом случае, я думаю, было бы удобно просто передать значение ошибки.

так что, по сути, вы можете затем использовать catch() для фактического восстановления паники() и превращения их в ошибки.
это выглядит довольно забавно для меня, потому что Go на самом деле не имеет исключений, но в этом случае у нас есть довольно аккуратный шаблон try()-catch(), который также не должен взорвать всю вашу кодовую базу чем-то вроде Java ( catch(Throwable) в Main + throws LiterallyAnything ). вы можете легко обрабатывать чьи-то паники, как это были обычные ошибки. В настоящее время у меня около 6 миллионов LoC в Go в моем текущем проекте, и я думаю, что это упростит ситуацию, по крайней мере, для меня.

@griesemer Спасибо за подведение итогов обсуждения.

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

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

Например:

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

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

Какие еще опасения были? Я почти уверен, что на все они можно ответить одинаково разумным образом.

Наконец, как вы говорите, «одним из способов подсластить сделку было бы разрешить что-то вроде try (или любую аналогичную нотацию быстрого доступа) только в том случае, если где-то указан явный (возможно, пустой) обработчик». Я думаю, что если мы собираемся продолжить работу над этим предложением, нам действительно следует зайти так далеко, чтобы поощрить правильную обработку ошибок "явное лучше, чем неявное".

@griesemer

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

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

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

Как упомянул @beoran , определение обработчика как закрытия в верхней части функции будет выглядеть очень похоже по стилю, и я лично ожидаю, что люди будут использовать обработчики чаще всего. Хотя я ценю ясность, полученную благодаря тому факту, что все функции, обрабатывающие ошибки, будут использовать defer , это может стать менее ясным, когда функция должна изменить свою стратегию обработки ошибок на полпути вниз по функции. Затем будет два defer , на которые нужно посмотреть, и читателю придется рассуждать о том, как они будут взаимодействовать друг с другом. Это ситуация, когда я считаю, что аргумент обработчика был бы более понятным и эргономичным, и я действительно думаю, что это будет _относительно_ распространенный сценарий.

Можно ли заставить его работать без скобок?

То есть что-то вроде:
a := try func(some)

@Cyberax - Как уже упоминалось выше, очень важно внимательно прочитать проектную документацию перед публикацией. Так как это тема с большим трафиком, на которую подписано много людей.

В документе подробно обсуждаются операторы и функции.

Мне это нравится намного больше, чем августовская версия.

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

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

См., например:

Опровержение этих двух возражений соответственно:

  1. «мы решили, что [названные параметры результата] в порядке»
  2. "Никто не заставит вас использовать try " / это не подходит для 100% случаев

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

В частности, ни встречное предложение tryf (которое было опубликовано независимо дважды в этой теме), ни встречное предложение try(X, handlefn) (которое было частью итераций дизайна) не имели этой проблемы.

Я думаю, трудно спорить с тем, что try как есть оттолкнет людей от декорирования ошибок соответствующим контекстом и приведет к единому общему оформлению ошибки для каждой функции.

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

  1. В настоящее время параметр defer может быть только вызовом функции или метода. Разрешить defer также иметь имя функции или функциональный литерал, т.е.
defer func(...) {...}
defer packageName.functionName
  1. Когда panic или deferreturn сталкиваются с этим типом отсрочки, они вызывают функцию, передающую нулевое значение для всех своих параметров.

  2. Разрешить try иметь более одного параметра

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

Например, учитывая:

func errorfn() error {
    return errors.New("an error")
}


func f(fail bool) {
    defer func(err *error, a, b, c int) {
        fmt.Printf("a=%d b=%d c=%d\n", a, b, c)
    }
    if fail {
        try(errorfn, 1, 2, 3)
    }
}

произойдет следующее:

f(false)        // prints "a=0 b=0 c=0"
f(true)         // prints "a=1 b=2 c=3"

Код в https://github.com/golang/go/issues/32437#issuecomment -499309304 @zeebo можно было бы переписать как:

func (c *Config) Build() error {
    defer func(err *error, msg string, args ...interface{}) {
        if *err == nil || msg == "" {
            return
        }
        *err = errors.WithMessagef(err, msg, args...)
    }
    pkgPath := try(c.load(), "load config dir")

    b := bytes.NewBuffer(nil)
    try(templates.ExecuteTemplate(b, "main", c), "execute main template")

    buf := try(format.Source(b.Bytes()), "format main template")

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)
    // ...
}

И определение ErrorHandlef как:

func HandleErrorf(err *error, format string, args ...interface{}) {
        if *err != nil && format != "" {
                *err = fmt.Errorf(format + ": %v", append(args, *err)...)
        }
}

предоставит всем столь востребованный tryf бесплатно, не вытягивая строки формата в стиле fmt в основной язык.

Эта функция обратно совместима, поскольку defer не допускает использования функциональных выражений в качестве аргумента. Он не вводит новые ключевые слова.
Изменения, которые необходимо внести для его реализации, в дополнение к описанным в https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md :

  1. научить парсер новому виду отсрочки
  2. измените проверку типов, чтобы проверить, что внутри функции все отсрочки, которые имеют функцию в качестве параметра (вместо вызова), также имеют одинаковую сигнатуру
  3. измените средство проверки типов, чтобы убедиться, что параметры, переданные в try , соответствуют сигнатуре функций, переданных в defer
  4. изменить бэкэнд (?), чтобы сгенерировать соответствующий вызов deferproc
  5. изменить реализацию try , чтобы копировать его аргументы в аргументы отложенного вызова, когда он встречает вызов, отложенный новым видом отложенного вызова.

После сложностей чернового проекта check/handle я был приятно удивлен, увидев, что это гораздо более простое и прагматичное предложение получило распространение, хотя я разочарован тем, что против него было так много возражений.

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

Что касается некоторых конкретных моментов:

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

  2. Жаль, что try плохо работает с пакетом тестирования для функций, которые не возвращают значение ошибки. Мое собственное предпочтительное решение этой проблемы состояло бы в том, чтобы иметь вторую встроенную функцию (возможно, ptry или must ), которая всегда паниковала, а не возвращалась при обнаружении ненулевой ошибки и, следовательно, могла быть используется с вышеупомянутыми функциями (включая main ). Хотя эта идея была отклонена в текущей итерации предложения, у меня сложилось впечатление, что это был «близкий вызов», и поэтому он может подлежать пересмотру.

  3. Я думаю, что людям было бы трудно понять, что делают go try(f) или defer try(f) , и поэтому лучше просто запретить их вообще.

  4. Я согласен с теми, кто думает, что существующие методы обработки ошибок выглядели бы менее многословными, если бы go fmt не переписывал однострочные операторы if . Лично я бы предпочел простое правило, согласно которому это будет разрешено для _любого_ одиночного оператора if , связанного с обработкой ошибок или нет. На самом деле я никогда не мог понять, почему в настоящее время это не разрешено при написании однострочных функций, где тело помещается в ту же строку, что и объявление.

В случае ошибок декорирования

func myfunc()( err error){
try(thing())
defer func(){
err = errors.Wrap(err,"more context")
}()
}

Это кажется значительно более многословным и болезненным, чем существующие парадигмы, и не таким лаконичным, как проверка/обработка. Вариант try() без упаковки более лаконичен, но создается впечатление, что люди в конечном итоге будут использовать сочетание try и возвращать простые ошибки. Я не уверен, что мне нравится идея смешивания попыток и простых возвратов ошибок, но я полностью увлекся декорированием ошибок (и с нетерпением жду Is/As). Заставьте меня думать, что, хотя это синтаксически аккуратно, я не уверен, что хотел бы на самом деле использовать его. check/handle чувствовал что-то, что я бы принял более тщательно.

Мне очень нравится простота этого и подход «делай одно дело хорошо». В моем интерпретаторе GoAWK это было бы очень полезно — у меня есть около 100 конструкций if err != nil { return nil } , которые можно было бы упростить и привести в порядок, и это в довольно небольшой кодовой базе.

Я прочитал обоснование предложения сделать его встроенным, а не ключевым словом, и оно сводится к тому, что не нужно настраивать синтаксический анализатор. Но разве это не относительно небольшая проблема для разработчиков компиляторов и инструментальных средств, в то время как лишние скобки и проблемы с читабельностью типа «это выглядит как функция, но не является читабельностью» будут чем-то, что все программисты Go и код- Читателям приходится терпеть. На мой взгляд, аргумент (извините? :-) о том, что «но panic() управляет потоком», не работает, потому что panic и recovery по своей природе исключительны , тогда как try() будут быть нормальной обработкой ошибок и потоком управления.

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

Я за это предложение. Это позволяет избежать моей самой большой оговорки по поводу предыдущего предложения: неортогональности handle по отношению к defer .

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

Во-первых, несмотря на то, что это предложение не упрощает добавление контекстно-зависимого текста ошибки к ошибке, оно _делает_ упрощает добавление к ошибке информации для отслеживания ошибок фрейма стека: https://play.golang.org/p /YL1MoqR08E6

Во-вторых, try , возможно, является справедливым решением большинства проблем, лежащих в основе https://github.com/golang/go/issues/19642. Чтобы взять пример из этой проблемы, вы можете использовать try , чтобы каждый раз не записывать все возвращаемые значения. Это также потенциально полезно при возврате типов структур по значению с длинными именами.

func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
    xx := int(x)
    if f.NumGlyphs() <= xx {
        try(ErrNotFound)
    }
    i := f.cached.locations[xx+0]
    j := f.cached.locations[xx+1]
    if j < i {
        try(errInvalidGlyphDataLength)
    }
    if j-i > maxGlyphDataLength {
        try(errUnsupportedGlyphDataLength)
    }
    buf, err = b.view(&f.src, int(i), int(j-i))
    return buf, i, j - i, err
}

Мне тоже нравится это предложение.

И у меня есть просьба.

Как и make , можем ли мы позволить try принимать переменное количество параметров

  • попробовать (ф):
    как указано выше.
    возвращаемое значение ошибки является обязательным (как последний возвращаемый параметр).
    НАИБОЛЕЕ РАСПРОСТРАНЕННАЯ МОДЕЛЬ
  • try(f, doPanic bool):
    как указано выше, но если doPanic, то вместо возврата будет panic(err).
    В этом режиме нет необходимости возвращать значение ошибки.
  • попробуйте (ф, фн):
    как указано выше, но вызовите fn(err) перед возвратом.
    В этом режиме нет необходимости возвращать значение ошибки.

Таким образом, это одна встроенная функция, которая может обрабатывать все варианты использования, оставаясь при этом явной. Его преимущества:

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

Хотя повторяющиеся if err !=nil { return ... err } — это, безусловно, уродливое заикание, я с такими
которые думают, что предложение try() очень плохо читается и несколько неясно.
Использование именованных возвратов также проблематично.

Если такая уборка необходима, почему бы не использовать try(err) в качестве синтаксического сахара для
if err !=nil { return err } :

file, err := os.Open("file.go")
try(err)

для

file, err := os.Open("file.go")
if err != nil {
   return err
}

И если есть более одного возвращаемого значения, try(err) может return t1, ... tn, err
где t1, ... tn — нулевые значения других возвращаемых значений.

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

Еще лучше, я думаю, будет:

file, try(err) := os.Open("file.go")

Или даже

file, err? := os.Open("file.go")

Это последнее обратно совместимо (? в настоящее время не разрешено в идентификаторах).

(Это предложение связано с https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring-themes. Но примеры повторяющихся тем кажутся другими, потому что это было на этапе, когда явный дескриптор все еще обсуждался вместо того, чтобы оставить это к отсрочке.)

Спасибо команде go за это продуманное и интересное предложение.

Комментарий @rogpeppe , если try автоматически добавляет кадр стека, а не я, я согласен с тем, что это препятствует добавлению контекста.

@aarzilli - Итак, согласно вашему предложению, является ли предложение отсрочки обязательным каждый раз, когда мы даем дополнительные параметры tryf ?

Что произойдет, если я сделаю

try(ioutil.WriteFile(target, buf, 0644), "write file %s", target)

и не писать функцию отсрочки?

@агниваде

Что произойдет, если я сделаю (...) и не напишу функцию отсрочки?

ошибка проверки типа.

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

func (f *Font) viewGlyphData(b *Buffer, x GlyphIndex) (buf []byte, offset, length uint32, err error) {
    xx := int(x)
    if f.NumGlyphs() <= xx {
        try(ErrNotFound)
    }
    //...

Я полностью понимаю желание избежать необходимости выписывать return nil, 0, 0, ErrNotFound , но я бы предпочел решить это каким-то другим способом.

Слово try не означает «возврат». И вот как это используется здесь. На самом деле я бы предпочел, чтобы предложение изменилось так, чтобы try не могло принимать значение error напрямую, потому что я не хочу, чтобы кто-либо когда-либо писал такой код ^^ . Неправильно читается. Если бы вы показали этот код новичку, он бы понятия не имел, что делает эта попытка.

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

return default(ErrNotFound)

По крайней мере, это читается с какой-то логикой.

Но не будем злоупотреблять try для решения какой-то другой задачи.

@natefinch , если встроенная функция try названа check , как в исходном предложении, это будет check(err) , что читается значительно лучше, imo.

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

a, b := try(1, f(), err)

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

@natefinch Если вы представляете это как панику, которая поднимается на один уровень, а затем делает другие вещи, это кажется довольно грязным. Однако я осмысливаю это по-другому. Функции, которые возвращают ошибки в Go, фактически возвращают Result, если использовать терминологию Rust. try — это утилита, которая распаковывает результат и либо возвращает «результат ошибки», если error != nil , либо распаковывает часть T результата, если error == nil .

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

@ugorji Вариант try(f, bool) , который вы предлагаете, звучит как must из #32219.

@ugorji Вариант try(f, bool) , который вы предлагаете, звучит как must из #32219.

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

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

func foo() error {
  defer fmt.HandleErrorf(try(), "important foo context info")
  try(bar())
  try(baz())
  try(etc())
}

@угоржи
Я думаю, что логическое значение в try(f, bool) затруднит чтение и легко пропустит. Мне нравится ваше предложение, но в случае паники я думаю, что пользователям можно было бы оставить возможность написать это внутри обработчика из вашего третьего маркера, например try(f(), func(err error) { panic('at the disco'); }) , это делает его более явным для пользователей, чем скрытый try(f(), true) , которую легко не заметить, и я не думаю, что встроенные функции должны вызывать панику.

@угоржи
Я думаю, что логическое значение в try(f, bool) затруднит чтение и легко пропустит. Мне нравится ваше предложение, но в случае паники я думаю, что пользователям можно было бы оставить возможность написать это внутри обработчика из вашего третьего маркера, например try(f(), func(err error) { panic('at the disco'); }) , это делает его более явным для пользователей, чем скрытый try(f(), true) , которую легко не заметить, и я не думаю, что встроенные функции должны вызывать панику.

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

@patrick-nyt является еще одним сторонником _синтаксиса присваивания_ для запуска нулевого теста, в https://github.com/golang/go/issues/32437#issuecomment -499533464.

Эта концепция появляется в 13 отдельных ответах на предложение проверить/обработать
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback#recurring -темы

f, ?return := os.Open(...)
f, ?panic  := os.Open(...)

Почему? Потому что он читается как Go 1, а try() и check — нет.

Одно из возражений против try , по-видимому, состоит в том, что это выражение. Вместо этого предположим, что существует унарный постфиксный оператор ? , который означает возврат, если не nil. Вот стандартный пример кода (при условии, что предложенный мной отложенный пакет добавлен):

func CopyFile(src, dst string) error {
    var err error // Don't need a named return because err is explicitly named
    defer deferred.Annotate(&err, "copy %s %s", src, dst)

    r, err := os.Open(src)
    err?
    defer deferred.AnnotatedExec(&err, r.Close)

    w, err := os.Create(dst)
    err?
    defer deferred.AnnotatedExec(&err, r.Close)

    defer deferred.Cond(&err, func(){ os.Remove(dst) })
    _, err = io.Copy(w, r)

    return err
}

Пример pgStore:

func (p *pgStore) DoWork() error {
    tx, err := p.handle.Begin()
    err?

    defer deferred.Cond(&err, func(){ tx.Rollback() })

    var res int64 
    err = tx.QueryRow(`INSERT INTO table (...) RETURNING c1`, ...).Scan(&res)
    // tricky bit: this would not change the value of err 
    // but the deferred.Cond would still be triggered by err being set before
    deferred.Format(err, "insert table")?

    _, err = tx.Exec(`INSERT INTO table2 (...) VALUES ($1)`, res)
    deferred.Format(err, "insert table2")?

    return tx.Commit()
}

Мне нравится это от @jargv :

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

Но вместо того, чтобы перегружать имя try на основе количества аргументов, я думаю, что может быть другая встроенная магия, скажем, reterr или что-то в этом роде.

Я ознакомился с некоторыми очень часто используемыми пакетами, ища код go, который "страдает" от обработки ошибок, но, должно быть, был хорошо продуман перед написанием, пытаясь понять, что "магия" могла бы сделать предложенная функция try().
В настоящее время, если я не понял предложение неправильно, многие из них (например, не сверхбазовая обработка ошибок) не получат многого или должны будут остаться со «старым» стилем обработки ошибок.
Пример из сети/http/request.go:

func (r *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) {
`

trace := httptrace.ContextClientTrace(r.Context())
if trace != nil && trace.WroteRequest != nil {
    defer func() {
        trace.WroteRequest(httptrace.WroteRequestInfo{
            Err: err,
        })
    }()
}

// Find the target host. Prefer the Host: header, but if that
// is not given, use the host from the request URL.
//
// Clean the host, in case it arrives with unexpected stuff in it.
host := cleanHost(r.Host)
if host == "" {
    if r.URL == nil {
        return errMissingHost
    }
    host = cleanHost(r.URL.Host)
}

// According to RFC 6874, an HTTP client, proxy, or other
// intermediary must remove any IPv6 zone identifier attached
// to an outgoing URI.
host = removeZone(host)

ruri := r.URL.RequestURI()
if usingProxy && r.URL.Scheme != "" && r.URL.Opaque == "" {
    ruri = r.URL.Scheme + "://" + host + ruri
} else if r.Method == "CONNECT" && r.URL.Path == "" {
    // CONNECT requests normally give just the host and port, not a full URL.
    ruri = host
    if r.URL.Opaque != "" {
        ruri = r.URL.Opaque
    }
}
if stringContainsCTLByte(ruri) {
    return errors.New("net/http: can't write control character in Request.URL")
}
// TODO: validate r.Method too? At least it's less likely to
// come from an attacker (more likely to be a constant in
// code).

// Wrap the writer in a bufio Writer if it's not already buffered.
// Don't always call NewWriter, as that forces a bytes.Buffer
// and other small bufio Writers to have a minimum 4k buffer
// size.
var bw *bufio.Writer
if _, ok := w.(io.ByteWriter); !ok {
    bw = bufio.NewWriter(w)
    w = bw
}

_, err = fmt.Fprintf(w, "%s %s HTTP/1.1\r\n", valueOrDefault(r.Method, "GET"), ruri)
if err != nil {
    return err
}

// Header lines
_, err = fmt.Fprintf(w, "Host: %s\r\n", host)
if err != nil {
    return err
}
if trace != nil && trace.WroteHeaderField != nil {
    trace.WroteHeaderField("Host", []string{host})
}

// Use the defaultUserAgent unless the Header contains one, which
// may be blank to not send the header.
userAgent := defaultUserAgent
if r.Header.has("User-Agent") {
    userAgent = r.Header.Get("User-Agent")
}
if userAgent != "" {
    _, err = fmt.Fprintf(w, "User-Agent: %s\r\n", userAgent)
    if err != nil {
        return err
    }
    if trace != nil && trace.WroteHeaderField != nil {
        trace.WroteHeaderField("User-Agent", []string{userAgent})
    }
}

// Process Body,ContentLength,Close,Trailer
tw, err := newTransferWriter(r)
if err != nil {
    return err
}
err = tw.writeHeader(w, trace)
if err != nil {
    return err
}

err = r.Header.writeSubset(w, reqWriteExcludeHeader, trace)
if err != nil {
    return err
}

if extraHeaders != nil {
    err = extraHeaders.write(w, trace)
    if err != nil {
        return err
    }
}

_, err = io.WriteString(w, "\r\n")
if err != nil {
    return err
}

if trace != nil && trace.WroteHeaders != nil {
    trace.WroteHeaders()
}

// Flush and wait for 100-continue if expected.
if waitForContinue != nil {
    if bw, ok := w.(*bufio.Writer); ok {
        err = bw.Flush()
        if err != nil {
            return err
        }
    }
    if trace != nil && trace.Wait100Continue != nil {
        trace.Wait100Continue()
    }
    if !waitForContinue() {
        r.closeBody()
        return nil
    }
}

if bw, ok := w.(*bufio.Writer); ok && tw.FlushHeaders {
    if err := bw.Flush(); err != nil {
        return err
    }
}

// Write body and trailer
err = tw.writeBody(w)
if err != nil {
    if tw.bodyReadError == err {
        err = requestBodyReadError{err}
    }
    return err
}

if bw != nil {
    return bw.Flush()
}
return nil

}
`

или как используется в тщательном тесте, таком как pprof/profile/profile_test.go:
`
func checkAggregation(prof *Profile, a *aggTest) error {
// Проверяем, что общее количество выборок для строк сохранилось.
всего: = int64 (0)

samples := make(map[string]bool)
for _, sample := range prof.Sample {
    tb := locationHash(sample)
    samples[tb] = true
    total += sample.Value[0]
}

if total != totalSamples {
    return fmt.Errorf("sample total %d, want %d", total, totalSamples)
}

// Check the number of unique sample locations
if a.rows != len(samples) {
    return fmt.Errorf("number of samples %d, want %d", len(samples), a.rows)
}

// Check that all mappings have the right detail flags.
for _, m := range prof.Mapping {
    if m.HasFunctions != a.function {
        return fmt.Errorf("unexpected mapping.HasFunctions %v, want %v", m.HasFunctions, a.function)
    }
    if m.HasFilenames != a.fileline {
        return fmt.Errorf("unexpected mapping.HasFilenames %v, want %v", m.HasFilenames, a.fileline)
    }
    if m.HasLineNumbers != a.fileline {
        return fmt.Errorf("unexpected mapping.HasLineNumbers %v, want %v", m.HasLineNumbers, a.fileline)
    }
    if m.HasInlineFrames != a.inlineFrame {
        return fmt.Errorf("unexpected mapping.HasInlineFrames %v, want %v", m.HasInlineFrames, a.inlineFrame)
    }
}

// Check that aggregation has removed finer resolution data.
for _, l := range prof.Location {
    if !a.inlineFrame && len(l.Line) > 1 {
        return fmt.Errorf("found %d lines on location %d, want 1", len(l.Line), l.ID)
    }

    for _, ln := range l.Line {
        if !a.fileline && (ln.Function.Filename != "" || ln.Line != 0) {
            return fmt.Errorf("found line %s:%d on location %d, want :0",
                ln.Function.Filename, ln.Line, l.ID)
        }
        if !a.function && (ln.Function.Name != "") {
            return fmt.Errorf(`found file %s location %d, want ""`,
                ln.Function.Name, l.ID)
        }
    }
}

return nil

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

Может ли кто-нибудь продемонстрировать, как это улучшится с помощью try() ?

Я в основном за это предложение.

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

  • Документирование параметров результата
  • Манипулирование значением результата (обычно error ) внутри отсрочки

Чтобы рассмотреть эту проблему с нового направления, вот идея, которая, как мне кажется, не совсем согласуется с чем-либо, что обсуждалось в проектном документе или в этой ветке комментариев к проблеме. Назовем это «отсрочка ошибок»:

Разрешить использование defer для вызова функций с неявным параметром ошибки.

Итак, если у вас есть функция

func f(err error, t1 T1, t2 T2, ..., tn Tn) error

Затем в функции g , где последний результирующий параметр имеет тип error (т. е. любая функция, в которой может использоваться try ), вызов f может быть отложено следующим образом:

func g() (R0, R0, ..., error) {
    defer f(t0, t1, ..., tn) // err is implicit
}

Семантика error-defer:

  1. Отложенный вызов f вызывается с последним параметром результата g в качестве первого входного параметра f
  2. f вызывается только в том случае, если эта ошибка не равна нулю
  3. Результат f присваивается последнему параметру результата g

Таким образом, чтобы использовать пример из старого документа по разработке обработки ошибок, используя error-defer и try, мы могли бы сделать

func printSum(a, b string) error {
    defer func(err error) error {
        return fmt.Errorf("printSum(%q + %q): %v", a, b, err)
    }()
    x := try(strconv.Atoi(a))
    y := try(strconv.Atoi(b))
    fmt.Println("result:", x+y)
    return nil
}

Вот как будет работать HandleErrorf:

func printSum(a, b string) error {
    defer handleErrorf("printSum(%q + %q)", a, b)
    x := try(strconv.Atoi(a))
    y := try(strconv.Atoi(b))
    fmt.Println("result:", x+y)
    return nil
}

func handleErrorf(err error, format string, args ...interface{}) error {
    return fmt.Errorf(format+": %v", append(args, err)...)
}

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

func(error, ...error) error

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


Если подумать об этой идее последние пару дней, то это немного волшебно, но отказ от именованных параметров результата является большим преимуществом в ее пользу. Поскольку try поощряет более широкое использование defer для манипулирования ошибками, есть некоторый смысл в том, что defer можно расширить, чтобы он лучше подходил для этой цели. Кроме того, существует определенная симметрия между try и задержкой ошибок.

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

// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) (_ []*File, err error) {
    files := make([]*file, len(keys))

    defer func() {
        if err != nil {
            // Return any successfully retrieved files.
            for _, f := range files {
                if f != nil {
                    c.put(f)
                }
            }
        }
    }()

    // ...
}

С отсрочкой ошибки это становится:

// GetMulti retrieves multiple files through the cache at once and returns its
// results as a slice parallel to the input.
func (c *FileCache) GetMulti(keys []string) ([]*File, error) {
    files := make([]*file, len(keys))

    defer func(err error) error {
        // Return any successfully retrieved files.
        for _, f := range files {
            if f != nil {
                c.put(f)
            }
        }
        return err
    }()

    // ...
}

@beoran Что касается вашего комментария о том, что нам следует подождать дженериков. Здесь дженерики не помогут — читайте FAQ .

Что касается ваших предложений по поведению @velovix с двумя аргументами try по умолчанию: как я уже говорил , ваше представление о том, что является очевидно разумным выбором, является чьим-то кошмаром.

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

(Если на то пошло, мне нравится иметь обработчик. Это одно из наших предыдущих предложений. И если мы примем try как есть, мы все равно сможем перейти к try с обработчиком в прямом направлении. -совместимый способ - по крайней мере, если обработчик необязателен. Но давайте пошагово.)

@aarzilli Спасибо за ваше предложение .

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

Итак, я не думаю, что предложенный try _отпугивает_ людей от ошибок декорирования (они уже отговариваются даже с if по вышеуказанной причине); дело в том, что try не _поощряет_ это.

(Один из способов поощрения этого состоит в том, чтобы связать его с try : try можно использовать только в том случае, если вы также украшаете ошибку или явно отказываетесь от нее.)

Но вернемся к вашим предложениям: я думаю, вы вводите здесь гораздо больше механизмов. Изменение семантики defer просто для того, чтобы она лучше работала для try , не является чем-то, что мы хотели бы рассматривать, если только эти изменения defer не будут полезны в более общем плане. Кроме того, ваше предложение связывает defer вместе с try и, таким образом, делает оба этих механизма менее ортогональными; то, чего мы хотели бы избежать.

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

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

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

@alanfo Спасибо за положительный отзыв .

По пунктам, которые вы подняли:

1) Если единственная проблема с try заключается в том, что нужно указать имя возврата ошибки, чтобы мы могли декорировать ошибку через defer , я думаю, что у нас все хорошо. Если именование результата окажется реальной проблемой, мы могли бы решить эту проблему. Простым механизмом, который я могу придумать, будет предварительно объявленная переменная, которая является псевдонимом для результата ошибки (представьте, что это содержит ошибку, которая вызвала самый последний try ). Могут быть лучшие идеи. Мы не предлагали этого, потому что в языке уже есть механизм, который должен назвать результат.
2) try и тестирование: это можно решить и заставить работать. См. подробный документ.
3) Это явно рассматривается в подробном документе.
4) Подтверждено.

@benhoyt Спасибо за положительный отзыв .

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

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

(Чтобы быть полным, есть также вариант оператора, такого как ? , который был бы обратно совместим. Однако это не кажется мне лучшим выбором для такого языка, как Go. Но опять же, если это все, что нужно, чтобы сделать try вкусными, возможно, нам следует это рассмотреть.)

@ugorji Спасибо за положительный отзыв .

try можно расширить, чтобы получить дополнительный аргумент. Мы бы предпочли взять только функцию с сигнатурой func (error) error . Если вы хотите паниковать, легко предоставить однострочную вспомогательную функцию:

func doPanic(err error) error { panic(err) }

Лучше сделать дизайн try простым.

@ patrick-nyt Что вы предлагаете :

file, err := os.Open("file.go")
try(err)

будет возможно с текущим предложением.

@dpinela , @ugorji Пожалуйста, прочтите также дизайн-документ на тему must против try . Лучше сделать try максимально простым. must является распространенным «шаблоном» в выражениях инициализации, но нет необходимости срочно «исправлять» это.

@jargv Спасибо за ваше предложение . Это интересная идея (см. также мой комментарий здесь на эту тему). Обобщить:

  • try(x) работает как предложено
  • try() возвращает *error , указывающий на результат ошибки

Это был бы действительно еще один способ получить результат без необходимости называть его.

@cespare Предложение @jargv кажется мне намного проще, чем то, что предлагаете вы. Это решает ту же проблему доступа к ошибке результата. Как вы думаете?

Согласно https://github.com/golang/go/issues/32437#issuecomment -499320588:

func doPanic(err error) error { паника(err) }

Я предполагаю, что эта функция будет довольно распространена. Может ли это быть предопределено во «встроенном» (или где-то еще в стандартном пакете, например, errors )?

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

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

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

Оп вр 7 июн. 2019 01:04 schreef pj уведомления@github.com :

Аспер #32437 (комментарий)
https://github.com/golang/go/issues/32437#issuecomment-499320588 :

func doPanic(err error) error { паника(err) }

Я предполагаю, что эта функция будет довольно распространена. Может ли это быть предопределено
во "встроенном"?


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAARM6OOOLLYO5ZCE6VVL2TPZGJWRA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXEMYZY#issuecomment8 , 7919
или заглушить тему
https://github.com/notifications/unsubscribe-auth/AAARM6K5AOR2DES4QDTNLSTPZGJWRANCNFSM4HTGCZ7Q
.

@pjebs , я десятки раз писал эквивалентную функцию. Обычно я называю это «orDie» или «check». Это настолько просто, что нет необходимости делать его частью стандартной библиотеки. Кроме того, разные люди могут захотеть вести журнал или что-то еще до прекращения.

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

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

Моя первоначальная реакция на это была 👎, так как я предполагал, что обработка нескольких вызовов, вызывающих ошибки, внутри функции сделает обработку ошибки defer запутанной. Прочитав все предложение, я изменил свою реакцию на ❤️ и 👍, поскольку узнал, что этого все еще можно достичь с относительно низкой сложностью.

@carlmjohnson Да, это просто, но...

Я писал эквивалентную функцию десятки раз.

Преимущества предварительно объявленной функции:

  1. Мы можем сделать это одной строкой
  2. Нам не нужно повторно объявлять функцию err => panic в каждом используемом нами пакете или сохранять для нее общее местоположение. Так как он, вероятно, является общим для всех в сообществе Go, «стандартный пакет» — это _ обычное место для него.

@griesemer Благодаря варианту исходного предложения try с обработчиком ошибок требование всеобъемлющей функции для возврата ошибки больше не требуется.

Когда я впервые спросил об этом, ошибка => паника, мне указали, что предложение рассматривало это, но считало это слишком опасным (по уважительной причине). Но если мы используем try() без обработчика ошибок в сценарии, где всеобъемлющая функция не возвращает ошибку, преобразование ее в ошибку времени компиляции снимает проблему, обсуждаемую в предложении.

@pjebs Требование всеобъемлющей функции для возврата ошибки не требовалось в исходном дизайне, _если_ был предоставлен обработчик ошибок. Но это просто еще одно усложнение try . _намного_ лучше, чтобы все было просто. Вместо этого было бы понятнее иметь отдельную функцию must , которая всегда паникует при ошибке (но в остальном похожа try ). Тогда понятно, что происходит в коде, и не нужно смотреть на контекст.

Главная привлекательность такого must заключается в том, что его можно использовать с модульными тестами; особенно если пакет testing был соответствующим образом скорректирован, чтобы восстанавливаться после паники, вызванной must , и сообщать о них как об ошибках теста в приятной форме. Но зачем добавлять еще один новый языковой механизм, если мы можем просто настроить тестовый пакет так, чтобы он также принимал тестовую функцию вида TestXxx(t *testing.T) error ? Если они вернут ошибку, что в конце концов кажется вполне естественным (возможно, мы должны были сделать это с самого начала), то try будет работать нормально. Локальные тесты потребуют немного больше работы, но это, вероятно, выполнимо.

Другое относительно распространенное использование must — это глобальные выражения инициализации ( must(regexp.Compile... и т. д.). Если бы было «приятно иметь», но это не обязательно поднимает его до уровня, необходимого для новой языковой функции.

@griesemer Учитывая, что must имеет неопределенное отношение к try , и учитывая, что импульс направлен на реализацию try , не думаете ли вы, что неплохо рассмотреть must в то же время - даже если это просто "приятно иметь".

Скорее всего, если это не будет обсуждаться в этом раунде, это просто не будет реализовано/серьезно рассмотрено, по крайней мере, в течение 3 с лишним лет (или, возможно, никогда). Перекрытие в обсуждениях также было бы хорошо, а не начинать с нуля и повторять обсуждения.

Многие люди утверждают, что must try .

@pjebs Определенно не похоже, что прямо сейчас есть какой-либо «импульс к реализации try » ... - И мы также только что опубликовали это два дня назад. Тоже ничего не решено. Давайте дадим этому некоторое время.

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

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

Я сделаю все возможное, чтобы уважать просьбу @ianlancetaylor не просто повторять предыдущие пункты , а вместо этого добавлять новые комментарии к обсуждению. Однако я не думаю, что смогу сделать новые комментарии, не ссылаясь на предыдущие комментарии.

Обеспокоенность

Неудачная перегрузка отсрочки

Предпочтение перегружать очевидную и прямолинейную природу defer вызывает тревогу. Если я напишу defer closeFile(f) , то мне будет просто и очевидно, что происходит и почему; в конце функции, которая будет вызываться. И хотя использование defer вместо panic() и recover() менее очевидно, я редко использую его, если вообще когда-либо использую, и почти никогда не вижу его при чтении чужого кода.

Spoo для перегрузки defer для обработки ошибок не является очевидным и запутанным. Почему ключевое слово defer ? Разве defer не означает _"Сделать позже"_ вместо _"Может быть, позже?"_

Также есть опасения, упомянутые командой Go по поводу производительности defer . Учитывая это, кажется вдвойне прискорбным, что defer рассматривается для потока кода _"горячего пути"_.

Нет статистики, подтверждающей значительный вариант использования

Как упоминал @prologic , это предложение try() основано на большом проценте кода, который будет использовать этот вариант использования, или вместо этого оно основано на попытке успокоить тех, кто жаловался на обработку ошибок Go?

Хотел бы я знать, как предоставить вам статистику из моей кодовой базы, не просматривая каждый файл и не делая заметок; Я не знаю, как @prologic смог, хотя рад, что он это сделал.

Но, как ни странно, я был бы удивлен, если бы try() адресовал 5% моих вариантов использования и подозревал, что он решит менее 1%. Знаете ли вы наверняка, что у других результаты сильно отличаются? Вы взяли подмножество стандартной библиотеки и попытались посмотреть, как оно будет применяться?

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

Облегчает разработчикам игнорирование ошибок

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

f, _ := os.Open(filename)

Я знаю, что могу быть лучше в своем собственном коде, но я также знаю, что многие из нас зависят от щедрости других разработчиков Go, которые публикуют чрезвычайно полезные пакеты, но из того, что я видел в _"Other People's Code(tm)"_ лучшие практики обработки ошибок часто игнорируются.

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

Может (в основном) уже реализовать try() в пространстве пользователя

Если я неправильно понимаю предложение — что я, вероятно, понимаю — вот try() в Go Playground, реализованном в userland , хотя и с одним (1) возвращаемым значением и возвращающим интерфейс вместо ожидаемого типа:

package main

import (
    "errors"
    "fmt"
    "strings"
)
func main() {
    defer func() {
        r := recover()
        if r != nil && strings.HasPrefix(r.(string),"TRY:") {
            fmt.Printf("Ouch! %s",strings.TrimPrefix(r.(string),"TRY: "))
        }
    }()
    n := try(badjuju()).(int)
    fmt.Printf("Just chillin %dx!",n)   
}
func badjuju() (int,error) {
    return 10, errors.New("this is a really bad error")
}
func try(args ...interface{}) interface{} {
    err,ok := args[1].(error)
    if ok && err != nil {
        panic(fmt.Sprintf("TRY: %s",err.Error()))
    }
    return args[0]
}

Таким образом, пользователь может добавить try2() , try3() и так далее в зависимости от того, сколько возвращаемых значений ему нужно вернуть.

Но Go потребуется только одна (1) простая _ , но универсальная _ языковая функция, чтобы позволить пользователям, которые хотят try() , реализовать собственную поддержку, хотя и требующую явного утверждения типа. Добавьте возможность _(полностью обратной совместимости)_ для Go func , чтобы возвращать переменное количество возвращаемых значений, например:

func try(args ...interface{}) ...interface{} {
    err,ok := args[1].(error)
    if ok && err != nil {
        panic(fmt.Sprintf("TRY: %s",err.Error()))
    }
    return args[0:len(args)-2]
}

И если вы сначала обратитесь к дженерикам, то утверждения типа даже не понадобятся _ (хотя я думаю, что варианты использования для дженериков должны быть сокращены путем добавления встроенных функций для адресации вариантов использования дженериков, а не добавлять сбивающую с толку семантику и синтаксический салат дженериков. из Явы и др.)_

Отсутствие очевидности

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

Когда я вижу, что try() оборачивает выражение, что произойдет, если будет возвращена ошибка?

Будет ли ошибка просто игнорироваться? Или он перейдет к первому или самому последнему defer , и если это так, он автоматически установит переменную с именем err внутри замыкания или передаст ее как параметр _(I не видите параметр?)_. И если не имя автоматической ошибки, то как мне его назвать? Означает ли это, что я не могу объявить свою собственную переменную err в своей функции, чтобы избежать конфликтов?

И будет ли он вызывать все defer s? В обратном порядке или в обычном порядке?

Или он вернется как из закрытия, так и из func , где была возвращена ошибка? _(Что-то я бы никогда не подумал, если бы не прочитал здесь слова, которые подразумевают это.)_

Прочитав предложение и все комментарии, я до сих пор, честно говоря, не знаю ответов на поставленные выше вопросы. Мы хотим добавить такую ​​функцию в язык, сторонники которого позиционируют себя как «Капитан Очевидность»?

Недостаток контроля

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

Часто я находил полезным делиться аспектами обработки ошибок в пакете func — или даже в пакете package — но при этом иметь более конкретную обработку, совместно используемую в одном или нескольких других пакетах.

Например, я могу вызвать пять (5) вызовов func , которые возвращают error() из другого func ; обозначим их как A() , B() , C() , D() и E() . Мне может понадобиться C() для собственной обработки ошибок, A() , B() , D() и E() для совместной обработки ошибок, и B() и E() для специальной обработки.

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

Однако по иронии судьбы в Go уже есть языковые функции, обеспечивающие высокий уровень гибкости, который не нужно ограничивать небольшим набором вариантов использования; func s и замыкания. Итак, мой риторический вопрос:

_ "Почему мы не можем просто добавить небольшие усовершенствования в существующий язык для решения этих задач и не добавлять новые встроенные функции или принимать запутанную семантику?" _

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

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

Отсутствие заявленной поддержки break

Может показаться, что это выходит из поля зрения, поскольку большинство людей используют ранние возвраты для обработки ошибок, но я обнаружил, что предпочтительнее использовать break для обработки ошибок, обертывая большую часть или всю функцию до return .

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

Чтобы использовать break вместо раннего возврата, используйте цикл for range "1" {...} , чтобы создать блок для выхода из break из _ (на самом деле я создаю пакет с именем only , который содержит только константу называется Once со значением "1" ):_

func (me *Config) WriteFile() (err error) {
    for range only.Once {
        var j []byte
        j, err = json.MarshalIndent(me, "", "    ")
        if err != nil {
            err = fmt.Errorf("unable to marshal config; %s", 
                err.Error(),
            )
            break
        }
        err = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
        if err != nil {
            err = fmt.Errorf("unable to make directory'%s'; %s", 
                me.GetDir(), 
                err.Error(),
            )
            break
        }
        err = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
        if err != nil {
            err = fmt.Errorf("unable to write to config file '%s'; %s", 
                me.GetFilepath(), 
                err.Error(),
            )
            break
        }
    }
    return err
}

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

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

Мое мнение err == nil проблематично

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

Поэтому для Go 2 мне бы очень хотелось, чтобы Go рассмотрел возможность добавления нового встроенного типа status и трех встроенных функций iserror() , iswarning() , issuccess() . status может реализовать error , что обеспечивает большую обратную совместимость, а значение nil , переданное в issuccess() , вернет true , но status будет иметь дополнительное внутреннее состояние для уровня ошибки, чтобы проверка уровня ошибки всегда выполнялась с помощью одной из встроенных функций и, в идеале, никогда не с проверкой nil . Это позволило бы использовать что-то вроде следующего подхода:

func (me *Config) WriteFile() (sts status) {
    for range only.Once {
        var j []byte
        j, sts = json.MarshalIndent(me, "", "    ")
        if iserror(sts) {
            sts.AddMessage("unable to marshal config")
            break
        }
        sts = me.MaybeMakeDir(me.GetDir(), os.ModePerm)
        if iserror(sts) {
            sts.AddMessage("unable to make directory'%s'", me.GetDir())
            break
        }
        sts = ioutil.WriteFile(string(me.GetFilepath()), j, os.ModePerm)
        if iserror(sts) {
            sts.AddMessage("unable to write to config file '%s'", 
                me.GetFilepath(), 
            )
            break
        }
        sts = fmt.Status("config file written")
    }
    return sts
}

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

Если вы считаете, что при таком подходе есть вероятность развития идиоматического кода Go, примите это во внимание при реализации обработки ошибок, в том числе при рассмотрении этого предложения try() .

_"Не для всех"_ обоснование

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

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

И разве это не то же самое оправдание, по которому члены основной команды отклоняли многочисленные просьбы сообщества? Ниже приводится прямая цитата из комментария, сделанного членом команды Go в типичном ответе на запрос функции, отправленный около 2 лет назад _ (я не называю имя человека или конкретный запрос функции, потому что это обсуждение не должно быть в состоянии народ, а вместо этого про язык):_

_ «Новая языковая функция требует убедительных примеров использования. Все языковые функции полезны, иначе никто не предложил бы их; вопрос в том, достаточно ли они полезны, чтобы оправдать усложнение языка и необходимость изучения новых концепций всеми? случаи здесь? Как люди будут их использовать? Например, рассчитывают ли люди, что смогут... и если да, то как они это сделают? Делает ли это предложение больше, чем позволяет вам...?"_
— Член основной команды Go

Честно говоря, когда я увидел эти ответы, я испытал одно из двух чувств:

  1. Негодование, если это особенность, с которой я согласен, или
  2. Восторг, если это функция, с которой я не согласен.

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

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

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

Если требование убедительного набора реальных вариантов использования является планкой для всех предложений функций, созданных сообществом, не должно ли это также быть той же планкой для _ всех _ предложений функций?

Вложенность try()

Это тоже обсуждалось некоторыми, но я хочу провести сравнение между try() и продолжающимся запросом тернарных операторов. Цитата из комментариев другого члена команды Go около 18 месяцев назад:

_"при "программировании в больших масштабах" (большие базы кода с большими командами в течение длительных периодов времени) код читается ГОРАЗДО чаще, чем пишется, поэтому мы оптимизируем для удобства чтения, а не для записи."_

Одной из _основных_ заявленных причин отказа от добавления тернарных операторов является то, что их трудно читать и/или легко неправильно читать, когда они вложены. Однако то же самое можно сказать и о вложенных операторах try() , таких как try(try(try(to()).parse().this)).easily()) .

Дополнительные причины возражений против тернарных операторов заключались в том, что они являются _"выражениями"_ с тем аргументом, что вложенные выражения могут добавить сложности. Но разве try() тоже не создает вложенное выражение?

Теперь кто-то здесь сказал: «Я думаю, что примеры вроде [вложенных try() s] нереалистичны»_, и это утверждение не оспаривалось.

Но если люди принимают как постулат, что разработчики не будут вкладывать try() , то почему такое же почтение не уделяется тернарным операторам, когда люди говорят: «Я думаю, что глубоко вложенные тернарные операторы нереалистичны?»_

В итоге я думаю, что если аргументы против тернарных операторов действительно действительны, то их также следует считать действительными аргументами против этого предложения try() .

В итоге

На момент написания этой статьи 58% проголосовало против, а 42% проголосовало за. Я думаю, что одного этого должно быть достаточно, чтобы указать, что это предложение вызывает достаточно разногласий и пора вернуться к чертежной доске по этому вопросу.

между прочим

PS Если говорить более иронично, я думаю, что мы должны следовать перефразированной мудрости Йоды:

_"Нет try() . Только do() ."_

@ianlancetaylor

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

Не говоря о @beoran, но в моем комментарии несколько минут назад вы увидите, что если бы у нас были дженерики _(плюс вариативные возвращаемые параметры)_, то мы могли бы создать свои собственные try() .

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

_ "Я думаю, что варианты использования дженериков должны быть сокращены путем добавления встроенных функций для решения вариантов использования дженериков, а не добавления запутанной семантики и синтаксиса дженериков из Java и др.)"_

@ianlancetaylor

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

func try(v interface{}, err error) interface{} {
   if err != nil { 
     panic(err)
   }
   return v
}

Посмотрите здесь, как его можно использовать: https://play.golang.org/p/Kq9Q0hZHlXL

Недостатками такого подхода являются:

  1. Требуется отложенное спасение, но с try , как в этом предложении, также необходим отложенный обработчик, если мы хотим правильно обрабатывать ошибки. Так что я считаю, что это не серьезный недостаток. Было бы даже лучше, если бы в Go была какая-то встроенная функция super(arg1, ..., argn) , заставляющая вызывающую сторону вызывающей стороны, на один уровень вверх по стеку вызовов, возвращаться с заданными аргументами arg1,...argn, что-то вроде супервозврата. если вы будете.
  2. Этот try , который я реализовал, может работать только с функцией, которая возвращает один результат и ошибку.
  3. Вы должны ввести assert для возвращаемых результатов интерфейса emtpy.

Достаточно мощные дженерики могут решить проблему 2 и 3, оставив только 1, которую можно решить, добавив super() . С этими двумя функциями мы могли бы получить что-то вроде:

func (T ... interface{})try(T, err error) super {
   if err != nil { 
      super(err)
   }
  super(T...)
}

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

На самом деле идея встроенной функции super() настолько мощная и интересная, что я мог бы опубликовать предложение по ней отдельно.

@beoran Приятно видеть, что мы независимо друг от друга пришли к одним и тем же ограничениям в отношении реализации try() в пространстве пользователя, за исключением суперчасти, которую я не включил, потому что хотел поговорить о чем-то подобном в альтернативном предложении. :-)

Мне нравится это предложение, но тот факт, что вам пришлось явно указать, что defer try(...) и go try(...) запрещены, заставил меня подумать, что что-то не так... Ортогональность - хорошее руководство по проектированию. При дальнейшем чтении и просмотре таких вещей, как
x = try(foo(...)) y = try(bar(...))
Интересно, может быть, try должен быть контекстом ! Рассмотреть возможность:
try ( x = foo(...) y = bar(...) )
Здесь foo() и bar() возвращают два значения, второе из которых равно error . Попробуйте использовать семантику только для вызовов в блоке try , где возвращаемое значение ошибки опускается (нет получателя), а не игнорируется (получатель _ ). Вы даже можете обрабатывать некоторые ошибки между вызовами foo и bar .

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

Если try является контекстом, то мы только что создали блоки try/catch, которых мы специально пытаемся избежать (и не зря).

Нет никакого улова. Будет сгенерирован точно такой же код, как если бы текущее предложение
x = try(foo(...)) y = try(bar(...))
Это просто другой синтаксис, а не семантика.
````

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

Что, если foo или bar не возвращают ошибку, можно ли их также поместить в контекст try? Если нет, то кажется, что было бы некрасиво переключаться между функциями ошибок и функциями без ошибок, и если они могут, то мы возвращаемся к проблемам с блоками try в старых языках.

Во-вторых, обычно синтаксис keyword ( ... ) означает, что вы ставите префикс ключевого слова в каждой строке. Итак, для импорта, var, const и т. д.: каждая строка начинается с ключевого слова. Делать исключение из этого правила не кажется хорошим решением

Вместо использования функции было бы более идиоматично использовать специальный идентификатор?

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

func foo() (error) {
    f, # := os.Open()
    defer f.Close()
    _, # = f.WriteString("foo")
    return nil
}

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

  • если они не названы нулевым значением
  • значение, присвоенное именованным переменным, в противном случае

@deanveloper семантика блока try имеет значение только для функций, которые возвращают значение ошибки и которым значение ошибки не присваивается. Таким образом, последний пример настоящего предложения также может быть записан как
try(x = foo(...)) try(y = bar(...))
размещение обоих операторов в одном блоке аналогично тому, что мы делаем для повторяющихся операторов import , const и var .

Теперь, если у вас есть, например
try( x = foo(...)) go zee(...) defer fum() y = bar(...) )
Это эквивалентно написанию
try(x = foo(...)) go zee(...) defer fum() try(y = bar(...))
Факторизация всего этого в одном блоке try делает его менее загруженным.

Рассмотреть возможность
try(x = foo())
Если foo() не возвращает значение ошибки, это эквивалентно
x = foo()

Рассмотреть возможность
try(f, _ := os.open(filename))
Поскольку возвращаемое значение ошибки игнорируется, это эквивалентно просто
f, _ := os.open(filename)

Рассмотреть возможность
try(f, err := os.open(filename))
Поскольку возвращаемое значение ошибки не игнорируется, это эквивалентно
f, err := os.open(filename) if err != nil { return ..., err }
Как в настоящее время указано в предложении.

И это также красиво убирает вложенные попытки!

Вот ссылка на альтернативное предложение, о котором я упоминал выше:

Он требует добавления двух (2) небольших, но общих языковых функций для решения тех же вариантов использования, что и try()

  1. Возможность вызова func /замыкания в операторе присваивания.
  2. Возможность break , continue или return более чем на один уровень.

С этими двумя функциями они не будут _"волшебными"_, и я полагаю, что их использование приведет к созданию кода Go, который будет легче понять и будет больше соответствовать идиоматичному коду Go, с которым мы все знакомы.

Я прочитал предложение, и мне очень нравится, куда идет попытка.

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

Рассмотрим карты. Это действительно:

v := m[key]

как это:

v, ok := m[key]

Что, если мы обработаем ошибки именно так, как предлагает try, но удалим встроенный. Итак, если мы начали с:

v, err := fn()

Вместо того, чтобы писать:

v := try(fn())

Вместо этого мы могли бы написать:

v := fn()

Когда значение ошибки не фиксируется, оно обрабатывается точно так же, как и попытка. Потребуется некоторое время, чтобы привыкнуть, но это очень похоже на v, ok := m[key] и v, ok := x.(string) . По сути, любая необработанная ошибка приводит к возврату функции и установке значения ошибки.

Вернемся к выводам проектной документации и требованиям к реализации:

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

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

Таким образом, используя пример CopyFile из предложения вместе с defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) , мы получаем:

func CopyFile(src, dst string) (err error) {
        defer fmt.HandleErrorf(&err, "copy %s %s", src, dst)

        r := os.Open(src)
        defer r.Close()

        w := os.Create(dst)
        defer func() {
                err := w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        io.Copy(w, r)
        w.Close()
        return nil
}

@savaki Мне это нравится, и я думал о том, что нужно сделать, чтобы Go перевернул обработку ошибок, всегда обрабатывая ошибки по умолчанию и позволяя программисту указывать, когда этого не делать (путем записи ошибки в переменную), но полное отсутствие каких-либо идентификатор сделал бы код трудным для понимания, поскольку невозможно было бы увидеть все точки возврата. Может быть соглашение об именах функций, которые могут возвращать ошибку по-разному, могло бы работать (например, использование общедоступных идентификаторов с заглавной буквы). Может быть, если функция вернула ошибку, она всегда должна заканчиваться, скажем, на ? . Тогда Go всегда сможет неявно обработать ошибку и автоматически вернуть ее вызывающей функции, как и try. Это делает его очень похожим на некоторые предложения, предлагающие использовать идентификатор ? вместо try, но важным отличием является то, что здесь ? будет частью имени функции, а не дополнительным идентификатором. На самом деле функция, возвращающая error в качестве последнего возвращаемого значения, даже не скомпилируется, если не будет иметь суффикс ? . Конечно, ? является произвольным и может быть заменено чем-либо еще, что делает намерение более явным. operation?() было бы эквивалентно обертыванию try(someFunc()) , но ? было бы частью имени функции, и его единственная цель состояла бы в том, чтобы указать, что функция может возвращать ошибку так же, как капитализация первая буква переменной.

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

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

Можем ли мы сделать что-то вроде исключений C++ с декораторами для старых функций?

func some_old_test() (int, error){
    return 0, errors.New("err1")
}
func some_new_test() (int){
        if true {
             return 1
        }
    throw errors.New("err2")
}
func throw_res(int, e error) int {
    if e != nil {
        throw e
    }
    return int
}
func main() {
    fmt.Println("Hello, playground")
    try{
        i := throw_res(some_old_test())
        fmt.Println("i=", i + some_new_test())
    } catch(err io.Error) {
        return err
    } catch(err error) {
        fmt.Println("unknown err", err)
    }
}

@owais Я думал, что семантика будет точно такой же, как и попытка, поэтому, по крайней мере, вам нужно будет объявить тип ошибки. Итак, если мы начали с:

func foo() error {
  _, err := fn() 
  if err != nil {
    return err
  }

  return nil
} 

Если я понимаю предложение try, просто сделайте следующее:

func foo() error {
  _  := fn() 
  return nil
} 

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

Тогда это будет работать:

func foo() (err error) {
  _  := fn() 
  return nil
} 

почему бы просто не обработать случай ошибки, которая не назначена переменной.

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

неявный возврат для случая if err != nil, компилятор может сгенерировать имя локальной переменной для возврата, если необходимо, чтобы программист не мог получить к нему доступ.
лично мне не нравится этот конкретный случай с точки зрения читабельности кода

f := os.Open("foo.txt")

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

f := os.Open("foo.txt") else return

интересно, что мы могли бы принимать обе формы, а gofmt автоматически добавлял бы возврат else.

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

f := os.Open("foo.txt") else err {
  return errors.Wrap(err, "some context")
}

добавление контекста с несколькими возвращаемыми значениями

f := os.Open("foo.txt") else err {
  return i, j, errors.Wrap(err, "some context")
}

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

bits := ioutil.ReadAll(os.Open("foo")) else err {
  // either error ends up here.
  return i, j, errors.Wrap(err, "some context")
}

компилятор отказывается от компиляции из-за отсутствия возвращаемого значения ошибки в функции

func foo(s string) int {
   i := strconv.Atoi(s) // cannot implicitly return error due to missing error return value for foo.
   return i * 2
}

успешно компилируется, потому что ошибка явно игнорируется.

func foo(s string) int {
   i, _ := strconv.Atoi(s)
   return i * 2
} 

компилятор доволен. он игнорирует ошибку, как и в настоящее время, потому что не происходит присваивания или суффикса.

func foo() error {
  return errors.New("whoops")
}

func bar() {
  foo()
}

внутри цикла вы можете использовать continue.

for _, s := range []string{"1","2","3","4","5","6"} {
  i := strconv.Atoi(s) else continue
}

изменить: заменить ; на else

@savaki Я думаю, что понял ваш первоначальный комментарий, и мне нравится идея обработки ошибок Go по умолчанию, но я не думаю, что это жизнеспособно без добавления некоторых дополнительных изменений синтаксиса, и как только мы это сделаем, оно станет поразительно похоже на текущее предложение.

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

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

Основным недостатком этого подхода является то, что параметр результата ошибки должен быть назван, что может привести к менее красивым API (но см. FAQ по этому вопросу). Мы верим, что привыкнем к этому, как только этот стиль утвердится.

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

func example() error {
        var err *error = funcerror() // always return a non-nil pointer
        fmt.Print(*err) // always nil if the return parameters are not named and not in a defer

        defer func() {
                err := funcerror()
                fmt.Print(*err) // "x"
        }

        return errors.New("x")
}
func exampleNamed() (err error) {
        funcErr := funcerror()
        fmt.Print(*funcErr) // "nil"

        err = errors.New("x")
        fmt.Print(*funcErr) // "x", named return parameter is reflected even before return is called

        *funcErr = errors.New("y")
        fmt.Print(err) // "y", unfortunate side effect?

        defer func() {
                funcErr := funcerror()
                fmt.Print(*funcErr) // "z"
                fmt.Print(err) // "z"
        }

        return errors.New("z")
}

использование с попыткой:

func CopyFile(src, dst string) (error) {
        defer func() {
                err := funcerror()
                if *err != nil {
                        *err = fmt.Errorf("copy %s %s: %v", src, dst, err)
                }
        }()
        // one liner alternative
        // defer fmt.HandleErrorf(funcerror(), "copy %s %s", src, dst)

        r := try(os.Open(src))
        defer r.Close()

        w := try(os.Create(dst))
        defer func() {
                w.Close()
                err := funcerror()
                if *err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

        try(io.Copy(w, r))
        try(w.Close())
        return nil
}

В качестве альтернативы funcerror (название находится в стадии разработки :D) может возвращать nil, если не вызывается внутри defer.

Другой альтернативой является то, что funcerror возвращает интерфейс «Errorer», чтобы сделать его доступным только для чтения:

type interface Errorer() {
        Error() error
}

@savaki Мне действительно нравится ваше предложение опустить try() и позволить ему больше походить на тестирование карты или утверждения типа. Это больше похоже на _"Go"._

Тем не менее, я все еще вижу одну вопиющую проблему, и ваше предложение предполагает, что все ошибки, использующие этот подход, вызовут return и покинут функцию. Чего он не рассматривает, так это выпуска break из текущих for или continue за текущие for .

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

Поэтому я утверждаю, что break и continue должны быть допустимыми стратегиями обработки ошибок, и в настоящее время ваше предложение предполагает только return , тогда как try() предполагает, что это или вызов ошибки обработчик, который сам может только return , но не break или continue .

Похоже, у нас с Саваки были похожие идеи, я просто добавил семантику блока для обработки ошибки, если это необходимо. Например, добавление comtext для петель, где вы хотите закоротить и т. д.

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

@Джеймс-Лоуренс

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

Взяв ваш пример:

f := os.Open("foo.txt"); err {
  return errors.Wrap(err, "some context")
}

Что можно сравнить с тем, что мы делаем сегодня:

f,err := os.Open("foo.txt"); 
if err != nil {
  return errors.Wrap(err, "some context")
}

Однозначно предпочтительнее для меня. За исключением нескольких проблем:

  1. err кажется _"магическим"_ объявленным. Магию нужно свести к минимуму, не так ли? Итак, объявим это:
f, err := os.Open("foo.txt"); err {
  return errors.Wrap(err, "some context")
}
  1. Но это все еще не работает, потому что Go не интерпретирует значения nil как false и значения указателя как true , поэтому это должно быть:
f, err := os.Open("foo.txt"); err != nil {
  return errors.Wrap(err, "some context")
}

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

Но что, если бы Go добавил две (2) встроенные функции; iserror() и error() ? Тогда мы могли бы сделать это, что мне кажется не таким уж плохим:

f := os.Open("foo.txt"); iserror() {
  return errors.Wrap(error(), "some context")
}

Или лучше _(что-то вроде):_

f := os.Open("foo.txt"); iserror() {
  return error().Extend("some context")
}

Что думаете вы и другие?

Кстати, проверьте правильность написания моего имени пользователя. Меня бы не уведомили о вашем упоминании, если бы я все равно не обращал внимания...

@mikeschinkel извините за имя, которое было на моем телефоне, а github не предлагал автоматически.

err кажется «волшебным образом» объявленным. Магию нужно свести к минимуму, не так ли? Итак, объявим это:

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

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

_"сама идея автоматической вставки возврата волшебна."_

Никаких аргументов от меня вы не получите.

_"это вряд ли самое волшебное, что происходит во всем этом предложении."_

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

_ "Кроме того, я бы сказал, что ошибка была объявлена; только в конце внутри контекста блока с областью действия..."_

Так что, если бы я хотел назвать это err2 , это тоже сработало бы?

f := os.Open("foo.txt"); err2 {
  return errors.Wrap(err, "some context")
}

Поэтому я предполагаю, что вы также предлагаете обработку специального случая err / err2 после точки с запятой, то есть предполагается, что это либо nil , либо не nil вместо bool , как при проверке карты?

if _,ok := m[a]; !ok {
   print("there is no 'a' in 'm'")
}

В целом я очень доволен обработкой ошибок go с предстоящими дополнениями к пакету ошибок.

Я тоже доволен обработкой ошибок в сочетании с break и continue _(но не return .)_

Как бы то ни было, я считаю это предложение try() скорее вредным, чем полезным, и предпочитаю не видеть ничего, кроме предложенного варианта реализации. #jmtcw.

@beoran @mikeschinkel Ранее я предположил, что мы не можем реализовать эту версию try с помощью дженериков, потому что это изменяет поток управления. Если я правильно понимаю, вы оба предполагаете, что мы могли бы использовать дженерики для реализации try , вызывая panic . Но эта версия try явно не panic . Поэтому мы не можем использовать дженерики для реализации этой версии try .

Да, мы могли бы использовать дженерики (версия дженериков, значительно более мощная, чем версия в черновике дизайна на https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md) для написания функции. который паникует при ошибке. Но паника при ошибке — это не та обработка ошибок, которую программисты Go пишут сегодня, и мне это не кажется хорошей идеей.

Специальная обработка @mikeschinkel заключается в том, что блок выполняется только при возникновении ошибки.
```
f := os.Open('foo'); err { return err } // здесь err всегда будет ненулевым.

@ianlancetaylor

_"Да, мы могли бы использовать дженерики... Но паника при ошибке - это не тот способ обработки ошибок, который программисты Go пишут сегодня, и мне это не кажется хорошей идеей."_

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

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

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

Это проясняет?

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

@ianlancetaylor Подтверждено. Опять же, я искал причину, по которой try() не нужно было бы добавлять, а не находил способ добавить его. Как я уже сказал выше, я бы предпочел не иметь ничего нового для обработки ошибок, чем иметь try() , как предлагается здесь.

Лично мне более раннее предложение check понравилось больше, чем это, основанное исключительно на визуальных аспектах; check имеет ту же мощность, что и этот try() , но bar(check foo()) для меня более удобочитаем, чем bar(try(foo())) (мне просто понадобилась секунда, чтобы сосчитать скобки!).

Что еще более важно, моя главная претензия к handle / check заключалась в том, что это не позволяло оборачивать отдельные чеки по-разному — и теперь это предложение try() имеет тот же недостаток, при вызове хитрых редко используемых функций отложенных и именованных возвратов, сбивающих с толку новичков. И с handle , по крайней мере, у нас была возможность использовать области для определения блоков дескрипторов, с defer даже это невозможно.

Насколько я понимаю, это предложение проигрывает более раннему предложению handle / check во всех отношениях.

Вот еще одна проблема с использованием отсрочек для обработки ошибок.

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

func someHTTPHandlerGuts() (err error) {
  defer func() {
    recordMetric("db call failed")
    return fmt.HandleErrorf("db unavailable: %v", err)
  }()
  data := try(makeDBCall)
  // some code that panics due to a bug
  return nil
}

Вспомните, что net/http восстанавливается после паники, и представьте себе отладку производственной проблемы, связанной с паникой. Вы бы посмотрели на свои инструменты и увидели всплеск сбоев вызовов db из вызовов recordMetric . Это может скрыть истинную проблему, а именно панику в следующей строке.

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

Вот модификация, которая может помочь с некоторыми поднятыми проблемами: относитесь к try как к goto , а не как к return . Выслушай меня. :)

Вместо этого try будет синтаксическим сахаром для:

t1, … tn, te := f()  // t1, … tn, te are local (invisible) temporaries
if te != nil {
        err = te   // assign te to the error result parameter
        goto error // goto "error" label
}
x1, … xn = t1, … tn  // assignment only if there was no error

Преимущества:

  • defer не требуется для оформления ошибок. (Однако именованные возвраты по-прежнему необходимы.)
  • Наличие метки error: является визуальным признаком того, что где-то в функции есть try .

Это также предоставляет механизм для добавления обработчиков, который обходит проблемы обработчика как функции: используйте метки в качестве обработчиков. try(fn(), wrap) будет goto wrap вместо goto error . Компилятор может подтвердить, что wrap: присутствует в функции. Обратите внимание, что наличие обработчиков также помогает при отладке: вы можете добавить/изменить обработчик, чтобы указать путь отладки.

Образец кода:

func CopyFile(src, dst string) (err error) {
    r := try(os.Open(src))
    defer r.Close()

    w := try(os.Create(dst))
    defer func() {
        w.Close()
        if err != nil {
            os.Remove(dst) // only if a “try” fails
        }
    }()

    try(io.Copy(w, r), copyfail)
    try(w.Close())
    return nil

error:
    return fmt.Errorf("copy %s %s: %v", src, dst, err)

copyfail:
    recordMetric("copy failure") // count incidents of this failure
    return fmt.Errorf("copy %s %s: %v", src, dst, err)
}

Другие комментарии:

  • Мы могли бы потребовать, чтобы любой метке, используемой в качестве цели try , предшествовал завершающий оператор. На практике это заставит их завершить функцию и может помешать некоторому спагетти-коду. С другой стороны, это может помешать разумному и полезному использованию.
  • try можно использовать для создания цикла. Я думаю, что это подпадает под лозунг «если больно, не делай этого», но я не уверен.
  • Это потребует исправления https://github.com/golang/go/issues/26058.

Предоставлено: я считаю, что вариант этой идеи был впервые предложен @griesemer лично на GopherCon в прошлом году.

@josharian Здесь важно думать о взаимодействии с panic , и я рад, что вы упомянули об этом, но ваш пример кажется мне странным. В следующем коде мне непонятно, что отсрочка всегда записывает метрику "db call failed" . Это будет ложная метрика, если someHTTPHandlerGuts завершится успешно и вернет nil . defer выполняется во всех случаях выхода, а не только в случаях ошибки или паники, поэтому код кажется неправильным, даже если паники нет.

func someHTTPHandlerGuts() (err error) {
  defer func() {
    recordMetric("db call failed")
    return fmt.HandleErrorf("db unavailable: %v", err)
  }()
  data := try(makeDBCall)
  // some code that panics due to a bug
  return nil
}

@josharian Да, это более или менее точная версия, которую мы обсуждали в прошлом году (за исключением того, что мы использовали check вместо try ). Я думаю, что было бы крайне важно, чтобы нельзя было перейти «назад» в остальную часть тела функции, как только мы достигнем метки error . Это гарантировало бы, что goto несколько "структурирован" (код спагетти невозможен). Одна из проблем, которая была поднята, заключалась в том, что метка обработчика ошибок ( error: ) всегда оказывалась в конце функции (иначе ее пришлось бы как-то обходить). Лично мне нравится код обработки ошибок в конце (в конце), но другие считают, что он должен быть виден с самого начала.

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

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

В, ок:= м[ключ]

Форма от чтения с карты

Вы можете избежать меток goto, заставляющих обработчики дойти до конца функции, воскресив предложение handle / check в упрощенной форме. Что, если бы мы использовали синтаксис handle err { ... } , но просто не позволяли цепочке обработчиков, вместо этого использовался только последний. Это значительно упрощает это предложение и очень похоже на идею goto, за исключением того, что обработка приближается к точке использования.

func CopyFile(src, dst string) (err error) {
    handle err {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }
    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    defer func() {
        w.Close()
        if err != nil {
            os.Remove(dst) // only if a “check” fails
        }
    }()

    {
        // handlers are scoped, after this scope the original handle is used again.
        // as an alternative, we could have repeated the first handle after the io.Copy,
        // or come up with a syntax to name the handlers, though that's often not useful.
        handle err {
            recordMetric("copy failure") // count incidents of this failure
            return fmt.Errorf("copy %s %s: %v", src, dst, err)
        }
        check io.Copy(w, r)
    }
    check w.Close()
    return nil
}

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

@josharian @griesemer , если вы введете именованные обработчики (которые требуют много ответов для проверки/обработки, см. повторяющиеся темы ), существуют параметры синтаксиса, предпочтительные для try(f(), err) :

try.err f()
try?err f()
try#err f()

?err    f() // because 'try' is redundant
?return f() // no handler
?panic  f() // no handler

(?err f()).method()

f?err() // lead with function name, instead of error handling
f?err().method()

file, ?err := os.Open(...) // many check/handle responses also requested this style

Что мне больше всего нравится в Go, так это то, что его синтаксис относительно свободен от пунктуации и его можно читать вслух без особых проблем. Я бы очень не хотел, чтобы Go закончился как $#@!perl .

Для меня создание "попробовать" встроенную функцию и включение цепочек имеет 2 проблемы:

  • Это несовместимо с остальной частью потока управления в go (например, с ключевыми словами for/if/return/etc).
  • Это делает код менее читаемым.

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

Тернарный оператор — это еще одно место, где go не имеет чего-то и требует больше нажатий клавиш, но в то же время улучшает читаемость/обслуживаемость. Добавление «попробовать» в этой более ограниченной форме лучше сбалансирует выразительность и удобочитаемость, ИМО.

FWIW, panic влияет на поток управления и имеет скобки, но go и defer также влияют на поток и не влияют. Я склонен думать, что try больше похожа на defer тем, что это необычная потоковая операция, и усложнение ее выполнения try (try os.Open(file)).Read(buf) — это хорошо, потому что мы хотим препятствовать однострочникам. так или иначе, но что угодно. Любой в порядке.

Предложение, которое всем не понравится из-за неявного имени для переменной, возвращающей окончательную ошибку: $err . Это лучше, чем try() IMO. :-)

@griesemer

_"Лично мне нравится, когда код обработки ошибок убран (в конце)"_

+1 к этому!

Я считаю, что обработка ошибок, реализованная _до_ возникновения ошибки, гораздо сложнее обосновать, чем обработка ошибок, реализованная _после_ возникновения ошибки. Необходимость мысленно отскочить назад и заставить следовать логическому потоку кажется, будто я вернулся в 1980-е, пишу Basic с GOTO.

Позвольте мне предложить еще один потенциальный способ обработки ошибок, снова используя CopyFile() в качестве примера:

func CopyFile(src, dst string) (err error) {

    r := os.Open(src)
    defer r.Close()

    w := os.Create(dst)
    defer w.Close()

    io.Copy(w, r)
    w.Close()

    for err := error {
        switch err.Source() {
        case w.Close:
            os.Remove(dst) // only if a “try” fails
            fallthrough
        case os.Open, os.Create, io.Copy:
            err = fmt.Errorf("copy %s %s: %v", src, dst, err)
        default:
            err = fmt.Errorf("an unexpected error occurred")
        }
    }

    return err
}

Требуемые языковые изменения:

  1. Разрешить конструкцию for error{} , аналогичную конструкции for range{} , но вводимую только при ошибке и выполняемую только один раз.

  2. Разрешить пропуск захвата возвращаемых значений, которые реализуют <object>.Error() string , но только если конструкция for error{} существует в том же func .

  3. Вызывает переход потока управления программой к первой строке конструкции for error{} , когда func возвращает _"ошибку"_ в своем последнем возвращаемом значении.

  4. При возврате _"ошибки"_ Go добавляет ссылку на функцию, которая вернула ошибку, которую можно получить с помощью <error>.Source()

Что такое _"ошибка"_?

В настоящее время _"ошибка"_ определяется как любой объект, который реализует Error() string и, конечно же, не является nil .

Однако часто возникает необходимость расширить ошибку _даже при успехе_, чтобы позволить возвращать значения, необходимые для успешных результатов RESTful API. Поэтому я бы попросил команду Go не предполагать автоматически, что err!=nil означает _"ошибку"_, а вместо этого проверить, реализует ли объект ошибки IsError() и возвращает ли IsError() true , прежде чем предположить, что любое значение, отличное nil , является _"ошибкой"._

_(Я не обязательно говорю о коде в стандартной библиотеке, но в первую очередь, если вы выбираете свой поток управления для ветвления при _"ошибке"_. Если вы посмотрите только на err!=nil , мы будем очень ограничены в том, что мы можно сделать с точки зрения возвращаемых значений в наших функциях.)_

Кстати, позволить каждому тестировать _"ошибку"_ таким же образом, вероятно, проще всего было бы сделать, добавив новую встроенную функцию iserror() :

type ErrorIser interface {
    IsError() bool
}
func iserror(err error) bool {
    if err == nil { 
        return false
    }
    if _,ok := err.(ErrorIser); !ok {
        return true
    }
    return err.IsError()
}

Побочные преимущества разрешения отсутствия захвата _"ошибок"_

Обратите внимание, что разрешение не захватывать последнюю _"error"_ из вызовов func позволит последующему рефакторингу возвращать ошибки из func , которым изначально не нужно было возвращать ошибки. И это позволит провести этот рефакторинг без нарушения какого-либо существующего кода , который использует эту форму исправления ошибок и вызывает указанные func s.

Для меня это решение «Должен ли я возвращать ошибку или отказаться от обработки ошибок ради простоты?» — одно из самых больших затруднений при написании кода на Go. Разрешение не захвата _"ошибок"_ выше почти устранило бы это затруднение.

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

https://github.com/rhysd/trygo

Я назвал расширенный язык TryGo и реализовал транслятор TryGo to Go.

С переводчиком код

func CreateFileInSubdir(subdir, filename string, content []byte) error {
    cwd := try(os.Getwd())

    try(os.Mkdir(filepath.Join(cwd, subdir)))

    p := filepath.Join(cwd, subdir, filename)
    f := try(os.Create(p))
    defer f.Close()

    try(f.Write(content))

    fmt.Println("Created:", p)
    return nil
}

можно перевести на

func CreateFileInSubdir(subdir, filename string, content []byte) error {
    cwd, _err0 := os.Getwd()
    if _err0 != nil {
        return _err0
    }

    if _err1 := os.Mkdir(filepath.Join(cwd, subdir)); _err1 != nil {
        return _err1
    }

    p := filepath.Join(cwd, subdir, filename)
    f, _err2 := os.Create(p)
    if _err2 != nil {
        return _err2
    }
    defer f.Close()

    if _, _err3 := f.Write(content); _err3 != nil {
        return _err3
    }

    fmt.Println("Created:", p)
    return nil
}

Из-за ограничения языка я не мог реализовать общий вызов try() . Это ограничено

  • RHS определения
  • RHS оператора присвоения
  • Заявление о вызове

но я мог бы попробовать это с моим небольшим проектом. Мой опыт был

  • в основном он отлично работает и сохраняет несколько строк
  • именованное возвращаемое значение на самом деле непригодно для err , поскольку возвращаемое значение его функции определяется как присваиванием, так и специальной функцией try() . очень запутанный
  • в этой функции try() отсутствовала функция «ошибки переноса», как обсуждалось выше.

_"И то, и другое кажется мне хорошим результатом."_

Мы должны согласиться не согласиться здесь.

_ "этот синтаксис try (не обязательно) обрабатывает каждый вариант использования"_

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

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

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

По моему мнению, создание этого прецедента не приведет к хорошим результатам в долгосрочной перспективе...

_"@mikeshenkel"_

PS Сначала не увидел твоего сообщения из-за опечатки. _(это меня не оскорбляет, я просто не получаю уведомления, когда мое имя пользователя пишется с ошибкой...)_

Я ценю приверженность обратной совместимости, которая побуждает вас сделать try встроенной, а не ключевой фразой, но после борьбы с полной _странностью_ наличия часто используемой функции, которая может изменить поток управления ( panic и recover встречаются крайне редко), мне стало интересно: кто-нибудь проводил масштабный анализ частотности try в качестве идентификатора в кодовых базах с открытым исходным кодом? Мне было любопытно и скептически, поэтому я провел предварительный поиск по следующему:

Среди 11 108 770 значимых строк Go, живущих в этих репозиториях, только 63 экземпляра try использовались в качестве идентификатора. Конечно, я понимаю, что эти кодовые базы (несмотря на то, что они большие, широко используемые и важные сами по себе) представляют лишь часть существующего кода Go, и, кроме того, у нас нет возможности напрямую анализировать частные кодовые базы, но это, безусловно, интересный результат.

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

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

Я бы предложил следующие конструкции.

1) Нет обработчика

// The existing proposal, but as a keyword rather than builtin.  When an error is 
// "caught", the function returns all zero values plus the error.  Nothing 
// particularly new here.
func doSomething() (int, error) {
    try SomeFunc()
    a, b := try AnotherFunc()

    // ...

    return 123, nil
}

2) Обработчик

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

func doSomething() (int, error) {
    // Inline error handler
    a, b := try SomeFunc() else err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    handler logAndContinue err {
        log.Errorf("non-critical error: %v", err)
    }
    handler annotateAndReturn err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d := try SomeFunc() else logAndContinue
    e, f := try OtherFunc() else annotateAndReturn

    // ...

    return 123, nil
}

Предлагаемые ограничения:

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

Преимущества:

  • Синтаксис try / else можно тривиально превратить в существующее «составное условие if»:
    go a, b := try SomeFunc() else err { return 0, errors.Wrap(err, "error in doSomething:") }
    становится
    go if a, b, err := SomeFunc(); err != nil { return 0, errors.Wrap(err, "error in doSomething:") }
    На мой взгляд, составные if всегда казались скорее запутанными, чем полезными по очень простой причине: условные операторы обычно появляются _после_ операции и как-то связаны с обработкой ее результатов. Если операция втиснута внутрь условного оператора, просто менее очевидно, что она происходит. Глаз отвлекается. Кроме того, область видимости определенных переменных не так очевидна, как если бы они были самыми левыми в строке.
  • Обработчики ошибок намеренно не определяются как функции (и не имеют ничего похожего на функциональную семантику). Это делает несколько вещей для нас:

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

    • Мы не ограничены в том, что мы можем делать внутри обработчика. Мы обходим критику check / handle то, что «эта структура обработки ошибок хороша только для спасения». Мы также обходим критику «цепочки обработчиков». Любой произвольный код может быть помещен внутрь одного из этих обработчиков, и никакой другой поток управления не подразумевается.

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

    • defer не нужно подрабатывать в качестве механизма обработки ошибок. Мы можем продолжать думать об этом в основном как о способе очистки ресурсов и т. д.

  • Что касается добавления контекста к ошибкам:

    • Добавление контекста с помощью обработчиков чрезвычайно просто и очень похоже на существующие блоки if err != nil

    • Несмотря на то, что конструкция «попробовать без обработчика» напрямую не поощряет добавление контекста, ее очень просто преобразовать в форму обработчика. Его предполагаемое использование в первую очередь будет во время разработки, и было бы очень просто написать проверку на go vet , чтобы выделить необработанные ошибки.

Извините, если эти идеи очень похожи на другие предложения — я старался не отставать от них всех, но, возможно, многое упустил.

@brynbellomy Спасибо за анализ ключевых слов - это очень полезная информация. Кажется, что try в качестве ключевого слова может подойти. (Вы говорите, что API не затронуты - это правда, но try может по-прежнему отображаться как имя параметра или что-то подобное - поэтому документацию, возможно, придется изменить. Но я согласен, что это не повлияет на клиентов этих пакетов.)

Что касается вашего предложения: оно будет прекрасно работать даже без именованных обработчиков, не так ли? (Это упростило бы предложение без потери мощности. Можно было бы просто вызвать локальную функцию из встроенного обработчика.)

Что касается вашего предложения: оно будет прекрасно работать даже без именованных обработчиков, не так ли? (Это упростило бы предложение без потери мощности. Можно было бы просто вызвать локальную функцию из встроенного обработчика.)

@griesemer В самом деле, мне было довольно прохладно включать их. Конечно, больше Go-ish без.

С другой стороны, кажется, что люди хотят иметь возможность выполнять однострочную обработку ошибок, включая однострочные, которые return . Типичным случаем будет log, затем return . Если мы раскошелимся на локальную функцию в предложении else , мы, вероятно, потеряем это:

a, b := try SomeFunc() else err {
    someLocalFunc(err)
    return 0, err
}

(Хотя я все же предпочитаю это составным если)

Тем не менее, вы все равно можете получить однострочный возврат, который добавляет контекст ошибки, реализовав простую настройку gofmt , обсуждавшуюся ранее в этой ветке:

a, b := try SomeFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }

Нужно ли новое ключевое слово в приведенном выше предложении? Почему бы нет:

SomeFunc() else return
a, b := SomeOtherFunc() else err { return 0, errors.Wrap(err, "bad stuff!") }

@griesemer , если обработчики снова на столе, я предлагаю вам создать новую проблему для обсуждения try/handle или try/_label_. В этом предложении специально исключены обработчики, и существует бесчисленное множество способов их определения и вызова.

Любой, кто предлагает обработчики, должен сначала прочитать вики обратной связи проверки/обработки. Велика вероятность, что все, что вы придумаете, там уже описано :-)
https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback

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

https://github.com/golang/go/issues/32437#issuecomment-499808741
https://github.com/golang/go/issues/32437#issuecomment-499852124
https://github.com/golang/go/issues/32437#issuecomment-500095505

@ianlancetaylor , команда go уже рассматривала этот особый вариант обработки ошибок? Его не так просто реализовать, как предлагаемый встроенный try, но он кажется более идиоматичным. ~ненужное заявление, извините.~

Я хотел бы повторить то, что сказал @deanveloper и еще несколько человек, но с собственным акцентом. В https://github.com/golang/go/issues/32437#issuecomment-498939499 @deanveloper сказал:

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

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

Некоторые утверждают, что panic уже создала прецедент для встроенной функции, которая изменяет поток управления, но я думаю, что panic принципиально отличается по двум причинам:

  1. Паника не условна; он всегда прерывает вызывающую функцию.
  2. Panic не возвращает никаких значений и поэтому может отображаться только как отдельная инструкция, что увеличивает ее видимость.

Попробуйте с другой стороны:

  1. является условным; он может возвращаться или не возвращаться из вызывающей функции.
  2. Возвращает значения и может появляться в составном выражении, возможно, несколько раз, в одной строке, потенциально за правым полем моего окна редактора.

По этим причинам я думаю, что try кажется больше, чем «откусил», я думаю, что это существенно вредит читабельности кода.

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

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

````
если это != "сломал" {
не исправлять (это)
}

@ChrisHines К вашему замечанию (которое повторяется в другом месте этой темы), давайте добавим еще одно ограничение:

  • любой оператор try (даже без обработчика) должен располагаться на отдельной строке.

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

Так что без всякой ерунды:

try EmitEvent(try (try DecodeMsg(m)).SaveToDB())

а скорее вот это:

dm := try DecodeMsg(m)
um := try dm.SaveToDB()
try EmitEvent(um)

что все еще кажется более ясным, чем это:

dm, err := DecodeMsg(m)
if err != nil {
    return nil, err
}

um, err := dm.SaveToDB()
if err != nil {
    return nil, err
}

err = EmitEvent(um)
if err != nil {
    return nil, err
}

Что мне нравится в этом дизайне, так это то, что невозможно молча игнорировать ошибки, не аннотируя их при этом. В то время как прямо сейчас вы иногда видите x, _ := SomeFunc() (что такое игнорируемое возвращаемое значение? ошибка? что-то еще?), теперь вам нужно четко аннотировать:

x := try SomeFunc() else err {}

Со времени моего предыдущего поста в поддержку этого предложения я увидел две идеи, опубликованные @jagv (без параметров try возвращает *error ) и @josharian (помеченные обработчики ошибок), которые, как мне кажется, слегка измененная форма значительно улучшила бы предложение.

Объединив эти идеи с еще одной, которую я придумал сам, у нас будет четыре версии try :

  1. пытаться()
  2. попробовать (параметры)
  3. попытка (параметры, метка)
  4. попытка (параметры, паника)

1 просто вернул бы указатель на параметр возврата ошибки (ERP) или ноль, если его не было (только #4). Это обеспечило бы альтернативу именованной ERP без необходимости добавления дополнительных встроенных модулей.

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

3 будет работать, как предложил @josharian , т.е. при ненулевой ошибке код будет переходить к метке. Однако не будет метки обработчика ошибок по умолчанию, так как этот случай теперь выродится в № 2.

Мне кажется, что обычно это лучший способ оформления ошибок (или обработки их локально, а затем возврата nil), чем defer , так как это проще и быстрее. Любой, кому это не нравилось, все еще мог использовать № 2.

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

Таким образом, нормальная метка и поведение goto будут применяться (как сказал @josharian ) к # 26058, исправленному в первую очередь, но я думаю, что это все равно должно быть исправлено.

Название ярлыка не может быть panic , так как это будет противоречить #4.

4 немедленно panic вместо возврата или ветвления. Следовательно, если бы это была единственная версия try , используемая в конкретной функции, ERP не потребовалось бы.

Я добавил это, чтобы пакет тестирования мог работать так, как сейчас, без необходимости дополнительных встроенных или других изменений. Однако это может быть полезно и в других _фатальных_ сценариях.

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

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

Например, @deanveloper очень хорошо выразил эту озабоченность в https://github.com/golang/go/issues/32437#issuecomment -498932961, что, я думаю, является комментарием, получившим наибольшее количество голосов.

@dominikh написал в https://github.com/golang/go/issues/32437#issuecomment -499067357:

В коде, созданном с помощью gofmt, return всегда соответствует /^\t*return / — это очень простой шаблон, который можно обнаружить на глаз без посторонней помощи. try, с другой стороны, может встречаться в любом месте кода, вложенного произвольно глубоко в вызовы функций. Никакое обучение не позволит нам сразу определить весь поток управления в функции без помощи инструментов.

Чтобы помочь с этим, @brynbellomy предложил вчера:

любой оператор try (даже без обработчика) должен располагаться на отдельной строке.

Принимая это во внимание, try может потребоваться в качестве начала строки даже для присваивания.

Так что это может быть:

try dm := DecodeMsg(m)
try um := dm.SaveToDB()
try EmitEvent(um)

а не следующее (из примера @brynbellomy ):

dm, err := DecodeMsg(m)
if err != nil {
    return nil, err
}

um, err := dm.SaveToDB()
if err != nil {
    return nil, err
}

err = EmitEvent(um)
if err != nil {
    return nil, err
}

Кажется, это сохранит достаточную видимость даже без какого-либо редактора или помощи IDE, при этом уменьшив шаблон.

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

Предложение включает в себя этот пример:

info := try(try(os.Open(file)).Stat())    // proposed try built-in

Вместо этого может быть:

try f := os.Open(file)
try info := f.Stat()

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

@elagergren-spideroak предоставил этот пример:

try(try(try(to()).parse().this)).easily())

Я думаю, что это несоответствие скобок, что, возможно, является преднамеренным замечанием или тонким юмором, поэтому я не уверен, предназначен ли этот пример для 2 try или 3 try . В любом случае, возможно, было бы лучше потребовать распространения на 2-3 строки, начинающиеся с try .

@thepudds , это то, что я имел в виду в своем предыдущем комментарии. За исключением того, что дано

try f := os.Open(file)
try info := f.Stat()

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

try (
    f := os.Open(file)
    into := f.Stat()
)

Если компилятор знает, как с этим справиться, то же самое работает и для вложенности. Итак, теперь вышеизложенное может стать

try info := os.Open(file).Stat()

Из сигнатур функций компилятор знает, что Open может возвращать значение ошибки, и, поскольку он находится в блоке try, ему необходимо сгенерировать обработку ошибок, а затем вызвать Stat() для основного возвращаемого значения и так далее.

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

try (
    f := os.Open(file)
    debug("f: %v\n", f) // debug returns nothing
    into := f.Stat()
)

Это позволяет развивать код без перестановки блоков try. Но по какой-то странной причине люди думают, что обработка ошибок должна быть явно прописана! Они хотят

try(try(try(to()).parse()).this)).easily())

Пока меня все устраивает

try to().parse().this().easily()

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

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

Меня несколько беспокоила удобочитаемость программ, в которых try появляется внутри других выражений. Поэтому я запустил grep "return .*err$" в стандартной библиотеке и начал читать блоки случайным образом. Всего 7214 результатов, я прочитал только пару сотен.

Первое, что следует отметить, это то, что там, где применяется try , почти все эти блоки становятся немного более читабельными.

Во-вторых, очень немногие из них, менее 1 из 10, поместили бы try в другое выражение. Типичным случаем являются операторы вида x := try(...) или ^try(...)$ .

Вот несколько примеров, когда try появляется внутри другого выражения:

текст/шаблон

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        lessOrEqual, err := le(arg1, arg2)
        if err != nil {
                return false, err
        }
        return !lessOrEqual, nil
}

становится:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        return !try(le(arg1, arg2)), nil
}

текст/шаблон

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
        switch v.Kind() {
        case reflect.Map:
                index, err := prepareArg(index, v.Type().Key())
                if err != nil {
                        return reflect.Value{}, err
                }
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

становится

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                if x := v.MapIndex(try(prepareArg(index, v.Type().Key()))); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

(это самый сомнительный пример, который я видел)

регулярное выражение/синтаксис:

regexp/syntax/parse.go

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        if c, t, err = nextRune(t); err != nil {
                return nil, err
        }
        p.literal(c)
        ...
}

становится

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        c, t = try(nextRune(t))
        p.literal(c)
        ...
}

Это не пример try внутри другого выражения, но я хочу вызвать его, потому что это улучшает читабельность. Здесь гораздо легче увидеть, что значения c и t находятся за пределами действия оператора if.

сеть/http

сеть/http/request.go:readRequest

        mimeHeader, err := tp.ReadMIMEHeader()
        if err != nil {
                return nil, err
        }
        req.Header = Header(mimeHeader)

становится:

        req.Header = Header(try(tp.ReadMIMEHeader())

база данных/sql

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                connector, err := driverCtx.OpenConnector(dataSourceName)
                if err != nil {
                        return nil, err
                }
                return OpenDB(connector), nil
        }

становится

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
        }

база данных/sql

        si, err := ctxDriverPrepare(ctx, dc.ci, query)
        if err != nil {
                return nil, err
        }
        ds := &driverStmt{Locker: dc, si: si}

становится

        ds := &driverStmt{
                Locker: dc,
                si: try(ctxDriverPrepare(ctx, dc.ci, query)),
        }

сеть/http

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                cc, err := p.t.dialclientconn(addr, singleuse)
                if err != nil {
                        return nil, err
                }
                return cc, nil
        }

становится

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                return try(p.t.dialclientconn(addr, singleuse))
        }

сеть/http

func (f *http2Framer) endWrite() error {
        ...
        n, err := f.w.Write(f.wbuf)
        if err == nil && n != len(f.wbuf) {
                err = io.ErrShortWrite
        }
        return err
}

становится

func (f *http2Framer) endWrite() error {
        ...
        if try(f.w.Write(f.wbuf) != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

(Этот мне очень нравится.)

сеть/http

        if f, err := fr.ReadFrame(); err != nil {
                return nil, err
        } else {
                hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        }

становится

        hc = try(fr.ReadFrame()).(*http2ContinuationFrame)// guaranteed by checkFrameOrder

}

(Тоже приятно.)

сеть :

        if ctrlFn != nil {
                c, err := newRawConn(fd)
                if err != nil {
                        return err
                }
                if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
                        return err
                }
        }

становится

        if ctrlFn != nil {
                try(ctrlFn(fd.ctrlNetwork(), laddr.String(), try(newRawConn(fd))))
        }

может быть, это слишком много, и вместо этого должно быть:

        if ctrlFn != nil {
                c := try(newRawConn(fd))
                try(ctrlFn(fd.ctrlNetwork(), laddr.String(), c))
        }

В целом, мне очень нравится эффект try на код стандартной библиотеки, который я читаю.

И последнее замечание: использование try для чтения кода помимо нескольких примеров в предложении было поучительным. Я думаю, что стоит подумать о написании инструмента для автоматического преобразования кода для использования try (где это не меняет семантику программы). Было бы интересно прочитать образец различий, созданных с популярными пакетами на github, чтобы увидеть, выдержит ли то, что я нашел в стандартной библиотеке. Выходные данные такой программы могли бы дать дополнительное представление о влиянии предложения.

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

Поскольку это было так близко к интересному предложению @thepudds сделать try оператором, я переписал все примеры, используя этот синтаксис, и нашел его намного яснее, чем выражение- try или статус-кво, не требуя слишком много дополнительных строк:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}
func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                try index := prepareArg(index, v.Type().Key())
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}
func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        try c, t = nextRune(t)
        p.literal(c)
        ...
}
        try mimeHeader := tp.ReadMIMEHeader()
        req.Header = Header(mimeHeader)
        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                try connector := driverCtx.OpenConnector(dataSourceName)
                return OpenDB(connector), nil
        }

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

        try si := ctxDriverPrepare(ctx, dc.ci, query)
        ds := &driverStmt{Locker: dc, si: si}

Это в основном наихудший случай для этого, и он выглядит нормально:

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                try cc := p.t.dialclientconn(addr, singleuse)
                return cc, nil
        }

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

func (f *http2Framer) endWrite() error {
        ...
        if try n := f.w.Write(f.wbuf); n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}
        try f := fr.ReadFrame()
        hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        if ctrlFn != nil {
                try c := newRawConn(fd)
                try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
        }

Просматривая примеры @crawshaw , я только больше убеждаюсь, что поток управления часто делается достаточно загадочным, чтобы быть еще более осторожным в дизайне. Связывание даже небольшой сложности становится трудным для чтения и легко испортить. Я рад рассмотреть варианты, но усложнение потока управления в таком защищенном языке кажется исключительно нехарактерным.

func (f *http2Framer) endWrite() error {
        ...
        if try(f.w.Write(f.wbuf) != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

Кроме того, try ничего не "пытается". Это «защитное реле». Если базовая семантика предложения отключена, я не удивлен, что полученный код также проблематичен.

func (f *http2Framer) endWrite() error {
        ...
        relay n := f.w.Write(f.wbuf)
        return checkShortWrite(n, len(f.wbuf))
}

Если вы делаете оператор try, вы можете использовать флаг, чтобы указать, какое возвращаемое значение и какое действие:

try c, @      := newRawConn(fd) // return
try c, <strong i="6">@panic</strong> := newRawConn(fd) // panic
try c, <strong i="7">@hname</strong> := newRawConn(fd) // invoke named handler
try c, <strong i="8">@_</strong>     := newRawConn(fd) // ignore, or invoke "ignored" handler if defined

Вам по-прежнему нужен синтаксис подвыражения (Расс заявил, что это требование), по крайней мере, для действий паники и игнорирования.

Во-первых, я аплодирую @crawshaw за то, что он нашел время, чтобы просмотреть примерно 200 реальных примеров, и уделил время своему вдумчивому описанию выше.

Во-вторых, @jimmyfrasche , относительно вашего ответа здесь о примере http2Framer :


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

```
func (f *http2Framer) ошибка endWrite() {
...
если попробовать n := fwWrite(f.wbuf); п != len(f.wbuf) {
вернуть io.ErrShortWrite
}
вернуть ноль
}

At least under what I was suggesting above in https://github.com/golang/go/issues/32437#issuecomment-500213884, under that proposal variation I would suggest `if try` is not allowed.

That `http2Framer` example could instead be:

func (f *http2Framer) ошибка endWrite() {
...
попробуйте n := fwWrite(f.wbuf)
если n != len(f.wbuf) {
вернуть io.ErrShortWrite
}
вернуть ноль
}
`` That is one line longer, but hopefully still "light on the page". Personally, I think that (arguably) reads more cleanly, but more importantly it is easier to see the попробуйте`.

@deanveloper написал выше в https://github.com/golang/go/issues/32437#issuecomment -498932961:

Возврат из функции, казалось, был «священным» делом.

Этот конкретный пример http2Framer оказывается не таким коротким, как мог бы быть. Тем не менее, возврат из функции более «священен», если try должен быть первым в строке.

@crowshaw упомянул:

Во-вторых, очень немногие из них, менее 1 из 10, поместили бы try внутрь другого выражения. Типичным случаем являются операторы вида x := try(...) или ^try(...)$.

Может быть, можно лишь частично помочь тем 1 из 10 примеров с более ограниченной формой try , особенно если типичный случай из этих примеров заканчивается тем же количеством строк, даже если try должен быть первым в строке?

@джиммифраше

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

Поскольку это было так близко к интересному предложению @thepudds сделать try оператором, я переписал все примеры, используя этот синтаксис, и нашел его намного яснее, чем выражение- try или статус-кво, не требуя слишком много дополнительных строк:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}

Ваш первый пример хорошо иллюстрирует, почему я предпочитаю выражение try . В вашей версии я должен поместить результат вызова le в переменную, но эта переменная не имеет семантического значения, которое термин le еще не подразумевает. Поэтому я не могу дать ему имя, которое не было бы либо бессмысленным (например, x ), либо избыточным (например, lessOrEqual ). С выражением- try промежуточная переменная не нужна, так что эта проблема даже не возникает.

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

Я рад поддержать несколько последних сообщений, в которых try (ключевое слово) было перемещено в начало строки. Он действительно должен иметь то же визуальное пространство, что и return .

Re: Предложение @jimmyfrasche разрешить try в составных операторах if — это именно то, чего, я думаю, многие здесь пытаются избежать по нескольким причинам:

  • он объединяет два очень разных механизма потока управления в одну строку
  • выражение try на самом деле вычисляется первым и может привести к возврату функции, но оно появляется после if
  • они возвращаются с совершенно разными ошибками, одну из которых мы на самом деле не видим в коде, а другую видим
  • это делает менее очевидным, что try фактически не обрабатывается, потому что блок очень похож на блок обработчика (хотя он обрабатывает совершенно другую проблему)

Можно подойти к этой ситуации с несколько иной точки зрения, предпочитая подталкивать людей к обработке try s. Как насчет того, чтобы позволить синтаксису try / else содержать последующие условные операторы (что является общим шаблоном для многих функций ввода-вывода, которые возвращают как err , так и n , любой из которых может указывать на проблему):

func (f *http2Framer) endWrite() error {
        // ...
        try n := f.w.Write(f.wbuf) else err {
                return errors.Wrap(err, "error writing:")
        } else if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

В случае, если вы не обрабатываете ошибку, возвращаемую .Write , у вас все равно будет четкая аннотация о том, что .Write может привести к ошибке (как указано @thepudds):

func (f *http2Framer) endWrite() error {
        // ...
        try n := f.w.Write(f.wbuf)
        if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

Я второй ответ @daved . На мой взгляд, каждый пример, который выделил @crawshaw , стал менее ясным и более подверженным ошибкам в результате try .

Я рад поддержать несколько последних сообщений, в которых try (ключевое слово) было перемещено в начало строки. Он действительно должен иметь то же визуальное пространство, что и return .

Учитывая два варианта для этой точки и предполагая, что был выбран один из них, что создало своего рода прецедент для будущих потенциальных функций:

А.)

try f := os.Open(filepath) else err {
    return errors.Wrap(err, "can't open")
}

Б.)

f := try os.Open(filepath) else err {
    return errors.Wrap(err, "can't open")
}

Какой из двух обеспечивает большую гибкость для будущего использования новых ключевых слов? _(Я не знаю ответа на этот вопрос, так как не освоил тёмное искусство написания компиляторов.)_ Будет ли один подход более ограничивающим, чем другой?

@davecheney @daved @crowshaw
Я склонен согласиться с Дэйвами в этом вопросе: в примерах @crawshaw есть много операторов try , встроенных глубоко в строки, в которых происходит много других вещей. Действительно трудно определить точки выхода. Кроме того, скобки try в некоторых примерах сильно загромождают.

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

  • try в качестве ключевого слова
  • только один try в строке
  • try должен стоять в начале строки

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

текст/шаблон

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        lessOrEqual, err := le(arg1, arg2)
        if err != nil {
                return false, err
        }
        return !lessOrEqual, nil
}

становится:

func gt(arg1, arg2 reflect.Value) (bool, error) {
        // > is the inverse of <=.
        try lessOrEqual := le(arg1, arg2)
        return !lessOrEqual, nil
}

текст/шаблон

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
...
        switch v.Kind() {
        case reflect.Map:
                index, err := prepareArg(index, v.Type().Key())
                if err != nil {
                        return reflect.Value{}, err
                }
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

становится

func index(item reflect.Value, indices ...reflect.Value) (reflect.Value, error) {
        ...
        switch v.Kind() {
        case reflect.Map:
                try index := prepareArg(index, v.Type().Key())
                if x := v.MapIndex(index); x.IsValid() {
                        v = x
                } else {
                        v = reflect.Zero(v.Type().Elem())
                }
        ...
        }
}

регулярное выражение/синтаксис:

regexp/syntax/parse.go

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        if c, t, err = nextRune(t); err != nil {
                return nil, err
        }
        p.literal(c)
        ...
}

становится

func Parse(s string, flags Flags) (*Regexp, error) {
        ...
        try c, t = nextRune(t)
        p.literal(c)
        ...
}

сеть/http

сеть/http/request.go:readRequest

        mimeHeader, err := tp.ReadMIMEHeader()
        if err != nil {
                return nil, err
        }
        req.Header = Header(mimeHeader)

становится:

        try mimeHeader := tp.ReadMIMEHeader()
        req.Header = Header(mimeHeader)

база данных/sql

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                connector, err := driverCtx.OpenConnector(dataSourceName)
                if err != nil {
                        return nil, err
                }
                return OpenDB(connector), nil
        }

становится

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                try connector := driverCtx.OpenConnector(dataSourceName)
                return OpenDB(connector), nil
        }

база данных/sql

        si, err := ctxDriverPrepare(ctx, dc.ci, query)
        if err != nil {
                return nil, err
        }
        ds := &driverStmt{Locker: dc, si: si}

становится

        try si := ctxDriverPrepare(ctx, dc.ci, query)
        ds := &driverStmt{Locker: dc, si: si}

сеть/http

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                cc, err := p.t.dialclientconn(addr, singleuse)
                if err != nil {
                        return nil, err
                }
                return cc, nil
        }

становится

        if http2isconnectioncloserequest(req) && dialonmiss {
                // it gets its own connection.
                http2tracegetconn(req, addr)
                const singleuse = true
                try cc := p.t.dialclientconn(addr, singleuse)
                return cc, nil
        }

сеть/http
Это на самом деле не экономит нам ни одной строки, но я нахожу его намного понятнее, потому что if err == nil — относительно необычная конструкция.

func (f *http2Framer) endWrite() error {
        ...
        n, err := f.w.Write(f.wbuf)
        if err == nil && n != len(f.wbuf) {
                err = io.ErrShortWrite
        }
        return err
}

становится

func (f *http2Framer) endWrite() error {
        ...
        try n := f.w.Write(f.wbuf)
        if n != len(f.wbuf) {
                return io.ErrShortWrite
        }
        return nil
}

сеть/http

        if f, err := fr.ReadFrame(); err != nil {
                return nil, err
        } else {
                hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
        }

становится

        try f := fr.ReadFrame()
        hc = f.(*http2ContinuationFrame) // guaranteed by checkFrameOrder
}

сеть:

        if ctrlFn != nil {
                c, err := newRawConn(fd)
                if err != nil {
                        return err
                }
                if err := ctrlFn(fd.ctrlNetwork(), laddr.String(), c); err != nil {
                        return err
                }
        }

становится

        if ctrlFn != nil {
                try c := newRawConn(fd)
                try ctrlFn(fd.ctrlNetwork(), laddr.String(), c)
        }

@james-lawrence В ответ на https://github.com/golang/go/issues/32437#issuecomment -500116099 : я не припомню, чтобы такие идеи, как необязательный , err , серьезно рассматривались, нет. Лично я думаю, что это плохая идея, потому что это означает, что если функция изменится, чтобы добавить завершающий параметр error , существующий код продолжит компилироваться, но будет действовать совсем по-другому.

Использование defer для обработки ошибок имеет большой смысл, но это приводит к необходимости называть ошибку и создавать новый тип шаблона if err != nil .

Внешние обработчики должны сделать это:

func handler(err *error) {
  if *err != nil {
    *err = handle(*err)
  }
} 

который используется как

defer handler(&err)

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

Внутренние обработчики должны сделать это:

defer func() {
  if err != nil {
    err = handle(err)
  }
}()

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

Как я упоминал ранее в ветке, это можно абстрагировать в одну функцию:

func catch(err *error, handle func(error) error) {
  if *err != nil && handle != nil {
    *err = handle(*err)
  }
}

Это противоречит озабоченности @griesemer по поводу неоднозначности функций обработчика nil и имеет свои собственные шаблоны defer и func(err error) error , в дополнение к необходимости называть err во внешней функции.

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

Синтаксически это будет очень похоже на handle :

catch err {
  return handleThe(err)
}

Семантически это было бы сахаром для кода внутреннего обработчика выше:

defer func() {
  if err != nil {
    err = handleThe(err)
  }
}()

Поскольку это несколько волшебно, он может перехватить ошибку внешней функции, даже если она не была названа. ( err после catch больше похоже на имя параметра для блока catch ).

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

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

Сложная обработка ошибок может потребовать не использовать catch так же, как может потребовать не использовать try .

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

catch err {
  log.Print(err)
  return err
}

или просто обернуть все возвращенные ошибки:

catch err {
  return fmt.Errorf("foo: %w", err)
}

@ianlancetaylor

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

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

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

Можете ли вы привести пример, когда кто-то меняет подпись, чтобы добавить возвращаемое значение? Для меня они обычно попадали в категорию _ «Я не понимал, что может произойти ошибка» _ или _ «Я чувствую себя ленивым и не хочу прикладывать усилия, потому что ошибка, вероятно, не произойдет». _

В обоих этих случаях я мог бы добавить возврат ошибки, потому что становится очевидным, что ошибку необходимо обработать. В таком случае, если я не могу изменить подпись, потому что не хочу нарушать совместимость для других разработчиков, использующих мои пакеты, что делать? Я предполагаю, что в подавляющем большинстве случаев ошибка будет возникать и что код, вызывающий func, который не возвращает ошибку, будет действовать совсем по-другому, _все равно._

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

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


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

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


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

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

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

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

Ключевое слово try , безусловно, улучшает читаемость (по сравнению с вызовом функции) и кажется менее сложным. @brynbellomy @crawshaw спасибо, что нашли время написать примеры.

Я полагаю, моя общая мысль такова, что try делает слишком много. Он решает: вызвать функцию, назначить переменные, проверить ошибку и вернуть ошибку, если она существует. Я предлагаю вместо этого сократить объем и решить только для условного возврата: «возврат, если последний аргумент не равен нулю».

Возможно, это не новая идея... Но просмотрев предложения в вики с отзывами об ошибках , я ее не нашел (не значит, что ее там нет)

Мини-предложение по условному возврату

отрывок:

err, thing := newThing(name)
refuse nil, err

Я также добавил его в вики в разделе «альтернативные идеи».

Ничего не делать также кажется очень разумным вариантом.

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

assert(nil, err)
assert(len(a), len(b))
assert(true, condition)
assert(expected, given)

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

Данное значение будет завернуто в ошибку и возвращено.

@alexhornbake

Точно так же, как try на самом деле не пытается, refuse на самом деле не «отказывается». Общее намерение здесь заключалось в том, что мы устанавливаем «защитное реле» ( relay — короткое, точное и аллитеративное слово к return ), которое «срабатывает», когда одно из подключенных значений соответствует условию. (т.е. ненулевая ошибка). Это своего рода автоматический выключатель, и я считаю, что он может повысить ценность, если его дизайн будет ограничен неинтересными случаями, чтобы просто уменьшить некоторые из самых низко висящих шаблонов. Все, что хоть немного сложно, должно основываться на простом коде Go.

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

        req.Header = Header(try(tp.ReadMIMEHeader())

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

        if driverCtx, ok := driveri.(driver.DriverContext); ok {
                return OpenDB(try(driverCtx.OpenConnector(dataSourceName))), nil
        }

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

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

        ds := &driverStmt{
                Locker: dc,
                si: try(ctxDriverPrepare(ctx, dc.ci, query)),
        }   

Это место, где код может дать сбой там, где раньше это было невозможно. Без try построение литералов структуры не может завершиться ошибкой . Мои глаза будут скользить по нему, как "хорошо, создание driverStmt... идем дальше...", и это будет так легко пропустить, что на самом деле это может привести к ошибке вашей функции. Единственный способ, который был бы возможен раньше, это если бы ctxDriverPrepare запаниковал... и мы все знаем, что это случай, когда 1.) в принципе никогда не должно произойти и 2.) если это произойдет, это означает, что что-то совершенно неправильно.

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

@daved Я не уверен, что следую. Вам не нравится название или идея?

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

Хотя мне нравится предложение catch от @jimmyfrasche , я хотел бы предложить альтернативу:
go handler fmt.HandleErrorf("copy %s %s", src, dst)
будет эквивалентно:
go defer func(){ if(err != nil){ fmt.HandleErrorf(&err,"copy %s %s", src, dst) } }()
где err — последнее именованное возвращаемое значение с ошибкой типа. Однако обработчики также могут использоваться, когда возвращаемые значения не названы. Допускается и более общий случай:
go handler func(err *error){ *err = fmt.Errorf("foo: %w", *err) }() `
Основная проблема, с которой я столкнулся при использовании именованных возвращаемых значений (которую catch не решает), заключается в том, что ошибка избыточна. При откладывании вызова обработчика, такого как fmt.HandleErrorf , нет разумного первого аргумента, кроме указателя на возвращаемое значение ошибки, зачем давать пользователю возможность совершить ошибку?

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

@yiius catch , как я определил, не требует имени err в функции, содержащей catch .

В catch err { err — это имя ошибки в блоке catch . Это похоже на имя параметра функции.

При этом нет необходимости в чем-то вроде fmt.HandleErrorf , потому что вы можете просто использовать обычный fmt.Errorf :

func f() error {
  catch err {
    return fmt.Errorf("foo: %w", err)
  }
  return errors.New("bar")
}

который возвращает ошибку, которая печатается как foo: bar .

Мне не нравится такой подход, потому что:

  • Вызов функции try() прерывает выполнение кода в родительской функции.
  • нет ключевого слова return , но код действительно возвращается.

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

  1. Он должен значительно отличаться и лучше, чем if x, err := thingie(); err != nil { handle(err) } . Я думаю, что предложения вроде try x := thingie else err { handle(err) } не соответствуют этой планке. Почему бы просто не сказать if ?

  2. Он должен быть ортогонален существующей функциональности defer . То есть он должен отличаться достаточно, чтобы было ясно, что предлагаемый механизм обработки необходим сам по себе, не создавая странных угловых случаев, когда handle и defer взаимодействуют.

Помните об этих пожеланиях, когда мы обсуждаем альтернативные механизмы для try /handle.

@carlmjohnson Мне нравится идея @jimmyfrasche catch относительно вашего пункта 2 - это просто синтаксический сахар для defer , который экономит 2 строки и позволяет вам не называть возвращаемое значение ошибки (которое в очередь также потребует от вас назвать все остальные, если вы еще этого не сделали). Это не вызывает проблемы ортогональности с defer , потому что это defer .

повторяя то, что сказал @ubombi :

Вызов функции try() прерывает выполнение кода в родительской функции.; нет ключевого слова return, но код действительно возвращается.

В Ruby procs и lambdas являются примером того, что делает try ... proc — это блок кода, который его оператор return возвращает не из самого блока, а из вызывающего.

Это именно то, что делает try ... это просто предопределенная процедура Ruby.

Я думаю, что если бы мы собирались пойти по этому пути, возможно, мы действительно могли бы позволить пользователю определить свою собственную функцию try , введя proc functions

Я по-прежнему предпочитаю if err != nil , потому что это более читабельно, но я думаю, что try было бы полезнее, если бы пользователь определял свой собственный процесс:

proc try(err *error, msg string) {
  if *err != nil {
    *err = fmt.Errorf("%v: %w", msg, *err)
    return
  }
}

И тогда вы можете назвать это:

func someFunc() (string, error) {
  err := doSomething()
  try(&err, "someFunc failed")
}

Преимущество здесь в том, что вы можете определить обработку ошибок в своих собственных терминах. И вы также можете сделать proc открытым, закрытым или внутренним.

Это также лучше, чем предложение handle {} в исходном предложении Go2, потому что вы можете определить его только один раз для всей кодовой базы, а не для каждой функции.

Одним из соображений удобочитаемости является то, что func() и proc() могут вызываться по-разному, например, func() и proc!() , чтобы программист знал, что вызов proc может фактически вернуться из вызывающая функция.

@marwan-at-work, разве try(err, "someFunc failed") не должно быть try(&err, "someFunc failed") в вашем примере?

@dpinela спасибо за исправление, обновил код :)

Обычная практика, которую мы здесь пытаемся переопределить, - это то, что стандартная раскрутка стека во многих языках предлагает в виде исключения (поэтому было выбрано слово «попытаться»...).
Но если бы мы могли разрешить только функцию (...try() или другую), которая перескакивала бы на два уровня назад в трассировке, тогда

try := handler(err error) {     //which corelates with - try := func(err error) 
   if err !=nil{
       //do what ever you want to do when there's an error... log/print etc
       return2   //2 levels
   }
} 

а потом такой код
f := try(os.Open(имя файла))
может делать именно так, как советует предложение, но как функция (или фактически «функция обработчика») разработчик будет иметь гораздо больший контроль над тем, что делает функция, как она форматирует ошибку в разных случаях, используйте аналогичный обработчик повсюду код для обработки (допустим) os.Open вместо того, чтобы каждый раз писать fmt.Errorf("ошибка открытия файла %s....").
Это также приведет к обработке ошибок, как если бы «попытка» не была определена - это ошибка времени компиляции.

@guybrand Такой двухуровневый возврат return2 (или «нелокальный возврат», как общая концепция называется в Smalltalk) был бы хорошим универсальным механизмом (также предложенным @mikeschinkel в #32473) . Но похоже, что try по-прежнему нужен в вашем предложении, поэтому я не вижу причин для return2try может просто сделать return . Было бы интереснее, если бы можно было написать try локально, но это невозможно для произвольных подписей.

@griesemer

_"поэтому я не вижу причин для return2 - try может просто сделать return ."_

Одна из причин — как я указал в #32473 _(спасибо за ссылку)_ — состоит в том, чтобы разрешить несколько уровней break и continue в дополнение к return .

Еще раз спасибо всем за все новые комментарии; это значительные затраты времени, чтобы не отставать от обсуждения и писать подробные отзывы. И даже лучше, несмотря на порой страстные споры, до сих пор это была довольно цивилизованная тема. Спасибо!

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

1) В общем, использование встроенной функции try считается плохим выбором: учитывая, что это влияет на поток управления, оно должно быть _по крайней мере_ ключевым словом ( @carloslenz "предпочитает делать это оператором без скобка"); try как выражение кажется не очень хорошей идеей, это вредит читабельности ( @ChrisHines , @jimmyfrasche), они "возвращаются без return ". @brynbellomy провел фактический анализ try , используемых в качестве идентификаторов; кажется, что их очень мало в процентном отношении, поэтому можно было бы пойти по маршруту ключевого слова, не затрагивая слишком много кода.

2) @crawshaw потратил некоторое время на анализ пары сотен вариантов использования из библиотеки std и пришел к выводу, что try в предложенном виде почти всегда улучшает читабельность. @jimmyfrasche пришел к противоположному выводу.

3) Еще одна тема заключается в том, что использование defer для оформления ошибок не является идеальным. @josharian указывает, что defer всегда запускаются при возврате функции, но если они здесь для оформления ошибки, мы заботимся об их теле только в случае ошибки, что может быть источником путаницы.

4) Многие написали предложения по улучшению предложения. @zeebo , @patrick-nyt поддерживают форматирование gofmt простых операторов if в одну строку (и довольны статус-кво). @jargv предположил, что try() (без аргументов) может возвращать указатель на текущую «ожидающую» ошибку, что устранит необходимость называть результат ошибки только для того, чтобы иметь доступ к нему в defer ; @masterada предложил вместо этого использовать errorfunc() . @velovix возродил идею try с двумя аргументами, где второй аргумент был бы обработчиком ошибок.

@klaidliadon , @networkimprov выступают за специальные «операторы присваивания», такие как f, # := os.Open() вместо try . @networkimprov подал более подробное альтернативное предложение по изучению таких подходов (см. проблему № 32500). @mikeschinkel также подал альтернативное предложение, предлагая ввести две новые языковые функции общего назначения, которые также можно было бы использовать для обработки ошибок, а не для конкретных ошибок try (см. проблему № 32473). @josharian возродил возможность, которую мы обсуждали на GopherCon в прошлом году, когда try не возвращается при ошибке, а вместо этого переходит (с goto ) к метке с именем error (альтернативно , try может принимать имя целевой метки).

5) По поводу ключевого слова try появилось два направления мыслей. @brynbellomy предложил версию, в которой можно указать обработчик:

a, b := try f()
a, b := try f() else err { /* handle error */ }

@thepudds идет еще дальше и предлагает try в начале строки, давая try ту же видимость, что и return :

try a, b := f()

Оба они могут работать с defer .

@griesemer

Спасибо за ссылку на @mikeschinkel #32473, у нее много общего.

касательно

Но, похоже, в вашем предложении все еще нужна попытка
Хотя мое предложение может быть реализовано с "любым" обработчиком, а не с зарезервированным "встроенным/ключевым словом/выражением", я не думаю, что "try()" - плохая идея (и поэтому не проголосовал за нее), я пытаюсь «расширить его» - чтобы показать больше преимуществ, как многие ожидали, «после выхода go 2.0»

Я думаю, что это также может быть источником «смешанных флюидов», о которых вы сообщили в своем последнем резюме — это не «try () не улучшает обработку ошибок» — конечно, это так, это «ожидание, пока Go 3.0 решит какую-то другую серьезную ошибку». справляться с болями" люди, указанные выше, выглядят слишком долго :)

Я провожу опрос о «болезнях обработки ошибок» (и озвучиваю, что некоторые из них просто «я не использую передовые методы», тогда как некоторые я даже не представлял себе, что люди (в основном из других языков) захотят сделать - от круто к ВТФ).

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

наконец-Спасибо за потрясающую работу и терпение!

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

try a, b := f() else err { return fmt.Errorf("Decorated: %s", err); }
if a,b, err :=f;  err != nil { return fmt.Errorf("Decorated: %s", err); }
try a, b := f()
if a,b, err :=f;  err != nil { return err; }

Итак, в отличие от того, что я думал раньше, может быть, достаточно просто изменить go fmt, по крайней мере, для случая оформленной/обработанной ошибки. В то время как для случая ошибки, что-то вроде try все еще может быть желательным в качестве синтаксического сахара для этого очень распространенного случая использования.

Что касается try else , я думаю, что условные функции ошибок, такие как fmt.HandleErrorf (редактировать: я предполагаю, что он возвращает ноль, когда ввод равен нулю) в начальном комментарии работают нормально, поэтому добавление else не нужно.

a, b, err := doSomething()
try fmt.HandleErrorf(&err, "Decorated "...)

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

Я также согласен с @daved в том, что имя не подходит. В конце концов, здесь мы пытаемся добиться защищенного присваивания, так почему бы не использовать guard , как в Swift, и сделать предложение else необязательным? Что-то типа

GuardStmt = "guard" ( Assignment | ShortVarDecl | Expression ) [  "else" Identifier Block ] .

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

Сначала мне не нравилось предложение else , потому что это просто синтаксический сахар вокруг обычного присваивания, за которым следует if err != nil , но после просмотра некоторых примеров это просто имеет смысл: использование guard делает намерение более ясным.

РЕДАКТИРОВАТЬ: некоторые предлагали использовать такие вещи, как catch , чтобы как-то указать разные обработчики ошибок. Я нахожу else одинаково жизнеспособным с точки зрения семантики, и это уже есть в языке.

Хотя мне нравится оператор try-else, как насчет такого синтаксиса?

a, b, (err) := func() else { return err }

Выражение try - else является тернарным оператором.

a, b := try f() else err { ... }
fmt.Println(try g() else err { ... })`

Оператор try - else является оператором if .

try a, b := f() else err { ... }
// (modulo scope of err) same as
a, b, err := f()
if err != nil { ... }

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

a, b := try(f(), decorate)
// same as
a, b := try(g())
// where g is
func g() (whatever, error) {
  x, err := f()
  if err != nil {
    try(decorate(err))
  }
  return x, nil
}

Все три сокращены по шаблону и помогают ограничить объем ошибок.

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

Для оператора try - else это дает преимущество по сравнению с использованием if вместо try . Но преимущество настолько незначительно, что мне трудно представить, что оно себя оправдывает, хотя мне оно нравится.

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

Обработка всех ошибок одинаково может быть выполнена в defer . Если одна и та же обработка ошибок выполняется в каждом блоке else , это немного повторяется:

func printSum(a, b string) error {
  try x := strconv.Atoi(a) else err {
    return fmt.Errorf("printSum: %w", err)
  }
  try y := strconv.Atoi(b) else err {
    return fmt.Errorf("printSum: %w", err)
  }
  fmt.Println("result:", x + y)
  return nil
}

Я, конечно, знаю, что бывают случаи, когда определенная ошибка требует специальной обработки. Это те случаи, которые врезались в мою память. Но если это происходит только, скажем, в 1 из 100 раз, не лучше ли было бы сохранить простоту try и просто не использовать try в таких ситуациях? С другой стороны, если это больше похоже на 1 из 10 случаев, добавление else /handler кажется более разумным.

Было бы интересно увидеть фактическое распределение того, как часто try без обработчика else /handler против try с else /обработчиком было бы полезно, хотя собрать данные непросто.

Я хочу расширить недавний комментарий @jimmyfrasche .

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

    a, b, err := f()
    if err != nil {
        return nil, err
    }

Этот код легко читается. Расширение языка имеет смысл только в том случае, если мы сможем добиться значительного сокращения шаблонного кода. Когда я вижу что-то вроде

    try a, b := f() else err { return nil, err }

Я не могу не чувствовать, что мы не так уж много экономим. Мы экономим три строки, и это хорошо, но, по моим подсчетам, мы сокращаем с 56 до 46 символов. Это немного. Сравнить с

    a, b := try(f())

который сокращает с 56 до 18 символов, что является гораздо более значительным сокращением. И хотя оператор try делает потенциальное изменение потока управления более ясным, в целом я не нахожу его более читабельным. Хотя с положительной стороны оператор try упрощает аннотирование ошибки.

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

Как и другие, я хотел бы поблагодарить @crawshaw за примеры.

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

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

  1. обоснование этого, по-видимому, заключается в уменьшении кода шаблона обработки ошибок. ИМХО, это «расхламляет» код, но на самом деле не устраняет сложность; это просто скрывает это. Это не кажется достаточно сильной причиной. "идти" синтаксис прекрасно зафиксировал запуск параллельного потока. Я не чувствую здесь такого рода "ага!". Это кажется неправильным. Соотношение затрат и выгод недостаточно велико.

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

  3. с неявным возвратом при ошибке try кажется, что Go неохотно поддерживает обработку исключений. То есть, если A вызывает be в попытке защиты, а B вызывает C в попытке защиты, а C вызывает D в попытке защиты, если D возвращает ошибку, в результате вы вызвали нелокальный переход. Это кажется слишком «волшебным».

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

@ianlancetaylor
Если я правильно понимаю предложение «попробовать еще», кажется, что блок else является необязательным и зарезервирован для обработки, предоставляемой пользователем. В вашем примере try a, b := f() else err { return nil, err } предложение else на самом деле избыточно, и все выражение можно записать просто как try a, b := f()

Я согласен с @ianlancetaylor ,
Удобочитаемость и шаблонность — две основные проблемы и, возможно, движущая сила
обработка ошибок go 2.0 (хотя я могу добавить некоторые другие важные проблемы)

Также, что текущий

a, b, err := f()
if err != nil {
    return nil, err
}

Легко читается.
И так как я верю

if a, b, err := f(); err != nil {
    return nil, err
}

Почти так же удобочитаем, но имеет свои "проблемы", возможно,

ifErr a, b, err := f() {
    return nil, err
}

Это было бы только ; err != nil часть и не создаст область, или

по аналогии

попробовать a, b, ошибиться := f() {
вернуть ноль, ошибиться
}

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

Вт, 11 июня 2019, 20:19 Дмитрий Матреничев, уведомления@github.com
написал:

@ianlancetaylor https://github.com/ianlancetaylor
Если я правильно понимаю предложение "попробовать еще", кажется, что еще блок
является необязательным и зарезервирован для обработки пользователем. В вашем примере
try a, b := f() else err { return nil, err } на самом деле предложение else
избыточно, и все выражение можно записать просто как try a, b :=
е()


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=ABNEY4XPURMASWKZKOBPBVDPZ7NALA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXN3VDA#issuecomment3 ,
или заглушить тему
https://github.com/notifications/unsubscribe-auth/ABNEY4SAFK4M5NLABF3NZO3PZ7NALANCNFSM4HTGCZ7Q
.

@ianlancetaylor

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

Согласен, и, учитывая, что else будет всего лишь синтаксическим сахаром (со странным синтаксисом!), который, скорее всего, будет использоваться очень редко, меня это не волнует. Я бы все же предпочел, чтобы try было выражением.

@ianlancetaylor Вторя @DmitriyMV , блок else будет необязательным. Позвольте мне привести пример, который иллюстрирует и то, и другое (и не кажется слишком далеким от истины с точки зрения относительной доли обработанных и необработанных блоков try в реальном коде):

func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    headRef, err := r.Head()
    if err != nil {
        return err
    }

    parentObjOne, err := headRef.Peel(git.ObjectCommit)
    if err != nil {
        return err
    }

    parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit)
    if err != nil {
        return err
    }

    parentCommitOne, err := parentObjOne.AsCommit()
    if err != nil {
        return err
    }

    parentCommitTwo, err := parentObjTwo.AsCommit()
    if err != nil {
        return err
    }

    treeOid, err := index.WriteTree()
    if err != nil {
        return err
    }

    tree, err := r.LookupTree(treeOid)
    if err != nil {
        return err
    }

    remoteBranchName, err := remoteBranch.Name()
    if err != nil {
        return err
    }

    userName, userEmail, err := r.UserIdentityFromConfig()
    if err != nil {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    _, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    if err != nil {
        return err
    }
    return nil
}
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    try headRef := r.Head()
    try parentObjOne := headRef.Peel(git.ObjectCommit)
    try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    try parentCommitOne := parentObjOne.AsCommit()
    try parentCommitTwo := parentObjTwo.AsCommit()
    try treeOid := index.WriteTree()
    try tree := r.LookupTree(treeOid)
    try remoteBranchName := remoteBranch.Name()
    try userName, userEmail := r.UserIdentityFromConfig() else err {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    try r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    return nil
}

Хотя шаблон try / else не сохраняет много символов по сравнению с составным if , он делает:

  • унифицировать синтаксис обработки ошибок с необработанным try
  • с первого взгляда понятно, что условный блок обрабатывает состояние ошибки
  • дайте нам шанс уменьшить странность области видимости, от которой страдают составные if s

Тем не менее, необработанные try , вероятно, будут наиболее распространенными.

@ianlancetaylor

Попробуйте прочитать эти примеры, пока смотрите на try, точно так же, как вы уже смотрите на if err != nil { return err }.

Я не думаю, что это возможно/равноценно. Не заметить, что попытка существует в переполненной строке, или что она точно обертывает, или что в одной строке есть несколько экземпляров... Это не то же самое, что легко/быстро отметить точку возврата и не беспокоиться о ее специфике.

@ianlancetaylor

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

Мои глаза могут затуманиться на if err != nil { return err } , но в то же время это все равно зафиксируется — четко и мгновенно.

Что мне нравится в варианте try -statement, так это то, что он уменьшает шаблон, но таким образом, что его легко замаскировать, но трудно не заметить.

Это может означать дополнительную строку здесь или там, но это все равно меньше строк, чем статус-кво.

@brynbellomy

  1. Как вы предлагаете обрабатывать функции, которые возвращают несколько значений, например:
    func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) (хэш, ошибка) {
  2. Как бы вы отследили правильную строку, которая вернула ошибку
  3. отбрасывая проблему области действия (которая может быть решена другими способами), я не уверен
func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {

    if headRef, err := r.Head(); err != nil {
        return err
    } else if parentObjOne, err := headRef.Peel(git.ObjectCommit); err != nil {
        return err
    } else parentObjTwo, err := remoteBranch.Reference.Peel(git.ObjectCommit); err != nil {
        return err
    } ...

не так сильно отличается читабельностью (или fmt.Errorf("ошибка при получении заголовка: %s", err.Error() ) позволяет вам легко изменять и предоставлять дополнительные данные.

Что еще ворчит, так это

  1. необходимость перепроверки; ошибка != ноль
  2. возвращая ошибку как есть, если мы не хотим предоставлять дополнительную информацию, что в некоторых случаях не является хорошей практикой, потому что вы зависите от вызываемой вами функции, чтобы отразить «хорошую» ошибку, которая будет намекать на «что пошло не так». ", в случаях функций file.Open , close , Remove , Db и т. д. многие вызовы функций могут возвращать одну и ту же ошибку (мы можем спорить, означает ли это, что разработчик, написавший ошибку, хорошо поработал или нет... но это СЛУЧАЕТСЯ) - и тогда - у вас есть ошибка, вероятно, зарегистрируйте ее из функции, которая вызвала
    « createMergeCommit », но не могу отследить его до точной строки, в которой он произошел.

Извините, если кто-то уже опубликовал что-то подобное (есть много хороших идей: P). Как насчет этого альтернативного синтаксиса:

fail := func(err error) error {
  log.Print("unexpected error", err)
  return err
}

a, b, err := f1()          // normal
c, d := f2() -> throw      // calls throw(err)
e, f := f3() -> panic      // calls panic(err)
g, h := f4() -> t.Error    // calls t.Error(err)
i, j := f5() -> fail       // calls fail(err)

то есть у вас есть -> handler справа от вызова функции, которая вызывается, если возвращенная ошибка != nil. Обработчик — это любая функция, которая принимает ошибку как единственный аргумент и необязательно возвращает ошибку (например, func(error) или func(error) error ). Если обработчик возвращает нулевую ошибку, функция продолжается, в противном случае возвращается ошибка.

поэтому a := b() -> handler эквивалентно:

a, err := b()
if err != nil {
  if herr := handler(err); herr != nil {
    return herr
  }
}

Теперь в качестве ярлыка вы можете поддерживать встроенную функцию try (или ключевое слово, или оператор ?= , или что-то еще), что является сокращением от a := b() -> throw , поэтому вы можете написать что-то вроде:

func() error {
  a, b := try(f1())
  c, d := try(f2())
  e, f := try(f3())
  ...
  return nil
}() -> panic // or throw/fail/whatever

Лично я считаю, что оператор ?= легче читать, чем ключевое/встроенное слово try:

func() error {
  a, b ?= f1()
  c, d ?= f2()
  e, f ?= f3()
  ...
  return nil
}() -> panic

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

Я пока не комментировал предложения по обработке ошибок, потому что в целом поддерживаю их, и мне нравится, как они продвигаются. И функция try, определенная в предложении, и оператор try, предложенный @thepudds , кажутся разумными дополнениями к языку. Я уверен, что все, что придумает команда Go, будет хорошим.

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

Try определяется как функция, принимающая переменное количество аргументов.

func try(t1 T1, t2 T2, … tn Tn, te error) (T1, T2, … Tn)

Передача результата вызова функции в try , как в try(f()) , работает неявно из-за того, как в Go работает несколько возвращаемых значений.

Судя по моему прочтению предложения, следующие фрагменты действительны и семантически эквивалентны.

a, b = try(f())
//
u, v, err := f()
a, b = try(u, v, err)

Предложение также повышает возможность расширения try дополнительными аргументами.

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

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

var h handler
a, b = try(h, f())
// or
a, b = try(f(), h)

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

func f() (handler, error) { ... }
func g() (error) { ... }
try(f())
try(h, g())

Размещение его в конце, вероятно, сработает, но тогда попытка будет уникальной в языке, поскольку это единственная функция с параметром varargs в начале списка аргументов.

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

@волшебный

Наличие обработчика, возможно, мощно:
Я тебя уже объявил ч,

ты сможешь

var h handler
a, b, h = f()

или

a, b, h.err = f()

если это похоже на функцию:

h:= handler(err error){
 log(...)
 return ....
} 

Потом было предложение

a, b, h(err) = f()

Все могут вызывать обработчик
И вы также можете «выбрать» обработчик, который возвращает или только фиксирует ошибку (conitnue/break/return), как некоторые предлагали.

Таким образом, проблема вараргов исчезла.

Одна альтернатива предложению else @brynbellomy:

a, b := try f() else err { /* handle error */ }

может быть поддержка функции украшения сразу после else:

decorate := func(err error) error { return fmt.Errorf("foo failed: %v", err) }

try a, b := f() else decorate
try c, d := g() else decorate

И, возможно, также некоторые служебные функции что-то вроде:

decorate := fmt.DecorateErrorf("foo failed")

Функция оформления может иметь сигнатуру func(error) error и вызываться функцией try при наличии ошибки непосредственно перед возвратом функции try из связанной пробуемой функции.

По духу это будет похоже на одну из более ранних «итераций дизайна» из документа с предложением:

f := try(os.Open(filename), handler)              // handler will be called in error case

Если кому-то нужно что-то более сложное или блок операторов, он может вместо этого использовать if (так же, как и сегодня).

Тем не менее, есть что-то приятное в визуальном выравнивании try , показанном в примере @brynbellomy в https://github.com/golang/go/issues/32437#issuecomment -500949780.

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

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

Вот пример @brynbellomy , переписанный с помощью функции try , с использованием блока var , чтобы сохранить хорошее выравнивание, указанное @thepudds в https://github.com/golang/go/issues .

package main

import (
    "fmt"
    "time"
)

func createMergeCommit(r *repo.Repo, index *git.Index, remote *git.Remote, remoteBranch *git.Branch) error {
    var (
        headRef          = try(r.Head())
        parentObjOne     = try(headRef.Peel(git.ObjectCommit))
        parentObjTwo     = try(remoteBranch.Reference.Peel(git.ObjectCommit))
        parentCommitOne  = try(parentObjOne.AsCommit())
        parentCommitTwo  = try(parentObjTwo.AsCommit())
        treeOid          = try(index.WriteTree())
        tree             = try(r.LookupTree(treeOid))
        remoteBranchName = try(remoteBranch.Name())
    )

    userName, userEmail, err := r.UserIdentityFromConfig()
    if err != nil {
        userName = ""
        userEmail = ""
    }

    var (
        now       = time.Now()
        author    = &git.Signature{Name: userName, Email: userEmail, When: now}
        committer = &git.Signature{Name: userName, Email: userEmail, When: now}
        message   = fmt.Sprintf(`Merge branch '%v' of %v`, remoteBranchName, remote.Url())
        parents   = []*git.Commit{
            parentCommitOne,
            parentCommitTwo,
        }
    )

    _, err = r.CreateCommit(headRef.Name(), author, committer, message, tree, parents...)
    return err
}

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

Однако это поднимает вопрос о том, как try работает в блоке var . Я предполагаю, что каждая строка var считается отдельным оператором, а не целым блоком, являющимся одним оператором, насколько порядок того, что присваивается, когда.

Было бы хорошо, если бы предложение «попробовать» явно указывало на последствия для таких инструментов, как cmd/cover, которые аппроксимируют статистику покрытия тестами, используя наивный подсчет операторов. Я беспокоюсь, что невидимый поток контроля ошибок может привести к занижению счета.

@пуддс

попробуйте a, b := f() иначе украсьте

Возможно, это слишком глубокий ожог клеток моего мозга, но это слишком сильно бьет по мне.

try a, b := f() ;catch(decorate)

и скользкий путь к

a, b := f()
catch(decorate)

Я думаю, вы можете видеть, к чему это ведет, и для меня сравнение

    try headRef := r.Head()
    try parentObjOne := headRef.Peel(git.ObjectCommit)
    try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    try parentCommitOne := parentObjOne.AsCommit()
    try parentCommitTwo := parentObjTwo.AsCommit()
    try treeOid := index.WriteTree()
    try tree := r.LookupTree(treeOid)
    try remoteBranchName := remoteBranch.Name()

с участием

    try ( 
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
    remoteBranchName := remoteBranch.Name()
    )

(или даже подвох в конце)
Второй более читабелен, но подчеркивает тот факт, что функции ниже возвращают 2 vars, и мы волшебным образом отбрасываем одну, собирая ее в "magic return err" .

    try(err) ( 
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
    remoteBranchName := remoteBranch.Name()
    ); err!=nil {
      //handle the err
    }

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

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

if f() { return nil, err }

Пожалуйста, нет. Если нам нужна одна строка if , сделайте одну строку if , например:

if f() then return nil, err

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

Я хотел бы подчеркнуть пару вещей, которые, возможно, были забыты в пылу дискуссии:

1) Весь смысл этого предложения состоит в том, чтобы сделать обычную обработку ошибок отодвинутой на второй план — обработка ошибок не должна доминировать в коде. Но все же должно быть явным. Любое из альтернативных предложений, которые делают обработку ошибок еще более выпирающей, упускает суть. Как уже сказал @ianlancetaylor , если эти альтернативные предложения не уменьшат значительно количество шаблонов, мы можем просто остаться с операторами if . (И запрос на сокращение шаблонного кода исходит от вас, сообщества Go.)

2) Одна из претензий к текущему предложению — необходимость назвать результат ошибки, чтобы получить к нему доступ. Любое альтернативное предложение будет иметь ту же проблему, если альтернатива не вводит дополнительный синтаксис, т. е. больше шаблонов (таких как ... else err { ... } и т.п.) для явного наименования этой переменной. Но что интересно: если мы не заботимся об оформлении ошибки и не называем результирующие параметры, но по-прежнему требуем явного return , потому что есть своего рода явный обработчик, этот оператор return должен будет перечислить все (обычно нулевые) значения результата, поскольку в этом случае не допускается голый возврат. Особенно, если функция возвращает много ошибок без оформления ошибки, эти явные возвраты ( return nil, err и т. д.) добавляются к шаблону. Текущее предложение и любая альтернатива, не требующая явного указания return , отменяет это. С другой стороны, если кто-то действительно хочет приукрасить ошибку, текущее предложение _требует_ того, чтобы имя результата ошибки (а вместе с ним и все остальные результаты) получило доступ к значению ошибки. У этого есть приятный побочный эффект, заключающийся в том, что в явном обработчике можно использовать голый возврат и не нужно повторять все остальные значения результата. (Я знаю, что есть некоторые сильные чувства по поводу голых возвратов, но реальность такова, что, когда все, о чем мы заботимся, - это результат ошибки, это настоящая неприятность, чтобы перечислить все остальные (обычно нулевые) значения результата - это ничего не добавляет к понимание кода). Другими словами, необходимость называть результат ошибки так, чтобы его можно было декорировать, позволяет еще больше сократить шаблон.

@magical Спасибо, что указали на это . Я заметил то же самое вскоре после публикации предложения (но не поднял его, чтобы не вызывать путаницы). Вы правы, что try не может быть расширен. К счастью, исправить это достаточно просто. (Как оказалось, в наших более ранних внутренних предложениях не было этой проблемы — она появилась, когда я переписал нашу окончательную версию для публикации и попытался упростить try , чтобы более точно соответствовать существующим правилам передачи параметров. - но, как оказалось, ущербно и в основном бесполезно - благо можно написать try(a, b, c, handle) .)

Более ранняя версия try определяла его примерно следующим образом: try(expr, handler) принимает одно (или, возможно, два) выражения в качестве аргументов, где первое выражение может быть многозначным (может случиться, только если выражение вызов функции). Последнее значение этого (возможно, многозначного) выражения должно иметь тип error , и это значение проверяется на нулевое значение. (и т.д. - остальное вы можете себе представить).

В любом случае, дело в том, что try синтаксически принимает только одно или, возможно, два выражения. (Но немного сложнее описать семантику try .) Следствием будет такой код, как:

a, b := try(u, v, err)

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

a, b := u, v  // we don't care if the assignment happens in case of an error
try(err)

(или используйте оператор if по мере необходимости). Но опять же, это кажется неважным.

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

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

@griesemer Спасибо за объяснение. Вот к такому выводу я тоже пришел.

Краткий комментарий к try как к утверждению: как я думаю, можно увидеть в примере в https://github.com/golang/go/issues/32437#issuecomment -501035322, try хоронит лед. Код становится серией операторов try , что скрывает то, что на самом деле делает код.

Существующий код может повторно использовать вновь объявленную переменную ошибки после блока if err != nil . Скрытие переменной нарушит это, и добавление именованной возвращаемой переменной в сигнатуру функции не всегда исправит это.

Может быть, лучше оставить объявление/назначение ошибок как есть и найти однострочный код обработки ошибок stmt.

err := f() // followed by one of

on err, return err            // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err)      // doesn't stop the function
on err, hname err             // handler invocation without parens
on err, ignore err            // optional ignore handler logs error if defined

if err, return err            // alternatively, use if

handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
   if err == io.Bad { return err }
   fmt.Println(clr.name, err)
}

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

f(try g()) // panic on error
f(try_ g()) // ignore any error

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

Мне нравится идея комментариев, в которых try как утверждение. Это явно, все еще легко замазать (поскольку он имеет фиксированную длину), но не настолько легко замазать (поскольку он всегда в одном и том же месте), чтобы их можно было спрятать в переполненной строке. Его также можно комбинировать с defer fmt.HandleErrorf(...) , как отмечалось ранее, однако у него есть ловушка злоупотребления именованными параметрами для переноса ошибок (что все еще кажется мне умным хаком. умные хаки - это плохо).

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

Попробуйте как выражение

// Too hidden, it's in a crowded function with many symbols that complicate the function.

f, err := os.OpenFile(try(FileName()), os.O_APPEND|os.O_WRONLY, 0600)

// Not easy enough, the word "try" already increases horizontal complexity, and it
// being an expression only encourages more horizontal complexity.
// If this code weren't collapsed to multiple lines, it would be extremely
// hard to read and unclear as to what's executing when.

fullContents := try(io.CopyN(
    os.Stdout,
    try(net.Dial("tcp", "localhost:"+try(buf.ReadString("\n"))),
    try(strconv.Atoi(try(buf.ReadString("\n")))),
))

Попробуйте как заявление

// easy to see while still not being too verbose

try name := FileName()
os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)

// does not allow for clever one-liners, code remains much more readable.
// also, the code reads in sequence from top-to-bottom.

try port := r.ReadString("\n")
try lengthStr := r.ReadString("\n")
try length := strconv.Atoi(lengthStr)

try con := net.Dial("tcp", "localhost:"+port)
try io.CopyN(os.Stdout, con, length)

Выводы

Этот код определенно придуман, я признаю. Но я имею в виду, что, как правило, try как выражение плохо работает в:

  1. Середина переполненных выражений, которые не нуждаются в большой проверке ошибок
  2. Относительно простые многострочные операторы, требующие большого количества проверок ошибок.

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

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

fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))

Он считывает порт из *bufio.Reader , запускает TCP-соединение и копирует количество байтов, указанное тем же *bufio.Reader , в stdout . Все с обработкой ошибок. Я не думаю, что для языка с такими строгими соглашениями о кодировании это вообще должно быть разрешено. Я думаю, что gofmt может помочь с этим.

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

На Go можно написать отвратительный код. Можно даже страшно отформатировать; есть только сильные нормы и инструменты против этого. В Go даже есть goto .

Во время проверки кода я иногда прошу людей разбить сложные выражения на несколько операторов с полезными промежуточными именами. Я бы сделал что-то подобное для глубоко вложенных try s по той же причине.

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

На Go можно написать отвратительный код. Можно даже страшно отформатировать; есть только сильные нормы и инструменты против этого. Go даже имеет goto.

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

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

Это хороший момент. Мы не должны запрещать хорошую идею только потому, что она может быть использована для создания плохого кода. Однако я думаю, что если у нас есть альтернатива, которая продвигает лучший код, это может быть хорошей идеей. Я действительно не видел много разговоров _против_ сырой идеи try в качестве утверждения (без всего мусора else { ... } ) до комментария @ianlancetaylor , однако я, возможно, просто пропустил это.

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

При этом я ужасно критически отношусь к этой идее, хотя на самом деле она не ужасна.

Оператор Try as значительно уменьшает шаблон, и даже больше, чем try как выражение, если мы позволим ему работать с блоком выражений, как было предложено ранее, даже без использования блока else или обработчика ошибок. Используя это, пример deandeveloper становится:

try (
    name := FileName()
    file := os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0600)
    port := r.ReadString("\n")
    lengthStr := r.ReadString("\n")
    length := strconv.Atoi(lengthStr)
    con := net.Dial("tcp", "localhost:"+port)
    io.CopyN(os.Stdout, con, length)
)

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

@beoran Тогда зачем вообще пытаться? Просто разрешите присваивание, в котором отсутствует последнее значение ошибки, и заставьте его вести себя так, как если бы это был оператор попытки (или вызов функции). Не то, чтобы я это предлагал, но это еще больше уменьшило бы количество шаблонов.

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

@deanveloper

fullContents := try(io.CopyN(os.Stdout, try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))), try(strconv.Atoi(try(r.ReadString("\n"))))))

Я должен признать, что это не читабельно для меня, я бы, вероятно, почувствовал, что должен:

fullContents := try(io.CopyN(os.Stdout, 
                               try(net.Dial("tcp", "localhost:"+try(r.ReadString("\n"))),
                                     try(strconv.Atoi(try(r.ReadString("\n"))))))

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

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

Что касается отступов, то для этого и существует go fmt, так что лично я не считаю это большой проблемой.

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

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

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

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

func foo() error {
    defer handleError(&err, etc)
    try(something())
}

Это будет функционально идентично:

func foo() (err error) {
    defer handleError(&err, etc)
    try(something())
}

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

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

func foo() error {
    f := func() error {
        defer handleError(&err, etc)
        try(something())
        return nil
    }
    return f()
}

но вы всегда можете написать это вместо этого:

func foo() error {
    f := func() (err error) {
        defer handleError(&err, etc)
        try(something())
        return nil
    }
    return f()
}

Что касается try как предопределенного идентификатора, а не оператора,
Я обнаружил, что отдаю предпочтение последнему после того, как неоднократно неправильно расставлял скобки при написании:

try(try(os.Create(filename)).Write(data))

В разделе «Почему мы не можем использовать? Как Rust» в FAQ говорится:

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

Я не совсем уверен, что это правда. Оператор .() необычен, пока вы не знаете Go, как и операторы канала. Если бы мы добавили оператор ? , я считаю, что вскоре он стал бы достаточно повсеместным и не стал бы серьезным барьером.

Однако оператор Rust ? добавляется после закрывающей скобки вызова функции, и это означает, что его легко пропустить, когда список аргументов длинный.

Как насчет добавления ?() в качестве оператора вызова:

Итак, вместо:

x := try(foo(a, b))

вы бы сделали:

x := foo?(a, b)

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

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

@ianlancetaylor Я полностью согласен с тем, что мы получим десятки строк с префиксом try . Тем не менее, я не вижу, чем это хуже, чем десятки строк, заканчивающихся условным выражением из двух-четырех строк, явно указывающим одно и то же выражение return . На самом деле, try (с предложениями else ) немного облегчает обнаружение, когда обработчик ошибок делает что-то особенное/не по умолчанию. Кроме того, по касательной, относительно условных выражений if , я думаю, что они хоронят лед больше, чем предлагаемый try -как-оператор: вызов функции живет в той же строке, что и условный , само условное выражение оказывается в самом конце уже переполненной строки, а присваивание переменных ограничивается блоком (что требует другого синтаксиса, если вам нужны эти переменные после блока).

@josharian У меня недавно была эта мысль. Go стремится к прагматизму, а не к совершенству, и его развитие часто кажется управляемым данными, а не принципами. Вы можете написать ужасный Go, но обычно это сложнее, чем написать приличный Go (который достаточно хорош для большинства людей). Также стоит отметить — у нас есть много инструментов для борьбы с плохим кодом: не только gofmt и go vet , но и наши коллеги, и культура, которую это сообщество создало (очень тщательно) для руководства. Я бы не хотел избегать улучшений, которые помогают в общем случае, просто потому, что кто-то где-то может застрелиться.

@beoran Это элегантно, и если подумать, то на самом деле семантически отличается от блоков try в других языках, поскольку имеет только один возможный результат: возврат из функции с необработанной ошибкой. Однако: 1) это, вероятно, сбивает с толку новых программистов Go, которые работали с этими другими языками (честно говоря, это не самая большая моя проблема; я доверяю интеллекту программистов), и 2) это приведет к тому, что огромное количество кода будет иметь отступ во многих кодовые базы. Что касается моего кода, то по этой причине я даже стараюсь избегать существующих блоков type / const / var . Кроме того, единственными ключевыми словами, которые в настоящее время допускают такие блоки, являются определения, а не операторы управления.

@yijus Я не согласен с удалением ключевого слова, поскольку ясность (на мой взгляд) является одним из достоинств Go. Но я согласен с тем, что делать отступы для огромных объемов кода, чтобы воспользоваться преимуществами выражений try , — плохая идея. Так что, может быть, вообще нет блоков try ?

@rogpeppe Я думаю, что такой тонкий оператор разумен только для вызовов, которые никогда не должны возвращать ошибку, и поэтому паникуют, если они это делают. Или звонки, где вы всегда игнорируете ошибку. Но оба кажутся редкими. Если вы открыты для нового оператора, см. #32500.

Я предложил, чтобы f(try g()) паниковал в https://github.com/golang/go/issues/32437#issuecomment -501074836 вместе с однострочным обработчиком stmt:
on err, return ...

Я думаю, что необязательный else в try ... else { ... } сдвинет код слишком далеко вправо, возможно, скрыв его. Я ожидаю, что блок ошибок должен занимать не менее 25 символов большую часть времени. Кроме того, до сих пор блоки не сохранялись в одной строке на go fmt , и я ожидаю, что это поведение сохранится для try else . Таким образом, мы должны обсуждать и сравнивать примеры, в которых блок else находится на отдельной строке. Но даже тогда я не уверен в удобочитаемости else { в конце строки.

@yiyus https://github.com/golang/go/issues/32437#issuecomment -501139662

@beoran Тогда зачем вообще пытаться? Просто разрешите присваивание, в котором отсутствует последнее значение ошибки, и заставьте его вести себя так, как если бы это был оператор попытки (или вызов функции). Не то, чтобы я это предлагал, но это еще больше уменьшило бы количество шаблонов.

Это невозможно сделать, потому что Go1 уже позволяет вызывать func foo() error просто как foo() . Добавление , error к возвращаемым значениям вызывающего объекта изменит поведение существующего кода внутри этой функции. См. https://github.com/golang/go/issues/32437#issuecomment-500289410

@rogpeppe В вашем комментарии о правильном расстановке скобок с вложенными try : Есть ли у вас какие-либо мнения о приоритете try ? См. также детальный дизайн-документ на эту тему .

@griesemer Я действительно не очень люблю try в качестве оператора унарного префикса по указанным там причинам. Мне пришло в голову, что альтернативным подходом было бы разрешить try в качестве псевдометода для возвращаемого кортежа функции:

 f := os.Open(path).try()

Я думаю, что это решает проблему приоритета, но на самом деле это не очень похоже на Go.

@рогпеппе

Очень интересно! . Вы действительно можете быть на что-то здесь.

А как насчет того, чтобы расширить эту идею вот так?

for _,fp := range filepaths {
    f := os.Open(path).try(func(err error)bool{
        fmt.Printf( "Cannot open file %s\n", fp );
        continue;
    });
}

Кстати, я мог бы предпочесть другое имя вместо try() , например, guard() , но я не должен отказываться от имени до того, как архитектура будет обсуждаться другими.

против :

for _,fp := range filepaths {
    if f,err := os.Open(path);err!=nil{
        fmt.Printf( "Cannot open file %s\n", fp )
    }
}

?

Мне нравится try a,b := foo() вместо if err!=nil {return err} , потому что он заменяет шаблон для действительно простого случая. Но для всего остального, добавляющего контекст, действительно ли нам нужно что-то еще, кроме if err!=nil {...} (найти лучше будет очень сложно)?

Если для оформления/обтекания обычно требуется дополнительная строка, давайте просто «выделим» для нее строку.

f, err := os.Open(path)  // normal Go \o/
on err, return fmt.Errorf("Cannot open %s, due to %v", path, err)

@networkimprov Думаю, мне бы это тоже понравилось. Подталкивая более аллитеративный и описательный термин, который я уже привел...

f, err := os.Open(path)
relay err { nil, fmt.Errorf("Cannot open %s, due to %v", path, err) }

// marginally shorter, doesn't trigger vertical formatting unless excessively wide
// enclosed expression restricted to a list of values that match the return args

или

f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)

// somewhere between statement and func, prob more pleasing to type w/out completion
// trailing expression restricted to a list of values that match the return args
// maybe excessive width triggers linting noise - with a reformatter available
// providing a reformatter would make swapping old (narrow enough) code easy

@daved рад, что тебе понравилось! on err, ... разрешит любой обработчик с одним stmt:

err := f() // followed by one of

on err, return err            // any type can be tested for non-zero
on err, return fmt.Errorf(...)
on err, fmt.Println(err)      // doesn't stop the function
on err, continue              // retry in a loop
on err, hname err             // named handler invocation without parens
on err, ignore err            // logs error if handle ignore() defined

handle hname(err error, clr caller) { // type caller has results of runtime.Caller()
   if err == io.Bad { return err } // non-local return
   fmt.Println(clr, err)
}

РЕДАКТИРОВАТЬ: on заимствует из Javascript. Я не хотел перегружать if .
Запятая не обязательна, но я не люблю там точку с запятой. Может двоеточие?

Я не совсем понимаю relay ; значит возврат по ошибке?

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

* Я не хотел бы перегружать , в этом случае, и я не поклонник термина on , но мне нравится предпосылка и общий вид структуры кода.

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

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

@networkimprov @daved Мне не нравятся эти две идеи, но они не кажутся достаточным улучшением по сравнению с простым разрешением однострочных операторов if err != nil { ... } , оправдывающих изменение языка. Кроме того, делает ли это что-нибудь, чтобы уменьшить повторяющийся шаблон в случае, когда вы просто возвращаете ошибку? Или идея в том, что вам всегда нужно выписывать return ?

@brynbellomy В моем примере нет return . relay — это защитное реле, определяемое как «если эта ошибка не равна нулю, будет возвращено следующее».

Используя мой второй пример из предыдущего:

f, err := os.Open(path)
relay(err) nil, fmt.Errorf("Cannot open %s, due to %v", path, err)

Также может быть что-то вроде:

f, err := os.Open(path)
relay(err)

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

wrap := func(err error, msg string) error {
    if err != nil {
        fmt.Errorf("%s: %s", msg, err)
    }
    return nil
}

// ...

f, err := os.Open(path)
relay(err, wrap(err, fmt.Sprintf("Cannot open %s", path)))

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

Должен ли _go fmt_ разрешать однострочные if , но не case, for, else, var () ? Мне все, пожалуйста ;-)

Команда Go отклонила множество запросов на однострочные проверки ошибок.

Операторы on err, return err могут повторяться, но они явные, краткие и ясные.

@magical Ваш отзыв был учтён в обновленной версии подробного предложения .

Мелочь, но если try является ключевым словом, оно может быть распознано как закрывающий оператор, поэтому вместо

func f() error {
  try(g())
  return nil
}

ты можешь просто сделать

func f() error {
  try g()
}

( try -оператор получает это бесплатно, try -оператор потребует специальной обработки, я понимаю, что приведенный выше пример не очень хороший: но он минимален)

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

Все действительные точки. Я предполагаю, что это можно надежно рассматривать как завершающее выражение только в том случае, если это самая последняя строка функции, которая возвращает только ошибку, например, CopyFile в подробном предложении, или она используется как try(err) в if , где известно, что err != nil . Кажется, оно того не стоит.

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

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

Я думаю, нам нужны какие-то объективные критерии, оценивающие наши «пробные» варианты и альтернативные предложения.

  • Уменьшает ли это шаблон?
  • Читабельность
  • В язык добавлена ​​сложность
  • Стандартизация ошибок
  • Иди-иш
    ...
    ...
  • усилия по реализации и риски
    ...

Мы, конечно, также можем установить некоторые основные правила для запретов (обратная совместимость не будет) и оставить серую область для «выглядит ли это привлекательно / интуитивно понятно и т. д.» («жесткие» критерии выше также могут быть спорными). .).

Если мы протестируем любое предложение по этому списку и оценим каждый балл (шаблон 5 баллов, удобочитаемость 4 балла и т. д.), то вместо этого, я думаю, мы можем согласовать:
Наши варианты, вероятно, A, B и C, кроме того, кто-то, желающий добавить новое предложение, может проверить (до некоторой степени), соответствует ли его предложение критериям.

Если это имеет смысл, поднимите палец вверх , мы можем попытаться рассмотреть исходное предложение.
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

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

Критерии += повторное использование кода обработки ошибок в пакете и внутри функции

Спасибо всем за постоянные отзывы об этом предложении.

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

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

Давайте снова сосредоточим обсуждение и вернемся в нужное русло.

Обратная связь наиболее продуктивна, если она помогает определить технические _факты_, которые мы упустили, например, "это предложение не работает должным образом в данном случае" или "оно будет иметь последствия, которых мы не осознавали".

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

@crawshaw потратил время, чтобы проанализировать пару сотен вариантов использования из библиотеки std и показал, что try редко оказывается внутри другого выражения, тем самым прямо опровергнув опасения, что try может стать скрытым и невидимым. Это очень полезная обратная связь, основанная на фактах, в данном случае подтверждающая дизайн.

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

Относительно выдвижения альтернативных предложений: Текущее предложение является плодом большой работы, начиная с прошлогоднего эскизного проекта . Мы повторяли этот дизайн несколько раз и запрашивали отзывы от многих людей, прежде чем мы чувствовали себя достаточно комфортно, чтобы опубликовать его и рекомендовать перейти к фактической фазе эксперимента, но мы еще не провели эксперимент. Имеет смысл вернуться к чертежной доске, если эксперимент провалится или если отзывы заранее говорят нам, что он явно провалится. Если мы переделываем дизайн на лету, основываясь на первых впечатлениях, мы просто тратим время всех и, что еще хуже, ничего не узнаем в процессе.

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

Спасибо.

(Я отмечаю, что несколько человек, выступающих за альтернативные предложения, начали свои собственные отдельные выпуски. Это правильно и помогает сосредоточить внимание на соответствующих вопросах. Спасибо.)

@griesemer
Я полностью согласен с тем, что нам следует сосредоточиться, и это именно то, что заставило меня написать:

Если это имеет смысл, поднимите палец вверх , мы можем попытаться рассмотреть исходное предложение.
https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md

Два вопроса:

  1. Согласны ли вы, если мы отметим положительные стороны (уменьшение стандартного шаблона, удобочитаемость и т. д.) по сравнению с недостатками (отсутствие явного оформления ошибок / более низкая возможность отслеживания источника строки ошибки), мы можем фактически заявить: это предложение в высшей степени направлено на решение a, b, в некоторой степени помощь c, не направлена ​​на решение d,e
    И тем самым избавьтесь от всего беспорядка «но это не так», «как это может быть» и сосредоточьтесь на технических проблемах, таких как @magical.
    А также не одобряйте комментарии типа «но решение XXX решает d, e лучше.
  2. многие встроенные сообщения представляют собой «предложения по незначительным изменениям в предложении» — я знаю, что это тонкая грань, но я думаю, что имеет смысл их сохранить.

ЛМКВИТ.

Использует ли try() с нулевыми аргументами (или другую встроенную функцию) все еще для рассмотрения или это было исключено.

После изменений в предложении меня все еще беспокоит, как это делает использование именованных возвращаемых значений более «общим». Однако у меня нет данных, подтверждающих это :upside_down_face :.
Если к предложению добавляется try() с нулевыми аргументами (или другая встроенная функция), можно ли обновить примеры в предложении, чтобы использовать try() (или другую встроенную функцию), чтобы избежать именованных возвратов?

@guybrand Проголосовать за и проголосовать против — это прекрасно, чтобы выразить _настроение_, но это все. Больше информации там нет. Мы не собираемся принимать решение на основании подсчета голосов, т.е. одних только настроений. Конечно, если все — скажем, 90%+ — ненавидят предложение, это, вероятно, плохой знак, и нам следует дважды подумать, прежде чем двигаться дальше. Но, похоже, здесь это не так. Большое количество людей, похоже, довольны тем, что попробовали что-то, и перешли к другим вещам (и не утруждают себя комментариями в этой теме).

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

@Goodwine Никто не исключил try() , чтобы получить значение ошибки; хотя _if_ что-то подобное необходимо, может быть лучше иметь предварительно объявленную переменную err , как предложил @rogpeppe (я думаю).

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

@griesemer
Я думаю, вы совершенно неправильно меня поняли.
Я не рассматриваю возможность голосования за/против этого или любого другого предложения, я просто искал способ получить хорошее представление о том, «думаем ли мы, что имеет смысл принимать решение, основанное на жестких критериях, а не на «мне нравится х». ' или 'ты не выглядишь красиво'"

Из того, что вы написали - это ИМЕННО то, что вы думаете ... поэтому, пожалуйста, проголосуйте за мой комментарий, как будто он говорит:

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

@guybrand они явно убеждены, что стоит создать прототип в предварительной версии 1.14 (?) И собрать отзывы от практических пользователей. ИОВ принято решение.

Также подан #32611 для обсуждения on err, <statement>

@guybrand Мои извинения. Да, я согласен, нам нужно смотреть на различные свойства предложения, такие как сокращение шаблона, решает ли оно стоящую проблему и т. д. Но предложение — это больше, чем сумма его частей — в конце концов мы нужно смотреть на общую картину. Это инженерия, а инженерия грязная: есть много факторов, влияющих на дизайн, и даже если объективно (на основе жестких критериев) часть проекта неудовлетворительна, в целом он все равно может быть «правильным». Так что я не колеблясь поддержу решение, основанное на какой-то _независимой_ оценке отдельных аспектов предложения.

(Надеюсь, это лучше описывает то, что вы имели в виду.)

Но что касается соответствующих критериев, я считаю, что это предложение ясно дает понять, что оно пытается решить. То есть список, на который вы ссылаетесь, уже существует:

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

Так уж получилось, что для оформления ошибок мы предлагаем использовать defer и именованные параметры результата (или старую инструкцию if ), потому что для этого не требуется изменение языка — а это фантастическая вещь. потому что языковые изменения имеют огромные скрытые затраты. Мы понимаем, что многие комментаторы считают эту часть дизайна «полностью отстойной». Тем не менее, на данный момент, в общей картине, со всем, что мы знаем, мы думаем, что это может быть достаточно хорошо. С другой стороны, нам нужно изменение языка — точнее, языковая поддержка — чтобы избавиться от шаблона, и try — это минимальное изменение, которое мы могли бы придумать. И ясно, что все по-прежнему явно в коде.

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

Это предложение, по сути, сводится к встроенному «макросу» для очень распространенного, но специфического случая шаблонного кода, очень похожего на встроенную функцию append() . Так что, хотя это полезно для конкретного конкретного случая использования id err!=nil { return err } , это также все, что он делает. Поскольку это не очень полезно в других случаях и не применимо в целом, я бы сказал, что это не впечатляет. У меня такое ощущение, что большинство программистов на Go ожидали большего, поэтому обсуждение в этой ветке продолжается.

Это контринтуитивно как функция. Потому что в Go невозможно иметь функцию с таким порядком аргументов func(... interface{}, error) .
Сначала набрано, а затем переменное количество шаблонов есть везде в модулях Go.

Чем больше я думаю, тем больше мне нравится текущее предложение, как есть.

Если нам нужна обработка ошибок, у нас всегда есть оператор if.

Всем привет. Спасибо за спокойное, уважительное и конструктивное обсуждение. Я потратил некоторое время на заметки и в конце концов настолько разочаровался, что создал программу, помогающую мне поддерживать другое представление этой ветки комментариев, которое должно быть более удобным и полным, чем то, что показывает GitHub. (Он также загружается быстрее!) См . https://swtch.com/try.html. Я буду обновлять его, но партиями, а не ежеминутно. (Это обсуждение требует тщательного обдумывания, и ему не помогает «интернет-время».)

У меня есть кое-что, что можно добавить, но это, вероятно, придется подождать до понедельника. Спасибо еще раз.

@mishak87 Мы обращаемся к этому в подробном предложении . Обратите внимание, что у нас есть другие встроенные модули ( try , make , unsafe.Offsetof и т. д.), которые являются «неправильными» — для этого и нужны встроенные модули.

@rsc , очень полезно! Если вы все еще пересматриваете его, может быть, свяжите ссылки на #id issue refs? А шрифт без засечек?

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

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

user := try(getUser(userID))

к

user, err := getUser(userID)
if err != nil {  
    // inspect error here
    return err
}

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

Переписывание нескольких вложенных вызовов try() в одной и той же функции было бы еще более раздражающим.

С другой стороны, добавление кода контекста или проверки в

user := try getUser(userID)

было бы так же просто, как добавить оператор catch в конце, за которым следует код

user := try getUser(userID) catch {
   // inspect error here
}

Удалить или временно отключить обработчик будет так же просто, как разорвать строку перед catch и закомментировать ее.

Переключение между try() и if err != nil кажется намного более раздражающим, IMO.

Это также относится к добавлению или удалению контекста ошибки. Можно написать try func() во время прототипирования чего-то очень быстро, а затем добавить контекст к конкретным ошибкам по мере необходимости по мере развития программы, в отличие от try() в качестве встроенного, где нужно было бы переписать код. строки, чтобы добавить контекст или добавить дополнительный код проверки во время отладки.

Я уверен, что функция try() была бы полезна, но поскольку я представляю себе ее использование в своей повседневной работе, я не могу не представить, насколько try ... catch была бы намного полезнее и менее раздражающей, когда я d необходимо добавить/удалить дополнительный код для некоторых ошибок.


Кроме того, я чувствую, что добавление try() , а затем рекомендация использовать if err != nil для добавления контекста очень похоже на make() против new() против := против var . Эти функции полезны в различных сценариях, но было бы неплохо, если бы у нас было меньше способов или хотя бы один способ инициализации переменных? Конечно, никто никого не заставляет использовать try, и люди могут продолжать использовать if err != nil, но я чувствую, что это разделит обработку ошибок в Go, как и несколько способов назначения новых переменных. Я думаю, что любой метод, добавленный в язык, также должен предоставлять возможность легко добавлять/удалять обработчики ошибок вместо того, чтобы заставлять людей переписывать целые строки для добавления/удаления обработчиков. Мне это не кажется хорошим результатом.

Еще раз извините за шум, но хотел указать на это на случай, если кто-то захочет написать отдельное подробное предложение по идее try ... else .

//cc @brynbellomy

Спасибо, @owais , за то, что снова подняли этот вопрос - это справедливо (и проблема отладки действительно упоминалась ранее ). try оставляет дверь открытой для расширений, таких как 2-й аргумент, который может быть функцией-обработчиком. Но это правда, что функция try не облегчает отладку — возможно, придется немного переписать код, чем функция trycatch или try - else .

@owais

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

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

Учитывая обсуждение до сих пор — особенно ответы команды Go — у меня сложилось сильное впечатление, что команда планирует продвинуться вперед с предложением, которое находится на столе. Если да, то комментарий и просьба:

  1. Предложение IMO «как есть» приведет к незначительному снижению качества кода в общедоступных репозиториях. Я ожидаю, что многие разработчики пойдут по пути наименьшего сопротивления, будут эффективно использовать методы обработки исключений и предпочтут использовать try() вместо обработки ошибок в момент их возникновения. Но, учитывая преобладающее мнение в этой ветке, я понимаю, что любое выставление напоказ сейчас будет просто ведением проигранной битвы, поэтому я просто регистрирую свое возражение для потомков.

  2. Предполагая, что команда продвигается вперед с предложением в том виде, в котором оно написано в настоящее время, не могли бы вы добавить переключатель компилятора, который отключит try() для тех, кто не хочет код, который игнорирует ошибки таким образом, и запретить программистам, которых они нанимают от использования? _(через CI, конечно.)_ Заранее благодарю вас за это внимание.

не могли бы вы добавить переключатель компилятора, который отключит функцию try()?

Это должно быть в инструменте линтинга, а не в компиляторе IMO, но я согласен

Это должно быть в инструменте линтинга, а не в компиляторе IMO, но я согласен

Я явно запрашиваю параметр компилятора, а не инструмент проверки, потому что запрещаю компилировать такой параметр. В противном случае будет слишком легко _"забыть"_ lint во время локальной разработки.

@mikeschinkel Не было бы так же легко забыть включить параметр компилятора в этой ситуации?

Флаги компилятора не должны изменять спецификацию языка. Это гораздо больше подходит для ветеринара / ворса

Не было бы так же легко забыть включить опцию компилятора в этой ситуации?

Не при использовании таких инструментов, как GoLand, где нет возможности принудительно запустить lint перед компиляцией.

Флаги компилятора не должны изменять спецификацию языка.

-nolocalimports изменяет спецификацию, а -s предупреждает.

Флаги компилятора не должны изменять спецификацию языка.

-nolocalimports изменяет спецификацию, а -s предупреждает.

Нет, спецификация не меняется. Мало того, что грамматика языка продолжает оставаться неизменной, в спецификации конкретно указано:

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

Не при использовании таких инструментов, как GoLand, где нет возможности принудительно запустить lint перед компиляцией.

https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint

@deanveloper

https://github.com/vmware/dispatch/wiki/Configure-GoLand-with-golint

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

Ворс всегда не настраивается и не может (AFAIK) быть настроен в качестве предварительного условия для запуска компилятора:

image

Нет, спецификация не меняется. Мало того, что грамматика языка продолжает оставаться неизменной, в спецификации конкретно указано:

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

Я прошу добавить параметр компилятора, запрещающий компиляцию кода с try() . Это не запрос на изменение спецификации языка, это просто запрос на остановку компилятора в этом особом случае.

И если это поможет, спецификацию языка можно обновить, чтобы она говорила что-то вроде:

Интерпретация try() зависит от реализации, но обычно она вызывает возврат, когда последний параметр является ошибкой, однако ее можно реализовать так, чтобы она не была разрешена.

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

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

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

func writeStuff(filename string) (io.ReadCloser, error) {
    f := try(os.Open(filename))
    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Можно переписать что-то вроде этого для операторов отладки или общей обработки или ошибки перед возвратом.

func writeStuff(filename string) (io.ReadCloser, error) {
    emit err {
        fmt.Printf("something happened [%v]\n", err.Error())
        return nil, err
    }

    f := try(os.Open(filename))
    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Итак, здесь я в конечном итоге предложил новое ключевое слово emit , которое могло бы быть оператором или одним вкладышем для немедленного возврата, как первоначальная функциональность try() :

emit return nil, err

То, чем будет эмиссия, по сути, является просто предложением, в которое вы можете поместить любую логику, которую пожелаете, если try() сработает из-за ошибки, не равной нулю. Еще одна возможность ключевого слова emit заключается в том, что вы можете получить доступ к ошибке прямо здесь, если вы добавите сразу после ключевого слова имя переменной, как я сделал в первом примере с его использованием.

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

Это ответ @mikeschinkel , я помещаю свой ответ в блок подробностей, чтобы не слишком загромождать обсуждение. В любом случае, @networkimprov прав в том, что это обсуждение должно быть вынесено на обсуждение до тех пор, пока это предложение не будет реализовано (если оно будет реализовано).

подробности о флаге для отключения попытки
@mikeschinkel

Ворс всегда не настраивается и не может (AFAIK) быть настроен в качестве предварительного условия для запуска компилятора:

Переустановил GoLand только для проверки. Кажется, это работает просто отлично, единственная разница в том, что если lint находит что-то, что ему не нравится, компиляция не завершается ошибкой. Однако это можно легко исправить с помощью пользовательского сценария, который запускает golint и завершается с ошибкой с ненулевым кодом выхода, если есть какие-либо выходные данные.
image

(Редактировать: я исправил ошибку, о которой он пытался сообщить мне внизу. Он работал нормально, даже когда присутствовала ошибка, но изменение «Run Kind» на каталог устранило ошибку, и она работала нормально)

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

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

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

Я прошу добавить параметр компилятора, запрещающий компиляцию кода с помощью try(). Это не запрос на изменение спецификации языка, это просто запрос на остановку компилятора в этом особом случае.

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

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

И если это поможет, спецификацию языка можно обновить, чтобы она говорила что-то вроде [...]

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

@deanveloper

_"В любом случае, @networkimprov прав в том, что это обсуждение должно быть отложено до тех пор, пока это предложение не будет реализовано (если оно будет реализовано)."_

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

Если у вас есть выбор, я тоже выберу ответ, также в подробном блоке

здесь:

_ "Однако это можно легко исправить с помощью пользовательского скрипта, который запускает golint и завершается с ошибкой с ненулевым кодом выхода, если есть какие-либо выходные данные."_

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

Поэтому я явно просил здесь простое решение, а не самостоятельное решение.

_"вы отключили бы попытку для каждой из библиотек, которые вы используете."_

И это _явная_ причина, по которой я его просил. Потому что я хочу гарантировать, что весь код, использующий эту проблемную _"функцию"_, не попадет в исполняемые файлы, которые мы распространяем.

_"Это запрос на изменение спецификации. Это предложение само по себе является запросом на изменение спецификации._"

Это АБСОЛЮТНО не изменение спецификации. Это запрос на изменение _behavior_ команды build , а не изменение спецификации языка.

Если кто-то попросит, чтобы команда go имела переключатель для отображения вывода терминала на мандаринском языке, это не является изменением спецификации языка.

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

_ «Это важная часть обработки ошибок языка, которая должна быть одинаковой для всех компиляторов Go».

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

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

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

_"Нет. Флаги компилятора не должны изменять спецификацию языка."_

Если вы не играете в семантику, это не значит, что вы не играете в семантику.

Отлично. Затем вместо этого я запрашиваю новую команду верхнего уровня под названием _(что-то вроде)_ build-guard , используемую для запрета проблемных функций во время компиляции, начиная с запрета try() .

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


Итак, теперь, если вы действительно согласны с @networkimprov , отложите свой ответ на потом, как они предложили.

Извините, что прерываю, но у меня есть факты, чтобы сообщить :-)

Я уверен, что команда Go уже провела бенчмаркинг defer, но я не видел никаких цифр...

$ go test -bench=.
goos: linux
goarch: amd64
BenchmarkAlways2-2      20000000                72.3 ns/op
BenchmarkAlways4-2      20000000                68.1 ns/op
BenchmarkAlways6-2      20000000                68.0 ns/op

BenchmarkNever2-2       100000000               16.5 ns/op
BenchmarkNever4-2       100000000               13.1 ns/op
BenchmarkNever6-2       100000000               13.5 ns/op

Источник

package deferbench

import (
   "fmt"
   "errors"
   "testing"
)

func Always(iM, iN int) (err error) {
   defer func() {
      if err != nil {
         err = fmt.Errorf("d: %v", err)
      }
   }()
   if iN % iM == 0 {
      return errors.New("e")
   }
   return nil
}

func Never(iM, iN int) (err error) {
   if iN % iM == 0 {
      return fmt.Errorf("r: %v", errors.New("e"))
   }
   return nil
}

func BenchmarkAlways2(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e2, a) }}
func BenchmarkAlways4(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e4, a) }}
func BenchmarkAlways6(iB *testing.B) { for a := 0; a < iB.N; a++ { Always(1e6, a) }}

func BenchmarkNever2(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e2, a) }}
func BenchmarkNever4(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e4, a) }}
func BenchmarkNever6(iB *testing.B) { for a := 0; a < iB.N; a++ { Never(1e6, a) }}

@networkimprov

Из https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md#efficiency -of-defer (выделено жирным шрифтом)

Независимо друг от друга команда среды выполнения и компилятора Go обсуждала альтернативные варианты реализации, и мы считаем, что мы можем сделать типичное использование отложенного кода для обработки ошибок примерно таким же эффективным, как существующий «ручной» код. Мы надеемся сделать эту более быструю реализацию отсрочки доступной в Go 1.14 (см. также * CL 171758 * , который является первым шагом в этом направлении).

т. е. defer теперь на 30% повышает производительность для go1.13 для обычного использования и должен быть быстрее и так же эффективен, как режим non-defer в go 1.14.

Может кто выложит номера для 1.13 и 1.14 CL?

Оптимизации не всегда выживают при контакте с врагом... э-э, экосистемой.

1.13 отложенные будут примерно на 30% быстрее:

name     old time/op  new time/op  delta
Defer-4  52.2ns ± 5%  36.2ns ± 3%  -30.70%  (p=0.000 n=10+10)

Вот что я получаю в тестах @networkimprov выше (от 1.12.5 до подсказки):

name       old time/op  new time/op  delta
Always2-4  59.8ns ± 1%  47.5ns ± 1%  -20.57%  (p=0.008 n=5+5)
Always4-4  57.9ns ± 2%  43.5ns ± 1%  -24.96%  (p=0.008 n=5+5)
Always6-4  57.6ns ± 2%  44.1ns ± 1%  -23.43%  (p=0.008 n=5+5)
Never2-4   13.7ns ± 8%   3.8ns ± 4%  -72.27%  (p=0.008 n=5+5)
Never4-4   10.5ns ± 6%   1.3ns ± 2%  -87.76%  (p=0.008 n=5+5)
Never6-4   10.8ns ± 6%   1.2ns ± 1%  -88.46%  (p=0.008 n=5+5)

(Я не уверен, почему Never намного быстрее. Может быть, встраиваются изменения?)

Оптимизация отложений для 1.14 еще не реализована, поэтому мы не знаем, какой будет производительность. Но мы думаем, что должны приблизиться к производительности обычного вызова функции.

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

Блок деталей был отредактирован позже, после того, как я прочитал комментарий @networkimprov . Прошу прощения за то, что это выглядело так, как будто я понял, что он сказал, и проигнорировал это. Я заканчиваю дискуссию после этого заявления, я хотел объясниться, так как вы спросили меня, почему я разместил комментарий.


Что касается отложенных оптимизаций, я в восторге от них. Они немного помогают этому предложению, делая defer HandleErrorf(...) немного менее тяжелым. Однако мне по-прежнему не нравится идея злоупотребления именованными параметрами, чтобы этот трюк работал. Насколько ожидается ускорение для 1.14? Должны ли они работать с одинаковой скоростью?

@griesemer Одна область, которую, возможно, стоит немного расширить, — это то, как работают переходы в мире с try , возможно, включая:

  • Стоимость перехода между стилями оформления ошибок.
  • Классы возможных ошибок, которые могут возникнуть при переходе между стилями.
  • Какие классы ошибок будут (а) обнаружены немедленно ошибкой компилятора, по сравнению с (б) обнаружены vet или staticcheck или аналогичными, по сравнению с (в) могут привести к ошибке, которая могут быть не замечены или должны быть обнаружены с помощью тестирования.
  • Степень, в которой инструментарий может уменьшить стоимость и вероятность ошибки при переходе между стилями, и, в частности, может или нет gopls (или другая утилита) играть роль в автоматизации общих переходов стилей оформления.

Этапы оформления ошибки

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

0. Без оформления ошибки (например, использование try без оформления).
1. Унифицированное оформление ошибки (например, использование try + defer для равномерного оформления).
2. Точки выхода N-1 имеют одинаковое оформление ошибки , но 1 точка выхода имеет другое оформление (например, возможно постоянное подробное оформление ошибки только в одном месте или, возможно, временный журнал отладки и т. д.).
3. Все точки выхода имеют уникальное оформление ошибки или что-то близкое к уникальному.

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

Этап 0 и этап 1 кажутся лучшими для текущего предложения, а также довольно распространенными вариантами использования. Переход стадии 0->1 кажется простым. Если вы использовали try без каких-либо украшений на этапе 0, вы можете добавить что-то вроде defer fmt.HandleErrorf(&err, "foo failed with %s", arg1) . Возможно, в этот момент вам также потребуется ввести именованные возвращаемые параметры в соответствии с предложением, как оно было изначально написано. Однако, если в предложении принимается одно из предложений в виде предопределенной встроенной переменной, которая является псевдонимом для окончательного параметра результата ошибки, то стоимость и риск ошибки здесь могут быть небольшими?

С другой стороны, переход этапа 1->2 кажется неудобным (или "раздражающим", как говорят некоторые другие), если этап 1 был однородным украшением ошибки с defer . Чтобы добавить один конкретный элемент декора в одной точке выхода, сначала вам нужно удалить defer (чтобы избежать двойного украшения), затем, похоже, вам нужно будет посетить все точки возврата, чтобы обесточить try используется в операторах if , при этом N-1 ошибка оформляется одинаково, а 1 — по-разному.

Переход с этапа 1 на 3 также кажется неудобным, если его выполнять вручную.

Ошибки при переходе между стилями оформления

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

        w := try(os.Create(dst))
        defer func() {
                w.Close()
                if err != nil {
                        os.Remove(dst) // only if a “try” fails
                }
        }()

Если бы кто-то сделал «очевидное» ручное удаление сахара из w := try(os.Create(dst)) , эту строку можно было бы расширить до:

        w, err := os.Create(dst)
        if err != nil {
            // do something here
            return err
        }

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

Автоматизация перехода между стилями оформления

Чтобы помочь с временными затратами и риском ошибок, возможно, gopls (или другая утилита) может иметь некоторый тип команды для удаления сахара из определенного try или команду удаления сахара из всех использований try в заданной функции, которая может быть безошибочной в 100% случаев. Один из подходов может заключаться в том, что любые команды gopls фокусируются только на удалении и замене try , но, возможно, другая команда может обезжирить все использование try , а также преобразовать, по крайней мере, общие случаи вещей. например, defer fmt.HandleErrorf(&err, "copy %s %s", src, dst) в верхней части функции, в эквивалентный код в каждом из прежних местоположений try (что поможет при переходе с этапа 1->2 или этапа 1->3). Это не полностью готовая идея, но, возможно, стоит подумать о том, что возможно или желательно, или обновить предложение с учетом текущих мыслей.

Идиоматические результаты?

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

x1, x2, x3 = try(f())

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

t1, t2, t3, te := f()  // visible temporaries
if te != nil {
        return x1, x2, x3, te
}
x1, x2, x3 = t1, t2, t3

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

В предложении говорится о возможных различиях в поведении между if и try из-за именованных параметров результата, но в этом конкретном разделе, похоже, речь идет в основном о переходе от if к try (в заключительном разделе _"Хотя это и незначительная разница, мы считаем, что подобные случаи редки. Если ожидается текущее поведение, сохраните оператор if."_). Напротив, при переходе от try обратно к if при сохранении идентичного поведения могут возникнуть различные возможные ошибки, которые стоит уточнить.


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

@thepudds Мне нравится, что вы подчеркиваете затраты и потенциальные ошибки, связанные с тем, как особенности языка могут положительно или отрицательно повлиять на рефакторинг. Я вижу, что это не та тема, которую часто обсуждают, но она может иметь большой последующий эффект.

переход между этапами 1->2 кажется неудобным, если этап 1 представляет собой единообразное оформление ошибок с отсрочкой. Чтобы добавить один конкретный элемент оформления в одну точку выхода, сначала вам нужно удалить отсрочку (чтобы избежать двойного оформления), а затем, похоже, потребуется посетить все точки возврата, чтобы удалить сахар, который try использует в операторах if, с N -1 из ошибок оформляется одинаково и 1 по-разному.

Именно здесь использование break вместо return сияет в версии 1.12. Используйте его в блоке for range once { ... } , где once = "1" , чтобы разграничить последовательность кода, из которой вы, возможно, захотите выйти, а затем, если вам нужно украсить только одну ошибку, вы делаете это в точке break . И если вам нужно декорировать все ошибки, вы делаете это непосредственно перед единственной return в конце метода.

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

между прочим

Результаты @ randall77 для моего теста показывают 40 + нс накладных расходов на вызов как для 1.12, так и для наконечника. Это означает, что отсрочка может препятствовать оптимизации, в некоторых случаях делая улучшения для отсрочки спорными.

@networkimprov Defer в настоящее время блокирует оптимизацию, и это часть того, что мы хотели бы исправить. Например, было бы неплохо встроить тело функции defer так же, как мы встраиваем обычные вызовы.

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

Откуда такое утверждение?

40+ нс накладных расходов на вызов для функции с отложенным переносом ошибки не изменились.

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

Re swtch.com/try.html и https://github.com/golang/go/issues/32437#issuecomment -502192315:

@rsc , очень полезно! Если вы все еще пересматриваете его, может быть, свяжите ссылки на #id issue refs? А шрифт без засечек?

Эта страница о содержании. Не сосредотачивайтесь на деталях рендеринга. Я использую вывод blackfriday для входной уценки без изменений (поэтому нет ссылок #id, специфичных для GitHub), и я доволен шрифтом с засечками.

Повторная попытка отключения/проверки :

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

@mikeschinkel , уже дважды по этому вопросу вы описали использование try как _игнорирование_ ошибок.
7 июня вы написали под заголовком «Облегчает для разработчиков игнорирование ошибок»:

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

f, _ := os.Open(filename)

Я знаю, что могу быть лучше в своем собственном коде, но я также знаю, что многие из нас зависят от щедрости других разработчиков Go, которые публикуют чрезвычайно полезные пакеты, но из того, что я видел в _"Other People's Code(tm)"_ лучшие практики обработки ошибок часто игнорируются.

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

А затем 14 июня вы снова сослались на использование try как на «код, который таким образом игнорирует ошибки».

Если бы не фрагмент кода f, _ := os.Open(filename) , я бы подумал, что вы просто преувеличиваете, характеризуя «проверку ошибки и ее возврат» как «игнорирование» ошибки. Но фрагмент кода, наряду со многими вопросами, на которые уже есть ответы в документе с предложением или в спецификации языка, заставляет меня задуматься, говорим ли мы в конце концов об одной и той же семантике. Итак, чтобы быть ясным и ответить на ваши вопросы:

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

Когда я вижу, что try() оборачивает выражение, что произойдет, если будет возвращена ошибка?

Когда вы видите try(f()) , если f() возвращает ошибку, try остановит выполнение кода и вернет эту ошибку из функции, в теле которой try появляется

Будет ли ошибка просто игнорироваться?

Нет. Ошибка никогда не игнорируется. Он возвращается так же, как и при использовании оператора return. Нравиться:

{ err := f(); if err != nil { return err } }

Или он перейдет к первому или самому последнему defer ,

Семантика такая же, как и при использовании оператора return.

Отложенные функции выполняются " в порядке, обратном их отложенным ".

и если это так, он автоматически установит переменную с именем err внутри замыкания или передаст ее как параметр _(я не вижу параметра?)_.

Семантика такая же, как и при использовании оператора return.

Если вам нужно сослаться на параметр результата в теле отложенной функции, вы можете дать ему имя. См. пример result в https://golang.org/ref/spec#Defer_statements.

И если не имя автоматической ошибки, то как мне его назвать? Означает ли это, что я не могу объявить свою собственную переменную err в своей функции, чтобы избежать конфликтов?

Семантика такая же, как и при использовании оператора return.

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

И будет ли он вызывать все defer s? В обратном порядке или в обычном порядке?

Семантика такая же, как и при использовании оператора return.

Отложенные функции выполняются " в порядке, обратном их отложенным ". (Обратный порядок является обычным порядком.)

Или он вернется как из закрытия, так и из func , где была возвращена ошибка? _(Что-то я бы никогда не подумал, если бы не прочитал здесь слова, которые подразумевают это.)_

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

Прочитав предложение и все комментарии, я до сих пор, честно говоря, не знаю ответов на поставленные выше вопросы. Мы хотим добавить такую ​​функцию в язык, сторонники которого позиционируют себя как «Капитан Очевидность»?

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

x, y := try(f())

означает

tmp1, tmp2, tmpE := f()
if tmpE != nil {
   return ..., tmpE
}
x, y := tmp1, tmp2

почти все остальное должно следовать из следствий этого определения.

Это не "игнорирование" ошибок. Игнорирование ошибки — это когда вы пишете:

c, _ := net.Dial("tcp", "127.0.0.1:1234")
io.Copy(os.Stdout, c)

и код паникует, потому что net.Dial дал сбой и ошибка была проигнорирована, c равно nil, а вызов c.Read от io.Copy дал сбой. Напротив, этот код проверяет и возвращает ошибку:

 c := try(net.Dial("tcp", "127.0.0.1:1234"))
 io.Copy(os.Stdout, c)

Чтобы ответить на ваш вопрос о том, хотим ли мы поощрять последнее, а не первое: да.

@damienfamed75 Предложенное вами утверждение emit выглядит по существу так же, как утверждение handle эскизного проекта . Основной причиной отказа от объявления handle было его пересечение с объявлением defer . Мне непонятно, почему нельзя просто использовать defer , чтобы получить тот же эффект, что и emit .

@dominikh спросил :

Начнет ли acme выделять попытку?

Так много о предложении о попытке не решено, в воздухе, неизвестно.

Но на этот вопрос я могу ответить однозначно: нет.

@rsc

Благодарю за ваш ответ.

_"уже дважды по этому вопросу вы описали использование try как игнорирование ошибок."_

Да, я комментировал, используя свою точку зрения и технически неправильно.

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

_"Когда вы видите try(f()) , если f() возвращает ошибку, попытка остановит выполнение кода и вернет эту ошибку из функции, в теле которой появляется попытка."_

Это был ответ на вопрос из моего комментария некоторое время назад, но к настоящему времени я понял это.

И это заканчивается двумя вещами, которые меня огорчают. Причины:

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

  2. Для таких, как я, которые используют break и continue для обработки ошибок вместо return — шаблон, более устойчивый к изменяющимся требованиям — мы даже не сможем использовать try() , даже если на самом деле нет причин аннотировать ошибку.

_"Или он вернется как из замыкания, так и из функции, где была возвращена ошибка? (Что-то, что я никогда бы не подумал, если бы не прочитал здесь слова, которые подразумевают это.)"_

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

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

Чтобы уточнить для потомков, у defer есть закрытие, верно? Если вы вернетесь из этого замыкания, то — если я не ошибаюсь — он вернется не только из замыкания, но и из func , где произошла ошибка, верно? _(Не нужно отвечать, если да.)_

func example() {
    defer func(err) {
       return err // returns from both defer and example()
    }
    try(SomethingThatReturnsAnError)    
} 

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

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


Кстати, некоторые люди предложили синтаксис, подобный одному из следующих _ (я добавил гипотетические .Extend() , чтобы мои примеры были краткими):_

f := try os.Open(filename) else err {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err   
}

Или

try f := os.Open(filename) else err {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err    
}

И тогда другие утверждают, что он действительно не сохраняет никаких символов по этому поводу:

f,err := os.Open(filename)
if err != nil {
    err.Extend("Cannot open file %s",filename)
    //break, continue or return err    
}

Но одна вещь, которой не хватает критики, это переход с 5 строк на 4 строки, сокращение вертикального пространства , и это кажется значительным, особенно когда вам нужно много таких конструкций в func .

Еще лучше было бы что-то вроде этого, которое устранило бы 40% вертикального пространства _ (хотя, учитывая комментарии о ключевых словах, я сомневаюсь, что это будет рассмотрено):_

try f := os.Open(filename) 
    else err().Extend("Cannot open file %s",filename)
    end //break, continue or return err    

#фув


В ЛЮБОМ СЛУЧАЕ , как я сказал ранее, судно уплыло, так что я просто научусь принимать это.

Цели

Несколько комментариев здесь поставили под сомнение то, что мы пытаемся сделать с предложением. Напоминаем, что в Заявлении о проблеме обработки ошибок , которое мы опубликовали в августе прошлого года, в разделе «Цели» говорится:

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

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

Существующий код должен продолжать работать и оставаться таким же актуальным, как и сегодня. Любые изменения должны взаимодействовать с существующим кодом».

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

@mikeschinkel ,

Чтобы уточнить для потомков, у defer есть замыкание, верно? Если вы вернетесь из этого замыкания, то — если я не ошибаюсь — он вернется не только из замыкания, но и из func , где произошла ошибка, верно? _(Не нужно отвечать, если да.)_

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

func (d *Data) Op() int {
    d.mu.Lock()
    defer d.mu.Unlock()

     ... code to implement Op ...
}

Любой возврат из d.Op запускает вызов отложенной разблокировки после оператора return, но до передачи кода вызывающей стороне d.Op. Никакие действия внутри d.mu.Unlock не влияют на возвращаемое значение d.Op. Оператор return в d.mu.Unlock возвращает значение Unlock. Само по себе оно не возвращается из d.Op. Конечно, после возврата d.mu.Unlock возвращается и d.Op, но не напрямую из-за d.mu.Unlock. Это тонкий момент, но важный.

Переход к вашему примеру:

func example() {
    defer func(err) {
       return err // returns from both defer and example()
    }
    try(SomethingThatReturnsAnError)    
} 

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

func example() (err error) {
    defer func() {
        if err != nil {
            println("FAILED:", err.Error())
        }
    }()

    try(funcReturningError())
    return nil
}

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

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

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

В общем, в Go уже есть прекрасно подходящая конструкция для обработки ошибок: весь язык, особенно операторы if. @DavexPro был прав, сославшись на запись в блоге Go Errors is values ​​. Нам не нужно создавать целый отдельный подъязык, связанный с ошибками, и мы не должны этого делать. Я думаю, что основное понимание за последние полгода или около того заключалось в том, чтобы удалить «обработать» из предложения «проверить/обработать» в пользу повторного использования того языка, который у нас уже есть, включая возврат к операторам if, где это уместно. Это наблюдение о том, чтобы делать как можно меньше, исключает из рассмотрения большинство идей, касающихся дальнейшей параметризации новой конструкции.

Благодаря @brynbellomy за его многочисленные хорошие комментарии, я буду использовать его try-else в качестве иллюстративного примера. Да, мы могли бы написать:

func doSomething() (int, error) {
    // Inline error handler
    a, b := try SomeFunc() else err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    handler logAndContinue err {
        log.Errorf("non-critical error: %v", err)
    }
    handler annotateAndReturn err {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d := try SomeFunc() else logAndContinue
    e, f := try OtherFunc() else annotateAndReturn

    // ...

    return 123, nil
}

но, учитывая все обстоятельства, это, вероятно, не является значительным улучшением по сравнению с использованием существующих языковых конструкций:

func doSomething() (int, error) {
    a, b, err := SomeFunc()
    if err != nil {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    // Named error handlers
    logAndContinue := func(err error) {
        log.Errorf("non-critical error: %v", err)
    }
    annotate:= func(err error) (int, error) {
        return 0, errors.Wrap(err, "error in doSomething:")
    }

    c, d, err := SomeFunc()
    if err != nil {
        logAndContinue(err)
    }
    e, f, err := SomeFunc()
    if err != nil {
        return annotate(err)
    }

    // ...

    return 123, nil
}

То есть продолжать полагаться на существующий язык для написания логики обработки ошибок кажется предпочтительнее, чем создавать новый оператор, будь то try-else, try-goto, try-arrow или что-то еще.

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

@rsc Спасибо за разъяснение.

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

_(FWIW я нахожу использование defer для чего-то более сложного, чем закрытие дескриптора файла, менее очевидным из-за перехода назад в func перед возвратом. Поэтому всегда просто помещайте этот код в конец func после for range once{...} моего кода обработки ошибок break из.)_

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

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

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

Возвращаясь от gofmt к общим инструментам, предложение сосредоточиться на инструментах для написания проверок ошибок вместо изменения языка также проблематично. Как выразились Абельсон и Сассман, «программы должны быть написаны для того, чтобы их могли читать люди, и лишь случайно для того, чтобы машины могли выполнять их». Если машинное оборудование _требуется_ для работы с языком, то язык не выполняет свою работу. Удобочитаемость не должна ограничиваться людьми, использующими определенные инструменты.

Несколько человек использовали логику в противоположном направлении: люди могут писать сложные выражения, поэтому они неизбежно будут это делать, поэтому вам потребуется поддержка IDE или другого инструмента для поиска выражений try, так что try — плохая идея. Однако здесь есть несколько неподдерживаемых прыжков. Основной из них является утверждение, что, поскольку _возможно_ писать сложный, нечитаемый код, такой код станет повсеместным. Как заметил @josharian , на Go уже «можно писать отвратительный код ». Это не обычное дело, потому что у разработчиков есть нормы, касающиеся попыток найти наиболее читаемый способ написания определенного фрагмента кода. Так что совершенно точно _не_ тот случай, когда поддержка IDE потребуется для чтения программ, использующих try. А в тех немногих случаях, когда люди пишут действительно ужасный код, злоупотребляя try, поддержка IDE вряд ли будет полезна. Это возражение — люди могут писать очень плохой код, используя новую функцию — возникает почти при каждом обсуждении каждой новой языковой функции в каждом языке. Это не очень полезно. Более полезным было бы возражение в форме «люди будут писать код, который на первый взгляд кажется хорошим, но оказывается менее хорошим по этой неожиданной причине», как в обсуждении отладки печати .

Еще раз: удобочитаемость не должна ограничиваться людьми, использующими определенные инструменты.
(Я до сих пор печатаю и читаю программы на бумаге, хотя люди часто смотрят на меня за это странно.)

Спасибо @rsc за то, что поделились своими мыслями о том, чтобы операторы if отображались в виде одной строки.

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

Я оцениваю эти утверждения по-разному.

Я считаю, что уменьшение количества строк с 3 до 1 значительно облегчает задачу. Не будет ли требование, чтобы оператор if содержал, например, 9 (или даже 5) новых строк вместо 3, было бы существенно более тяжелым? Это же коэффициент (количество) сокращения/расширения. Я бы сказал, что структурные литералы имеют именно этот компромисс, и с добавлением try разрешает поток управления так же, как оператор if .

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

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

Я считаю, что уменьшение количества строк с 3 до 1 значительно облегчает задачу.

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

n, err := src.Read(buf)
if err == io.EOF {
    return nil
} else if err != nil {
    return err
}

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

n, err := src.Read(buf)
if err == io.EOF { return nil }
else if err != nil { return err }

вместо. Если бы мы начали так, я уверен, все было бы хорошо. Но мы этого не сделали, и это не то, где мы сейчас.

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

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

Несколько человек использовали логику в противоположном направлении: люди могут писать сложные выражения, поэтому они неизбежно будут это делать, поэтому вам потребуется поддержка IDE или другого инструмента для поиска выражений try, так что try — плохая идея. Однако здесь есть несколько неподдерживаемых прыжков. Основной из них является утверждение, что, поскольку _возможно_ писать сложный, нечитаемый код, такой код станет повсеместным. Как заметил @josharian , на Go уже «можно писать отвратительный код ». Это не обычное дело, потому что у разработчиков есть нормы, касающиеся попыток найти наиболее читаемый способ написания определенного фрагмента кода. Так что совершенно точно _не_ тот случай, когда поддержка IDE потребуется для чтения программ, использующих try. А в тех немногих случаях, когда люди пишут действительно ужасный код, злоупотребляя try, поддержка IDE вряд ли будет полезна. Это возражение — люди могут писать очень плохой код, используя новую функцию — возникает почти при каждом обсуждении каждой новой языковой функции в каждом языке. Это не очень полезно.

Разве это не единственная причина, по которой в Go нет тернарного оператора ?

Разве это не единственная причина, по которой в Go нет тернарного оператора?

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

Опыт работы с C подсказывает, что ? : попадает прямо во вторую категорию. (За возможным исключением min и max, я не уверен, что когда-либо видел код, использующий ? : который не был улучшен путем переписывания его с использованием оператора if. Но этот абзац выходит за рамки темы.)

Синтаксис

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

(Извините, если я неправильно понял историю происхождения!)

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

Я нашел этот пример @brynbellomy наводящим на размышления:

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := try(parentObjOne.AsCommit())
parentCommitTwo := try(parentObjTwo.AsCommit())
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
try parentCommitOne := parentObjOne.AsCommit()
try parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// vs

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
    parentCommitOne := parentObjOne.AsCommit()
    parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

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

Но, как заметил @ianlancetaylor , «попытка похоронит лидерство. Код становится серией операторов try, которые скрывают, что на самом деле делает код».

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

Предположим в качестве аргумента, что AsCommit никогда не дает сбоев и, следовательно, не возвращает ошибку. Теперь у нас есть:

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// vs

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

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

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

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// vs

headRef := try r.Head()
parentObjOne := try headRef.Peel(git.ObjectCommit)
parentObjTwo := try remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try index.WriteTree()
tree := try r.LookupTree(treeOid)

// vs

headRef := r.Head()?
parentObjOne := headRef.Peel(git.ObjectCommit)?
parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)?
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree()?
tree := r.LookupTree(treeOid)?

// vs

headRef := r.Head?()
parentObjOne := headRef.Peel?(git.ObjectCommit)
parentObjTwo := remoteBranch.Reference.Peel?(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := index.WriteTree?()
tree := r.LookupTree?(treeOid)

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

(1) точный операнд очень ясен, особенно по сравнению с префиксным оператором try x.y().z() .
(2) инструменты, которым не нужно знать о try, могут рассматривать его как простой вызов функции, поэтому, например, goimports будет работать нормально без каких-либо настроек, и
(3) есть место для будущего расширения и корректировки, если это необходимо.

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

Спасибо за все разъяснения. Чем больше я думаю, тем больше мне нравится предложение и вижу, как оно соответствует целям.

Почему бы не использовать такую ​​функцию, как recover() вместо err , откуда мы не знаем, откуда она взялась? Это было бы более последовательно и, возможно, проще в реализации.

func f() error {
 defer func() {
   if err:=error();err!=nil {
     ...
   }
 }()
}

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

@flibustenet , см. также https://swtch.com/try.html#named , где есть несколько подобных предложений.
(Отвечая на все из них: мы могли бы сделать это, но это не является строго необходимым, учитывая именованные результаты, поэтому мы могли бы также попытаться использовать существующую концепцию, прежде чем решить, что нам нужно предоставить второй способ.)

Непреднамеренным последствием try() может быть то, что проекты отказываются от _go fmt_, чтобы получить однострочные проверки ошибок. Это почти все преимущества try() без затрат. Я делал это в течение нескольких лет; это работает хорошо.

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

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

Неясно, какую «стоимость», по вашему мнению, имеет попытка. И хотя вы говорите, что отказ от gofmt не имеет «никаких затрат» на попытку (какими бы они ни были), вы, похоже, игнорируете тот факт, что форматирование gofmt используется всеми программами, помогающими переписать исходный код Go, такими как goimports, например, gorename , и так далее. Вы отказываетесь от go fmt за счет отказа от этих хелперов или, по крайней мере, примирения с существенными случайными изменениями кода при их вызове. Тем не менее, если у вас это получается хорошо, это здорово: во что бы то ни стало продолжайте это делать.

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

@rsc , как вы, возможно, заметили, хотя я предложил синтаксис блока try, позже я вернулся к варианту «нет» для этой функции - отчасти потому, что мне неудобно скрывать одну или несколько условных ошибок в приложении оператора или функции. Но позвольте мне уточнить один момент. В предложении блока try я явно разрешил операторы, которым не нужны try . Итак, ваш последний пример блока try будет таким:

try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
        parentCommitOne := parentObjOne.AsCommit()
        parentCommitTwo := parentObjTwo.AsCommit()
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

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

Ты сказал

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

Именно по этой причине я подумал о блоке try! Учитывается не только ключевое слово, но и обработка ошибок. Я не хочу думать о N разных местах, которые могут генерировать ошибки (за исключением случаев, когда я явно пытаюсь обработать определенные ошибки).

Еще несколько моментов, о которых стоит упомянуть:

  1. Вызывающий не знает, откуда именно возникла ошибка внутри вызываемого. Это также относится к простому предложению, которое вы рассматриваете в целом. Я предположил, что компилятор можно заставить добавить собственную аннотацию к точке возврата ошибки. Но я не думал об этом много.
  2. Мне не ясно, разрешены ли такие выражения, как try(try(foo(try(bar)).fum()) . Такое использование может быть осуждено, но их семантика должна быть определена. В случае с блоком try компилятору приходится прилагать больше усилий, чтобы обнаружить такое использование и перенести всю обработку ошибок на уровень блока try.
  3. Мне больше нравится return-on-error вместо try . Это легче проглотить на уровне блоков!
  4. С другой стороны, любые длинные ключевые слова делают текст менее читаемым.

FWIW, я все еще не думаю, что это стоит делать.

@rsc

[...]
Основной из них является утверждение, что, поскольку можно написать сложный, нечитаемый код, такой код станет повсеместным. Как заметил @josharian , на Go уже «можно писать отвратительный код».
[...]

headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := try(parentObjOne.AsCommit())
parentCommitTwo := try(parentObjTwo.AsCommit())

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

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

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

Если вы запретите вложенные try в первой версии, вы сможете снять это ограничение позже, если это необходимо, иначе это было бы невозможно.

Я уже обсуждал этот момент, но он кажется актуальным — сложность кода должна масштабироваться вертикально, а не горизонтально.

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

@rsc , на ваши вопросы,

Мой обработчик на уровне пакета в крайнем случае -- когда ошибка не ожидается:

func quit(err error) {
   fmt.Fprintf(os.Stderr, "quit after %s\n", err)
   debug.PrintStack()      // because panic(err) produces a pile of noise
   os.Exit(3)
}

Контекст: я активно использую os.File (где я нашел две ошибки: # 26650 и # 32088)

Декоратору уровня пакета, добавляющему базовый контекст, потребуется аргумент caller — сгенерированная структура, которая предоставляет результаты runtime.Caller().

Я бы хотел, чтобы переписчик _go fmt_ использовал существующее форматирование или позволял указать форматирование для каждого преобразования. Я обходюсь другими инструментами.

Затраты (т.е. недостатки) try() хорошо документированы выше.

Я искренне поражен тем, что команда Go предложила нам сначала check/handle (благотворно говоря, новая идея), а затем троичные try() . Я не понимаю, почему вы не опубликовали запрос предложений по обработке ошибок , а затем не собрали комментарии сообщества по некоторым полученным предложениям (см. #29860). Здесь много мудрости, которую вы могли бы использовать!

@rsc

Синтаксис

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

try {error} {optional wrap func} {optional return args in brackets}

f, err := os.Open(file)
try err wrap { a, b }

... и, ИМО, улучшение читаемости (за счет аллитерации), а также семантической точности:

f, err := os.Open(file)
relay err

или

f, err := os.Open(file)
relay err wrap

или

f, err := os.Open(file)
relay err wrap { a, b }

или

f, err := os.Open(file)
relay err { a, b }

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

Изменить, чтобы уточнить:
Попытка может означать: 1. испытать что-то, а затем оценить это субъективно 2. объективно проверить что-то 3. попытаться что-то сделать 4. запустить несколько потоков управления, которые можно прервать, и запустить перехватываемое уведомление, если это так

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

В предложении блока try я явно разрешил операторы, которые не нуждаются в блоке try.

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

Чтобы позаимствовать примеры @Goodwine , несмотря на его уродство, с точки зрения обработки ошибок даже это:

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

... лучше, чем то, что вы часто видите в языках try-catch

parentCommitOne := r.Head().Peel(git.ObjectCommit).AsCommit()
parentCommitTwo := remoteBranch.Reference.Peel(git.ObjectCommit).AsCommit()

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

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

Я написал небольшой инструмент: tryhard (который на данный момент не очень усердно работает) работает с файлом за файлом и использует простое сопоставление шаблонов AST для распознавания потенциальных кандидатов на try и сообщить (и переписать) их. Инструмент примитивен (без проверки типов), и есть неплохая вероятность ложных срабатываний, в зависимости от преобладающего стиля кодирования. Подробности читайте в документации .

Применяя его к $GOROOT/src в отчетах чаевых > 5000 (!) возможностей для try . Может быть много ложных срабатываний, но проверка приличного образца вручную показывает, что большинство возможностей реальны.

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

( Внимание: функция перезаписи уничтожит файлы! Используйте на свой страх и риск. )

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

Спасибо и наслаждайтесь.

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

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

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

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

Большая часть простоты Go проистекает из выбора ортогональных функций, которые составляются независимо. Добавление ограничений нарушает ортогональность, компонуемость, независимость и тем самым нарушает простоту.

Сегодня это правило, если у вас есть:

x := expression
y := f(x)

без использования x где-либо еще, то допустимым преобразованием программы является упрощение этого до

y := f(expression)

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

Короче говоря, ограничения добавляют значительную сложность. Им нужно серьезное обоснование, а не «давайте посмотрим, не наткнется ли кто-нибудь на эту стену и не попросит нас ее снести».

Я написал более подробное объяснение два года назад на https://github.com/golang/go/issues/18130#issuecomment -264195616 (в контексте псевдонимов типов), которое одинаково хорошо применимо и здесь.

@бакул ,

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

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

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

try {
    s = canThrowErrors()
    t = cannotThrowErrors()
    u = canThrowErrors() // a second call
} catch {
    // how many ways can you get here?
}

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

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

do {
    let s = try canThrowErrors()
    let t = cannotThrowErrors()
    let u = try canThrowErrors() // a second call
} catch {
    handle error from try above
}

Будь то Rust, Swift или это предложение, ключевым, критическим улучшением по сравнению с обработкой исключений является явная маркировка в тексте — даже очень легким маркером — каждого места, где стоит галочка.

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

Редактировать: см. также третий комментарий @velovix , который появился, пока я работал над этим.

@daved , я рад, что аналогия с «защитным реле» работает для вас. Это не работает для меня. Программы — это не схемы.

Любое слово можно понять неправильно:
«перерыв» не нарушает вашу программу.
«продолжить» не продолжает выполнение на следующем операторе, как обычно.
"goto" ... ну на самом деле goto невозможно понять неправильно. :-)

https://www.google.com/search?q=define+try говорит: «предпримите попытку или попытку что-то сделать» и «подлежит испытанию». Оба они относятся к «f := try(os.Open(file))». Он пытается выполнить os.Open (или подвергает результат ошибки испытанию), и если попытка (или результат ошибки) терпит неудачу, он возвращается из функции.

Мы использовали чек в августе прошлого года. Это тоже было хорошее слово. Мы переключились на try, несмотря на исторический багаж C++/Java/Python, потому что текущее значение try в этом предложении совпадает со значением try в Swift (без окружающего do-catch) и в оригинальном try в Rust! . Не будет ничего страшного, если позже мы решим, что слово «чек» — правильное слово, но сейчас мы должны сосредоточиться на других вещах, а не на имени.

Вот интересный ложноотрицательный результат tryhard от github.com/josharian/pct . Я упоминаю об этом здесь, потому что:

  • это показывает, как сложно автоматическое обнаружение try
  • это показывает, что визуальная стоимость if err != nil влияет на то, как люди (по крайней мере, я) структурируют свой код, и что try может помочь с этим.

До:

var err error
switch {
case *flagCumulative:
    _, err = fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s)
case *flagQuiet:
    _, err = fmt.Fprintln(w, line.s)
default:
    _, err = fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s)
}
if err != nil {
    return err
}

После (ручная перезапись):

switch {
case *flagCumulative:
    try(fmt.Fprintf(w, "% 6.2f%% % 6.2f%%% 6d %s\n", p, f*float64(runtot), line.n, line.s))
case *flagQuiet:
    try(fmt.Fprintln(w, line.s))
default:
    try(fmt.Fprintf(w, "% 6.2f%%% 6d %s\n", p, line.n, line.s))
}

Изменение https://golang.org/cl/182717 упоминает эту проблему: src: apply tryhard -r $GOROOT/src

Для наглядного представления о try в стандартной библиотеке перейдите к CL 182717 .

Спасибо, @josharian , за это . Да, даже для хорошего инструмента может оказаться невозможным обнаружить все возможные варианты использования для try . Но, к счастью, это не основная цель (данного предложения). Наличие инструмента полезно, но я вижу основное преимущество try в коде, который еще не написан (потому что его будет гораздо больше, чем кода, который у нас уже есть).

«перерыв» не нарушает вашу программу.
«продолжить» не продолжает выполнение на следующем операторе, как обычно.
"goto" ... ну на самом деле goto невозможно понять неправильно. :-)

break разрывает цикл. continue продолжает цикл, а goto переходит к указанному месту назначения. В конечном счете, я вас слышу, но, пожалуйста, подумайте, что происходит, когда функция в основном завершается и возвращает ошибку, но не выполняет откат. Это не было попыткой/испытанием. Я действительно думаю, что check намного лучше в этом отношении (чтобы «остановить прогресс» через «экзамен», безусловно, уместно).

Более того, мне любопытна форма try/check, которую я предложил, в отличие от других синтаксисов.
try {error} {optional wrap func} {optional return args in brackets}

f, err := os.Open(file)
try err wrap { a, b }

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

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

Мне любопытна форма try/check, которую я предложил, в отличие от других синтаксисов.

Я думаю, что эта форма в конечном итоге воссоздает существующие структуры управления .

@networkimprov , https://github.com/golang/go/issues/32437#issuecomment -502879351

Честно говоря, я поражен тем, что команда Go предложила нам сначала проверку/обработку (благотворно говоря, новая идея), а затем троичную функцию try(). Я не понимаю, почему вы не опубликовали запрос предложений по обработке ошибок, а затем не собрали комментарии сообщества по некоторым полученным предложениям (см. № 29860). Здесь много мудрости, которую вы могли бы использовать!

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

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

Я также не понимаю, как try является каким-то образом "тройным", что бы это ни значило.

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

@rsc , извините, что не по теме!
Я поднял обработчики уровня пакета в https://github.com/golang/go/issues/32437#issuecomment -502840914.
и ответил на ваш запрос о разъяснении в https://github.com/golang/go/issues/32437#issuecomment -502879351

Я рассматриваю обработчики на уровне пакета как функцию, от которой может отказаться практически каждый.

пожалуйста, используйте синтаксис try {} catch{}, не стройте больше колес

пожалуйста, используйте синтаксис try {} catch{}, не стройте больше колес

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

@джимвей

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

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

В ответ на https://github.com/golang/go/issues/32437#issuecomment -502837008 (комментарий @rsc о try как утверждение)

Вы поднимаете хороший вопрос. Мне жаль, что я каким-то образом пропустил этот комментарий, прежде чем сделать этот: https://github.com/golang/go/issues/32437#issuecomment -502871889

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

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

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

parentCommitOne := try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit()
parentCommitTwo := try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit()
tree := try(r.LookupTree(try(index.WriteTree())))

Первоначальный пример на самом деле выглядел довольно красиво, но этот показывает, что вложенность выражений try (даже всего 2 в глубину) действительно сильно ухудшает читабельность кода. Отказ от вложенных вызовов try также поможет решить проблему «отладки», поскольку гораздо проще расширить try в if , если он находится вне выражения.

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

x := 5 + try(strconv.Atoi(input))

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

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

@rsc

Мы можем и должны различать _"эту функцию можно использовать для написания очень читаемого кода, но также можно злоупотреблять ею для написания нечитаемого кода"_ и "основное использование этой функции будет заключаться в написании нечитаемого кода".
Опыт работы с C подсказывает, что ? : попадает прямо во вторую категорию. (За возможным исключением min и max,

Первое, что меня поразило в try() — против try как утверждения — это то, насколько он похож по вложенности на тернарный оператор и в то же время насколько противоположны аргументы в пользу try() и против тернарного оператора. были _(перефразируя):_

  • тернарный: _"Если мы разрешим это, люди будут вкладывать его, и в результате будет много плохого кода"_ игнорируя то, что некоторые люди пишут с ними лучший код, чем обычный.
  • try(): _"Вы можете вложить его, но мы сомневаемся, что многие это сделают, потому что большинство людей хотят писать хороший код"_,

При всем уважении, это рациональное объяснение разницы между ними кажется настолько субъективным, что я бы попросил немного самоанализа и, по крайней мере, подумал, можете ли вы рационализировать разницу в отношении функции, которую вы предпочитаете, по сравнению с функцией, которая вам не нравится? #please_dont_shoot_the_messenger

_"Я не уверен, что когда-либо видел код, использующий ? : который не был улучшен путем перезаписи с использованием оператора if вместо этого. Но этот абзац выходит за рамки темы.)"_

В других языках я часто улучшаю операторы, переписывая их с if на тернарный оператор, например, из кода, который я написал сегодня на PHP:

return isset( $_COOKIE[ CookieNames::CART_ID ] )
    ? intval( $_COOKIE[ CookieNames::CART_ID ] )
    : null;

Сравнить с:

if ( isset( $_COOKIE[ CookieNames::CART_ID ] ) ) {
    return intval( $_COOKIE[ CookieNames::CART_ID ] );
} else { 
    return null;
}

Насколько я понимаю, первое намного лучше второго.

между прочим

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

Сравните эту функцию, например, со встроенной функцией append() . Append был создан, потому что добавление к срезу было очень распространенным вариантом использования, и хотя это можно было сделать вручную, также легко было сделать это неправильно. Теперь append() позволяет добавлять не один, а много элементов, или даже целый слайс, и даже позволяет добавлять строку к []byte слайсу. Он достаточно мощный, чтобы охватить все случаи использования добавления к срезу. И, следовательно, никто больше не добавляет слайсы вручную.

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

Для более сложной обработки ошибок нам все еще нужно будет использовать if err != nil {} . Затем это приводит к двум различным стилям обработки ошибок, тогда как раньше был только один. Если это предложение — все, что мы можем сделать, чтобы помочь с обработкой ошибок в Go, то, я думаю, было бы лучше ничего не делать и продолжать обрабатывать обработку ошибок с if , как мы всегда делали, потому что, по крайней мере, это последовательно и имел то преимущество, что «есть только один способ сделать это».

@rsc , извините, что не по теме!
Я поднял обработчики уровня пакета в #32437 (комментарий)
и ответил на ваш запрос о разъяснении в #32437 (комментарий)

Я рассматриваю обработчики на уровне пакета как функцию, от которой может отказаться практически каждый.

Я не вижу, что связывает концепцию пакета с конкретной обработкой ошибок. Трудно представить, что концепция обработчика уровня пакета может быть полезна, скажем, для net/http . Аналогичным образом, несмотря на то, что в целом я писал пакеты меньше net/http , я не могу придумать ни одного варианта использования, в котором я бы предпочел конструкцию на уровне пакета для обработки ошибок. В общем, я обнаружил, что предположение о том, что все делятся своим опытом, вариантами использования и мнениями, опасно :)

@beoran , я считаю, что это предложение делает возможным дальнейшее улучшение. Как декоратор at last аргумент try(..., func(err) error) или tryf(..., "context of my error: %w") ?

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

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

https://github.com/golang/go/issues/32437#issuecomment-502975437

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

@beoran Согласен. Вот почему я предложил объединить подавляющее большинство ошибок по ключевому слову try ( try и try / else ). Несмотря на то, что синтаксис try / else не дает нам значительного сокращения длины кода по сравнению с существующим стилем if err != nil , он обеспечивает согласованность с try Случай else ). Эти два случая (попробуй и попробуй-иначе), скорее всего, охватывают подавляющее большинство случаев обработки ошибок. Я ставлю это в противопоставление встроенной версии try без использования других средств, которая применяется только в тех случаях, когда программист на самом деле ничего не делает для обработки ошибки, кроме возврата (что, как уже упоминалось другими в этой ветке, это не обязательно то, что мы действительно хотим поощрять в первую очередь).

Последовательность важна для удобочитаемости.

append — это окончательный способ добавления элементов в срез. make — это окончательный способ создания нового канала, карты или среза (за исключением литералов, которые мне не нравятся). Но try() (как встроенная функция и без else ) будет разбросана по кодовым базам, в зависимости от того, как программист должен обрабатывать данную ошибку, таким образом, что это, вероятно, немного хаотично и сбивает с толку. читатель. Кажется, это не в духе других встроенных функций (а именно, обработка случая, который либо довольно сложен, либо совершенно невозможен по-другому). Если эта версия try окажется успешной, согласованность и удобочитаемость заставят меня не использовать ее, так же как я стараюсь избегать литералов map/slice (и избегать new как чумы).

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

@deanveloper написал:

Думаю, я был бы признателен за это предложение немного больше, если бы golint запрещал вложенные вызовы try.

Я согласен с тем, что глубоко вложенные try трудно читать. Но это верно и для стандартных вызовов функций, а не только для встроенной функции try . Таким образом, я не понимаю, почему golint должны это запрещать.

@brynbellomy написал:

Несмотря на то, что синтаксис try/else не дает нам значительного сокращения длины кода по сравнению с существующим стилем if err != nil, он дает нам согласованность со случаем try (больше ничего).

Уникальная цель встроенной функции try состоит в том, чтобы уменьшить шаблон, поэтому трудно понять, почему мы должны использовать синтаксис try/else, который вы предлагаете, когда признаете, что он «не дает нам значительного сокращения». по длине кода».

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

Я чувствую необходимость выражать свое мнение, чего оно стоит. Хотя не все это носит академический и технический характер, я думаю, об этом нужно сказать.

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

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

Избегайте именования возвращаемых аргументов
Есть несколько пограничных случаев с операторами defer , где имя возвращаемого аргумента допустимо. Вне их, этого следует избегать. Это предложение продвигает использование именования возвращаемых аргументов. Это не поможет сделать код Go более читабельным.

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

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

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

Обработка ошибок в Go не нарушена, и это предложение во многом нарушает философию дизайна, которую я люблю в Go.

Билл прекрасно выражает мои мысли.

Я не могу остановить введение try , но если это так, я не буду использовать его сам; Я не буду учить этому и не буду принимать это в PR, которые я рецензирую. Он просто будет добавлен в список других «вещей в Go, которые я никогда не использую» (дополнительные сведения см. в забавном выступлении Мэта Райера на YouTube).

@ardan-bkennedy, спасибо за ваши комментарии.

Вы спросили о «деловой проблеме, которую пытаются решить». Я не думаю, что мы нацелены на проблемы какого-то конкретного бизнеса, за исключением, может быть, «программирования на Go». Но в более общем плане мы сформулировали проблему, которую пытаемся решить, в августе прошлого года в начале обсуждения проекта Gophercon (см. Обзор проблемы , особенно раздел Цели). Тот факт, что этот разговор ведется с августа прошлого года, также категорически противоречит вашему утверждению о том, что «все это началось 13 дней назад».

Вы не единственный человек, который предположил, что это не проблема или проблема, которую не стоит решать. См. https://swtch.com/try.html#nonissue для других подобных комментариев. Мы отметили их и хотим убедиться, что решаем реальную проблему. Часть пути к выяснению этого состоит в том, чтобы оценить предложение на реальных кодовых базах. В этом нам помогают такие инструменты, как tryhard Роберта. Ранее я просил людей сообщать нам, что они находят в своих собственных кодовых базах. Эта информация будет иметь решающее значение для оценки целесообразности изменений. У тебя одна догадка, у меня другая, и это нормально. Ответ состоит в том, чтобы заменить эти предположения данными.

Мы сделаем все необходимое, чтобы убедиться, что мы решаем реальную проблему.

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

@ardan-bkennedy, извините за второе продолжение, но по поводу:

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

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

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

Во-вторых, с самого начала проекта Go мы — Роберт, Роб, Кен, Ян и я — оценивали языковые изменения и функции, основываясь на нашем коллективном опыте создания множества систем. Мы спрашиваем: «Будет ли это хорошо работать в программах, которые мы пишем?» Это был успешный рецепт с широким применением, и мы намерены продолжать использовать его, снова дополненный данными, которые я просил в предыдущем комментарии, и отчетами об опыте в более общем плане. Мы бы не предлагали и не поддерживали изменение языка, которое мы не можем использовать в наших собственных программах или которое, по нашему мнению, не подходит для Go. И мы, конечно же, не стали бы предлагать или поддерживать плохие изменения только для того, чтобы иметь больше программистов на Go. В конце концов, мы тоже используем Go.

@rsc
Не будет недостатка в местах, где можно разместить это удобство. Какая метрика, помимо этого, ищется, которая докажет сущность механизма? Есть ли список классифицированных случаев обработки ошибок? Как будет извлекаться ценность из данных, когда большая часть общественного процесса управляется настроениями?

Инструменты tryhard очень информативны!
Я мог видеть, что я часто использую return ...,err , но только тогда, когда я знаю, что я вызываю функцию, которая уже оборачивает ошибку (с pkg/errors ), в основном в обработчиках http. Я выигрываю в удобочитаемости с меньшим количеством строк кода.
Затем в обработчике http тезисов я бы добавил defer fmt.HandleErrorf(&err, "handler xyz") и, наконец, добавил больше контекста, чем раньше.

Я также вижу много случаев, когда меня вообще не волнует ошибка fmt.Printf , и я сделаю это с помощью try .
Можно ли например сделать defer try(f.Close()) ?

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

Мне очень не терпится проверить в реале!

@flibustenet Предложение как таковое не позволит defer try(f()) (см. обоснование ). С этим всякие проблемы.

При использовании этого инструмента tryhard для просмотра изменений в кодовой базе, можем ли мы также сравнить соотношение if err != nil до и после, чтобы увидеть, что более распространено: добавить контекст или просто передать ошибку обратно?

Я думаю, что, возможно, гипотетический огромный проект может увидеть 1000 мест, где было добавлено try() , но есть 10000 if err != nil , которые добавляют контекст, поэтому, хотя 1000 выглядят огромными, это всего лишь 10% от всего. .

@Гудвин Да. Я, вероятно, не смогу внести это изменение на этой неделе, но код довольно прямолинейный и автономный. Не стесняйтесь попробовать (без каламбура), клонировать и настроить по мере необходимости.

Разве defer try(f()) не будет эквивалентно

defer func() error {
    if err:= f(); err != nil { return err }
    return nil
}()

Эта (версия if) в настоящее время не запрещена, верно? Мне кажется, здесь не стоит делать исключение -- может выдать предупреждение? И неясно, обязательно ли приведенный выше код отсрочки неверен. Что делать, если close(file) терпит неудачу в операторе defer ? Должны ли мы сообщать об этой ошибке или нет?

Я прочитал обоснование, в котором, кажется, говорится о defer try(f) , а не о defer try(f()) . Может опечатка?

Аналогичный аргумент можно привести для go try(f()) , что означает

go func() error {
    if err:= f(); err != nil { return err }
    return nil
}()

Здесь try не делает ничего полезного, но безвредно.

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

Что касается некоторых моментов, на которые @rsc ранее не обращал внимания:

  • Мы никогда не говорили, что обработка ошибок нарушена. Дизайн основан на наблюдении (сообщества Go!), что текущая обработка в порядке, но во многих случаях многословна — это бесспорно. Это главная предпосылка предложения.

  • Упрощение действий может также облегчить их понимание — эти два понятия не исключают друг друга и даже не подразумевают друг друга. Я призываю вас посмотреть на этот код для примера. Использование try удаляет значительное количество шаблонов, и этот шаблон практически ничего не добавляет к понятности кода. Исключение повторяющегося кода является стандартной и широко распространенной практикой кодирования для повышения качества кода.

  • Что касается «это предложение во многом нарушает философию дизайна»: важно то, что мы не относимся к «философии дизайна» догматически — это часто приводит к краху хороших идей (кроме того, я думаю, что мы знаем кое-что о философии дизайна Go). Существует много «религиозного рвения» (из-за отсутствия лучшего термина) вокруг именованных и неназванных параметров результата. Такие мантры, как «вы никогда не должны использовать именованные параметры результата» вне контекста, бессмысленны. Они могут служить общим руководством, но не абсолютной истиной. Именованные параметры результата по своей сути не являются «плохими». Хорошо названные параметры результата могут существенно дополнить документацию API. Короче говоря, давайте не будем использовать лозунги для принятия решений по языковому дизайну.

  • Смысл этого предложения состоит в том, чтобы не вводить новый синтаксис. Он просто предлагает новую функцию. Мы не можем написать эту функцию на языке, поэтому встроенная функция — естественное место для нее в Go. Это не только простая функция, но и определенная очень точно. Мы предпочитаем этот минимальный подход более комплексным решениям именно потому, что он делает одну вещь очень хорошо и практически ничего не оставляет на произвол дизайнерских решений. Мы также не сильно отклоняемся от проторенной дорожки, поскольку другие языки (например, Rust) имеют очень похожие конструкции. Предполагать, что «сообщество согласится с тем, что абстракция скрывает цену и не стоит простоты, которую она пытается обеспечить», означает вкладывать слова в уста других людей. Хотя мы ясно слышим ярых противников этого предложения, есть значительный процент (приблизительно 40%) людей, которые выразили одобрение продолжения эксперимента. Давайте не будем лишать их гражданских прав гиперболой.

Спасибо.

return isset( $_COOKIE[ CookieNames::CART_ID ] )
    ? intval( $_COOKIE[ CookieNames::CART_ID ] )
    : null;

Почти уверен, что это должно быть return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null; FWIW. 😁

@bakul , поскольку аргументы оцениваются немедленно, на самом деле это примерно эквивалентно:

<result list> := f()
defer try(<result list>)

Для некоторых это может быть неожиданным поведением, поскольку f() не откладывается на потом, а выполняется сразу. То же самое относится и к go try(f()) .

@bakul В документе упоминается defer try(f) (а не defer try(f()) , потому что try обычно применяется к любому выражению, а не только к вызову функции (вы можете сказать try(err) для например, если err имеет тип error ). Так что это не опечатка, но, возможно, сначала это сбивает с толку. f просто означает выражение, которое обычно оказывается функцией вызов.

@deanveloper , @griesemer Неважно :-) Спасибо.

@карл-мастранджело

_"Уверен, что это должно быть return intval( $_COOKIE[ CookieNames::CART_ID ] ) ?? null; _

Вы предполагаете, что PHP 7.x. Я не был. Но опять же, учитывая ваше язвительное лицо, вы понимаете, что дело было не в этом. :подмигивание:

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

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

  1. Расширьте структуру ошибок, поддерживая интерфейс ошибок mystruct.Error()
  2. Встроить ошибку либо как поле, либо как анонимное поле структуры
type ExtErr struct{
  error
  someOtherField string
}  

Они используются в нескольких стеках, которые на самом деле построили мои команды.

В предложении вопросов и ответов говорится
Q: Последний аргумент, передаваемый в try, должен иметь тип error. Почему недостаточно, чтобы входящий аргумент можно было присвоить ошибке?
О: "... Мы можем пересмотреть это решение в будущем, если это будет необходимо"

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

@mikeschinkel Я не тот Карл, которого ты ищешь.

@daved , повторно:

Не будет недостатка в местах, где можно разместить это удобство. Какая метрика, помимо этого, ищется, которая докажет сущность механизма? Есть ли список классифицированных случаев обработки ошибок? Как будет извлекаться ценность из данных, когда большая часть общественного процесса управляется настроениями?

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

Контекст ошибки

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

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

Но прежде чем мы начнем пытаться, стоит убедиться, что мы все на одной странице в отношении соответствующего контекста ошибки. Канонический пример — os.Open. Цитирование сообщения в блоге Go « Обработка ошибок и Go »:

Ответственность за обобщение контекста лежит на реализации ошибки.
Ошибка, возвращаемая форматами os.Open, как «открыть /etc/passwd: разрешение отклонено», а не просто «отказано в разрешении».

См. также раздел Effective Go об ошибках .

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

Сегодня существует много кода, следующего соглашению Go, но есть также много кода, предполагающего противоположное соглашение. Слишком часто можно увидеть такой код:

f, err := os.Open(file)
if err != nil {
    log.Fatalf("opening %s: %v", file, err)
}

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

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

В черновом проекте проверки/обработки Gophercon использовался такой код:

func CopyFile(src, dst string) error {
    handle err {
        return fmt.Errorf("copy %s %s: %v", src, dst, err)
    }

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

Это предложение изменило это, но идея та же:

func CopyFile(src, dst string) (err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("copy %s %s: %v", src, dst, err)
        }
    }()

    r := try(os.Open(src))
    defer r.Close()

    w := try(os.Create(dst))
    ...
}

и мы хотим добавить пока безымянный помощник для этого общего шаблона:

func CopyFile(src, dst string) (err error) {
    defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)

    r := try(os.Open(src))
    defer r.Close()

    w := try(os.Create(dst))
    ...
}

Короче говоря, разумность и успех этого подхода зависят от следующих предположений и логических шагов:

  1. Люди должны следовать заявленному соглашению Go «вызываемый объект добавляет соответствующий контекст, который он знает».
  2. Поэтому большинству функций нужно только добавить контекст функционального уровня, описывающий общий процесс.
    операция, а не конкретная часть, которая не удалась (эта часть уже сообщила о себе).
  3. Большая часть кода Go сегодня не добавляет контекст функционального уровня, потому что он слишком повторяющийся.
  4. Предоставление способа написать контекст функционального уровня один раз повысит вероятность того, что
    разработчики так делают.
  5. Конечным результатом будет больше кода Go, следующего соглашению и с добавлением соответствующего контекста.

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

Спасибо.

Спасибо @rsc за дополнительную информацию о лучших практиках контекста ошибки. В частности, я упоминал этот пункт о передовой практике, но он значительно улучшает отношение try s к контексту ошибки.

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

Таким образом, место, где try не помогает, — это когда нам нужно реагировать на ошибки, а не просто контекстуализировать их.

Чтобы адаптировать пример от Cleaner, более элегантный и неправильный , вот их пример функции, которая слегка ошибается в обработке ошибок. Я адаптировал его для Go, используя перенос ошибок в стиле try и defer :

func AddNewGuy(name string) (guy Guy, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("adding guy %v: %v", name, err)
        }
    }()

    guy = Guy{name: name}
    guy.Team = ChooseRandomTeam()
    try(guy.Team.Add(guy))
    try(AddToLeague(guy))
    return guy, nil
}

Эта функция неверна, потому что если guy.Team.Add(guy) $ завершится успешно, а AddToLeague(guy) не удастся, у команды будет недопустимый объект Guy, который не входит в лигу. Правильный код будет выглядеть так, где мы откатываем guy.Team.Add(guy) и больше не можем использовать try :

func AddNewGuy(name string) (guy Guy, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("adding guy %v: %v", name, err)
        }
    }()

    guy = Guy{name: name}
    guy.Team = ChooseRandomTeam()
    try(guy.Team.Add(guy))
    if err := AddToLeague(guy); err != nil {
        guy.Team.Remove(guy)
        return Guy{}, err
    }
    return guy, nil
}

Или, если мы не хотим предоставлять нулевые значения для возвращаемых значений без ошибок, мы можем заменить return Guy{}, err на try(err) . Несмотря на это, функция defer -ed по-прежнему выполняется, и контекст добавляется, что приятно.

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

Вы не единственный человек, который предположил, что это не проблема или проблема, которую не стоит решать. См. https://swtch.com/try.html#nonissue для других подобных комментариев. Мы отметили их и хотим убедиться, что решаем реальную проблему.

@rsc Я также думаю, что с текущим кодом ошибки проблем нет. Так что, пожалуйста, засчитайте меня.

В этом нам помогают такие инструменты, как tryhard Роберта. Ранее я просил людей сообщать нам, что они находят в своих собственных кодовых базах. Эта информация будет иметь решающее значение для оценки целесообразности изменений. У тебя одна догадка, у меня другая, и это нормально. Ответ состоит в том, чтобы заменить эти предположения данными.

Я посмотрел https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go , и мне больше нравится старый код. Меня удивляет, что вызов функции try может прервать текущее выполнение. Текущий Go работает иначе.

Я подозреваю, вы обнаружите, что мнения будут различаться. Я думаю, что это очень субъективно.

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

Я думаю, нам нужно переучить всех существующих пользователей Go, чтобы они теперь думали по-другому.

Нам также нужно решить, что делать с некоторыми пользователями/компаниями, которые откажутся использовать try в своем коде. Какие-то точно будут.

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

Как бы мы справлялись с ошибками компиляции, когда люди используют go1.13 и ранее для сборки кода с помощью try?

Я, вероятно, пропустил много других проблем, которые нам пришлось бы решить, чтобы реализовать это изменение. Стоит ли заморачиваться? Я так не думаю.

Алекс

@griesemer
Пытаясь попробовать файл с ошибкой 97, никто не поймал, я обнаружил, что 2 шаблона не переведены
1 :

    if err := updateItem(tx, fields, entityView.DataBinding, entityInstance); err != nil {
        tx.Rollback()
        return nil, err
    }

Не заменяется, вероятно, потому, что tx.Rollback() между err := и строкой возврата,
Что, как я предполагаю, может обрабатываться только defer - и если все пути ошибок должны быть tx.Rollback()
Это правильно ?

  1. Также не рекомендуется:
if err := db.Error; err != nil {
        return nil, err
    } else if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
        return nil, err
    } else {
        return itemDb, nil
    }

или

    if err := db.Error; err != nil {
        return nil, err
    } else {
            if itemDb, err := GetItem(c, entity, entityView, ItemRequest{recNo}); err != nil {
                return nil, err
            } else {
                return itemDb, nil
            }
        return result, nil
    }

Это из-за затенения или попытка вложения будет переведена в ? значение - следует ли использовать попытку или предлагается оставить как err := ... return err ?

@guybrand Re: два шаблона, которые вы нашли:

1) да, tryhard особо не старается. проверка типов необходима для более сложных случаев. Если tx.Rollback() нужно делать во всех путях, defer может быть правильным подходом. В противном случае сохранение if может быть правильным подходом. Это зависит от конкретного кода.

2) То же самое здесь: tryhard не ищет этот более сложный шаблон. Может быть, это могло бы.

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

@alexbrainman

Как бы мы справлялись с ошибками компиляции, когда люди используют go1.13 и ранее для сборки кода с помощью try?

Насколько я понимаю, версия самого языка будет контролироваться директивой языковой версии $# go go.mod в файле go.mod для каждого компилируемого фрагмента кода.

В go.mod документации go.mod директива языковой версии go описана следующим образом:

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

Если гипотетически что-то вроде нового встроенного try появится в чем-то вроде Go 1.15, то в этот момент кто-то, чей файл go.mod читает go 1.12 , не будет иметь доступа к этому новому try встроены, даже если они компилируются с набором инструментов Go 1.15. Насколько я понимаю, текущий план заключается в том, что им нужно будет изменить языковую версию Go, объявленную в их go.mod , с go 1.12 , чтобы вместо этого читать go 1.15 , если они хотят использовать новый Go. 1.15 языковая функция try .

С другой стороны, если у вас есть код, который использует try , и этот код находится в модуле, чей файл go.mod объявляет свою языковую версию Go как go 1.15 , но затем кто-то пытается создайте это с помощью набора инструментов Go 1.12, в этот момент набор инструментов Go 1.12 завершится ошибкой компиляции. Цепочка инструментов Go 1.12 ничего не знает о try , но она знает достаточно, чтобы напечатать дополнительное сообщение о том, что код, который не удалось скомпилировать, требует Go 1.15 на основе того, что находится в файле go.mod . Вы можете прямо сейчас провести этот эксперимент, используя сегодняшнюю цепочку инструментов Go 1.12, и увидеть полученное сообщение об ошибке:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

В документе с предложением переходов Go2 есть гораздо более продолжительное обсуждение.

Тем не менее, точные детали этого лучше обсудить в другом месте (например, в #30791 или в этой недавней ветке о голанг-орехах ).

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

Итого = количество строк кода
$find /path/to/repo -name '*.go' -exec cat {} \; | wc -l
Errs = количество строк с err := (это, вероятно, пропускает err = и myerr := , но я думаю, что в большинстве случаев это покрывает)
$find /path/to/repo -name '*.go' -exec cat {} \; | grep "err :=" | wc -l
tryhard = количество найденных строк

первый случай, который я тестировал для изучения, вернулся:
Итого = 5106
Ошибки = 111
трихард = 16

большая кодовая база
Всего = 131777
Ошибки = 3289
трихард = 265

Если этот формат приемлем, сообщите нам, как вы хотите получить результаты, я предполагаю, что просто бросить его здесь было бы неправильным форматом.
Кроме того, было бы, вероятно, по-быстрому подсчитать строки, случаи ошибки := (и, вероятно, err = , только 4 в кодовой базе, на которой я пытался учиться)

Спасибо.

От @griesemer в https://github.com/golang/go/issues/32437#issuecomment -503276339

Я призываю вас посмотреть на этот код для примера.

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

Я привожу это не как отчет об ошибке (хотя, может быть, так и должно быть?), а как шанс увидеть, влияет ли try на то, как это можно исправить. Я перечислю все способы, которые я могу придумать, чтобы исправить это, и подумаю, поможет ли добавление try или навредит. Вот несколько способов:

  1. Добавьте явные вызовы outf.Close() прямо перед возвратом ошибки.
  2. Назовите возвращаемое значение и добавьте отсрочку, чтобы закрыть файл, записав ошибку, если ее еще нет. например
func foo() (err error) {
    outf := try(os.Create())
    defer func() {
        cerr := outf.Close()
        if err == nil {
            err = cerr
        }
    }()

    ...
}
  1. Шаблон «двойного закрытия», когда выполняется defer outf.Close() для обеспечения очистки ресурсов и try(outf.Close()) перед возвратом, чтобы гарантировать отсутствие ошибок.
  2. Рефакторинг, чтобы вспомогательная функция брала открытый файл, а не путь, чтобы вызывающая сторона могла обеспечить правильное закрытие файла. например
func foo() error {
    outf := try(os.Create())
    if err := helper(outf); err != nil {
        outf.Close()
        return err
    }
    try(outf.Close())
    return nil
}

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

Надеюсь, этот анализ был полезен.

Если гипотетически что-то вроде нового встроенного try появится в чем-то вроде Go 1.15, то в этот момент кто-то, чей файл go.mod читает go 1.12 , не будет иметь доступа

@thepudds спасибо за объяснение. Но я не использую модули. Так что ваше объяснение выше моего понимания.

Алекс

@alexbrainman

Как бы мы справлялись с ошибками компиляции, когда люди используют go1.13 и ранее для сборки кода с помощью try?

Если бы try гипотетически попал в нечто вроде Go 1.15, то очень краткий ответ на ваш вопрос заключается в том, что кто-то, использующий Go 1.13 для сборки кода с try , увидит такую ​​ошибку компиляции:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

(По крайней мере, насколько я понимаю, что было сказано о предложении о переходе).

@alexbrainman Спасибо за отзыв.

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

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

Но вернемся к вашей точке зрения: программисты привыкают к тому, как язык программирования работает и ощущается. Если я чему-то и научился за 35 лет программирования, так это тому, что практически к любому языку можно привыкнуть, и это происходит очень быстро. После того, как я изучил оригинальный Pascal в качестве моего первого языка высокого уровня, было _немыслимо_, чтобы язык программирования не использовал все свои ключевые слова с заглавной буквы. Но потребовалась всего неделя или около того, чтобы привыкнуть к «морю слов», которым был C, где «нельзя было видеть структуру кода, потому что все строчные». После тех первых дней с C код Pascal выглядел ужасно громким, и весь фактический код казался погребенным в беспорядке кричащих ключевых слов. Перенесемся в Go: когда мы ввели заглавные буквы для обозначения экспортируемых идентификаторов, это было шокирующим изменением по сравнению с предыдущим, если я правильно помню, подходом на основе ключевых слов (это было до того, как Go стал общедоступным). Теперь мы думаем, что это одно из лучших дизайнерских решений (конкретная идея на самом деле исходит от команды Go). Или рассмотрим следующий мысленный эксперимент: Представьте себе, что в Go не было оператора defer , и теперь кто-то приводит веские доводы в пользу defer . defer не имеет семантики, как что-либо еще в этом языке, новый язык больше не похож на тот, что был до defer Go. Тем не менее, после того, как он прожил с ним десять лет, он кажется полностью «похожим на Go».

Дело в том, что первоначальная реакция на изменение языка почти бессмысленна без фактической проверки механизма в реальном коде и получения конкретной обратной связи. Конечно, существующий код обработки ошибок прекрасен и выглядит яснее, чем замена с использованием try — нас приучили думать об этих операторах if уже десять лет. Ну и конечно код try выглядит странно и имеет "странную" семантику, мы никогда раньше его не использовали, и не сразу распознаем его как часть языка.

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

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

https://github.com/golang/go/issues/32437#issuecomment -503297387 в значительной степени говорит, что если вы оборачиваете ошибки более чем одним способом в одну функцию, вы, очевидно, делаете это неправильно. Между тем, у меня есть много кода, который выглядит так:

        if err := gen.Execute(tmp, s); err != nil {
                return fmt.Errorf("template error: %v", err)
        }

        if err := tmp.Close(); err != nil {
                return fmt.Errorf("cannot write temp file: %v", err)
        }
        closed = true

        if err := os.Rename(tmp.Name(), *genOutput); err != nil {
                return fmt.Errorf("cannot finalize file: %v", err)
        }
        removed = true

( closed и removed используются отложенными для очистки, если это необходимо)

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

processing path/to/dir: template: gen:42:17: executing "gen" at <.Broken>: can't evaluate field Broken in type main.state

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

Я не думаю, что пользователь должен видеть

processing path/to/dir: rename /tmp/blahDs3x42aD commands.gen.go: No such file or directory

без какого-либо понятия о том, _почему_ моя программа пытается сделать это переименование, какова семантика, какова цель. Я считаю, что добавление небольшого количества «невозможно завершить файл:» действительно помогает.

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

processing path/to/dir: open /some/path/here: No such file or directory

Что это значит? Я хочу добавить причину, по которой приложение пыталось создать там файл (вы даже не знали, что это было создание, а не только os.Open! Это ENOENT, потому что промежуточного пути не существует). Это не то, что следует добавлять к _всем_ ошибкам, возвращаемым этой функцией.

Итак, чего мне не хватает. Я "неправильно держу"? Должен ли я помещать каждую из этих вещей в отдельную крошечную функцию, которая использует отсрочку для переноса всех своих ошибок?

@guybrand Спасибо за эти цифры . Было бы неплохо получить некоторое представление о том, почему цифры tryhard такие, какие они есть. Возможно, происходит много специфического оформления ошибок? Если это так, это здорово, и операторы if — правильный выбор.

Я улучшу инструмент, когда доберусь до него.

Спасибо, @zeebo за ваш анализ . Я не знаю конкретно об этом коде , но похоже, что outf является частью loadCmdReader (строка 173), которая затем передается в строке 204. Возможно, поэтому outf не закрыт. (Извините, я не написал этот код).

@tv42 Из примеров в вашем https://github.com/golang/go/issues/32437#issuecomment -503340426, если вы не делаете это «неправильно», похоже, что вы используете оператор if это способ обработки этих случаев, если все они требуют разных ответов. try не поможет, а defer только усложнит задачу (любое другое предложение по изменению языка в этой ветке, которое пытается упростить написание этого кода, настолько близко к if заявление о том, что не стоит внедрять новый механизм). См. также FAQ подробного предложения.

@griesemer Тогда все, о чем я могу думать, это то, что вы и @rsc не согласны. Или что я действительно «делаю это неправильно» и хотел бы поговорить об этом.

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

@tv42 Сообщение @rsc посвящено общей структуре обработки ошибок хорошего кода, с чем я согласен. Если у вас есть существующий фрагмент кода, который не совсем соответствует этому шаблону, и вы довольны этим кодом, оставьте его в покое.

откладывает

Основное изменение от проекта проверки/обработки Gophercon к этому предложению заключалось в отказе от handle в пользу повторного использования defer . Теперь контекст ошибки будет добавлен кодом, подобным этому отложенному вызову (см. мой предыдущий комментарий о контексте ошибки):

func CopyFile(src, dst string) (err error) {
    defer HelperToBeNamedLater(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

Жизнеспособность отсрочки в качестве механизма аннотации ошибок в этом примере зависит от нескольких вещей.

  1. _Именованные результаты ошибок._ Было много опасений по поводу добавления именованных результатов ошибок. Это правда, что мы не одобряли это в прошлом, когда это не было необходимо для целей документации, но это соглашение, которое мы выбрали в отсутствие какого-либо более сильного решающего фактора. И даже в прошлом более сильный решающий фактор, такой как ссылка на конкретные результаты в документации, перевешивал общее соглашение для неназванных результатов. Теперь есть второй более важный решающий фактор, а именно желание сослаться на ошибку в отсрочке. Кажется, что это должно вызывать не больше возражений, чем наименования результатов для использования в документации. Многие люди отреагировали на это довольно негативно, и я, честно говоря, не понимаю, почему. Создается впечатление, что люди путают возвраты без списков выражений (так называемые «голые возвраты») с именованными результатами. Это правда, что возвраты без списков выражений могут привести к путанице в больших функциях. Избежать этой путаницы, избегая этих возвратов в длинных функциях, часто имеет смысл. Окрашивание названных результатов одной и той же кистью не дает.

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

  3. _Defer сам по себе._ Несколько человек выразили обеспокоенность по поводу использования defer в качестве языковой концепции, опять же, потому что новые пользователи могут быть с ней незнакомы. Как и в случае с адресными выражениями, defer является базовой концепцией языка, которую необходимо изучить в конце концов. Стандартные идиомы вокруг таких вещей, как defer f.Close() и defer l.mu.Unlock() , настолько распространены, что трудно оправдать отказ от отложенного использования как малоизвестного уголка языка.

  4. _Производительность._ В течение многих лет мы обсуждали работу над тем, чтобы общие шаблоны отложений, такие как отложенные в верхней части функции, имели нулевые накладные расходы по сравнению с вставкой этого вызова вручную при каждом возврате. Мы думаем, что знаем, как это сделать, и изучим это для следующего выпуска Go. Даже если это не так, текущие накладные расходы примерно в 50 нс не должны быть чрезмерными для большинства вызовов, которым необходимо добавить контекст ошибки. И несколько вызовов, чувствительных к производительности, могут продолжать использовать операторы if до тех пор, пока defer не станет быстрее.

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

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

С тех пор, как мы начали это обсуждение в августе прошлого года, я выполнял умственное упражнение: «Как этот код будет выглядеть с проверкой/обработкой?» и совсем недавно «с try/defer?» каждый раз, когда я пишу новый код. Обычно ответ означает, что я пишу другой, лучший код, с добавлением контекста в одном месте (отложенном), а не при каждом возврате или вообще опущенном.

Учитывая идею использования отложенного обработчика для обработки ошибок, существует множество шаблонов, которые мы могли бы реализовать с помощью простого пакета библиотеки. Я подал # 32676, чтобы подумать об этом, но с использованием API пакета в этой проблеме наш код будет выглядеть так:

func CopyFile(src, dst string) (err error) {
    defer errd.Add(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

Если бы мы отлаживали CopyFile и хотели увидеть любую возвращенную ошибку и трассировку стека (аналогично желанию вставить отладочную печать), мы могли бы использовать:

func CopyFile(src, dst string) (err error) {
    defer errd.Trace(&err)
    defer errd.Add(&err, "copy %s %s", src, dst)

    r := check os.Open(src)
    defer r.Close()

    w := check os.Create(dst)
    ...
}

и так далее.

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

Будет ли это работать на практике? Мы хотим узнать.

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

Люди могут поэкспериментировать с этим подходом сегодня, продолжая писать операторы if err != nil , но копируя хелперы defer и используя их по мере необходимости. Если вы склонны сделать это, сообщите нам, что вы узнали.

@tv42 , я согласен с @griesemer. Если вы обнаружите, что для сглаживания соединения необходим дополнительный контекст, например, переименование является шагом «финализации», нет ничего плохого в использовании операторов if для добавления дополнительного контекста. Однако во многих функциях такой дополнительный контекст не требуется.

@guybrand , цифры tryhard великолепны, но еще лучше было бы описание того, почему конкретные примеры не конвертировались, и, кроме того, было бы неуместно переписывать, чтобы их можно было конвертировать. Пример и объяснение @ tv42 являются примером этого.

@griesemer о вашем беспокойстве по поводу defer . Я шел к этому emit или в первоначальном предложении handle . emit/handle будет вызываться, если err не равен нулю. И будет инициироваться в этот момент, а не в конце функции. Отсрочка вызывается в конце. emit/handle БУДЕТ завершить функцию в зависимости от того, равна ли err ноль или нет. Вот почему отсрочка не сработает.

некоторые данные:

из ~70 тыс. LOC-проектов, над которыми я работал, чтобы неукоснительно устранить «голые возвраты ошибок», у нас все еще есть 612 возвратов голых ошибок. в основном имеет дело со случаем, когда регистрируется ошибка, но сообщение важно только для внутреннего использования (сообщение пользователю предопределено). Однако try() будет иметь большую экономию, чем просто две строки на каждый голый возврат, потому что с предопределенными ошибками мы можем отложить обработчик и использовать try в большем количестве мест.

что более интересно, в каталоге поставщика из примерно 620 тыс. LOC у нас есть только 1600 возвратов ошибок. библиотеки, которые мы выбираем, имеют тенденцию украшать ошибки даже более религиозно, чем мы.

@rsc , если позже обработчики будут добавлены в try , будет ли пакет ошибок/errc с такими функциями, как func Wrap(msg string) func(error) error , чтобы вы могли сделать try(f(), errc.Wrap("f failed")) ?

@damienfamed75 Спасибо за ваши объяснения . Таким образом, emit будет вызываться, когда try находит ошибку, и вызывается с этой ошибкой. Это кажется достаточно ясным.

Вы также говорите, что emit завершит функцию, если есть ошибка, а не если ошибка была каким-то образом обработана. Если вы не завершите функцию, где продолжится код? Предположительно с возвратом из try (иначе я не понимаю emit , который не завершает функцию). Не проще ли было бы в этом случае просто использовать if вместо try ? Использование emit или handle в этих случаях сильно затеняет поток управления, особенно потому, что предложение emit может находиться в совершенно другой части (предположительно, более ранней) в функции. (Кроме того, можно ли иметь более одного emit ? Если нет, то почему? Что произойдет, если не будет emit ? Множество тех же вопросов, которые мучили оригинальный check / handle эскизный проект.)

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

@guybrand (и @griesemer) в отношении вашего второго нераспознанного паттерна см. https://github.com/griesemer/tryhard/issues/2

@daved

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

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

Так что по крайней мере есть надежда. :)

Для людей, пробующих tryhard , если вы еще этого не сделали, я бы посоветовал вам не только посмотреть, какие изменения сделал инструмент, но также выполнить grep для оставшихся экземпляров err != nil и посмотреть на что он оставил в покое и почему.

(И также обратите внимание, что на https://github.com/griesemer/tryhard/ есть несколько проблем и PR.)

@rsc вот мое понимание того, почему мне лично не нравится шаблон defer HandleFunc(&err, ...) . Это не потому, что я ассоциирую это с голой отдачей или чем-то еще, это просто кажется слишком «умным».

Несколько месяцев (может быть, год?) назад было предложение по обработке ошибок, однако сейчас я потерял его из виду. Я забыл, что он просил, однако кто-то ответил что-то вроде:

func myFunction() (i int, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("wrapping the error: %s", err)
        }
    }()

    // ...
    return 0, err

    // ...
    return someInt, nil
}

Было интересно посмотреть, если не сказать больше. Я впервые увидел, как defer используется для обработки ошибок, и теперь это показано здесь. Я вижу его как «умный» и «хакерский», и, по крайней мере, в примере, который я привожу, он не похож на Go. Тем не менее, обернув его в правильный вызов функции с чем-то вроде fmt.HandleErrorf , он выглядит намного приятнее. Но я все равно негативно к этому отношусь.

Другая причина, по которой я вижу, что людям это не нравится, заключается в том, что когда кто-то пишет return ..., err , похоже, что нужно вернуть err . Но он не возвращается, вместо этого значение изменяется перед отправкой. Я уже говорил, что return всегда казалось «священной» операцией в Go, и поощрять код, который изменяет возвращаемое значение перед фактическим возвратом, кажется неправильным.

Хорошо, цифры и данные, это то. :)

Я попробовал исходники нескольких сервисов нашей микросервисной платформы и сравнил их с результатами loccount и grep 'if err'. Я получил следующие результаты в порядке loccount / grep 'if err' | туалет / трихард:

1382/64/14
108554 / 66 / 5
58401/22/5
2052/247/39
12024/1655/1

Некоторые из наших микросервисов много обрабатывают ошибки, а некоторые — совсем немного, но, к сожалению, tryhard смог автоматически улучшить код только в лучшем случае в 22% случаев, в худшем — менее чем в 1%. Теперь мы не собираемся вручную переписывать нашу обработку ошибок, поэтому такой инструмент, как tryhard, будет необходим для добавления try() в нашу кодовую базу. Я понимаю, что это простой предварительный инструмент, но я был удивлен тем, как редко он мог помочь.

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

Я также обнаружил в наших базах кода, что случай использования if err != nil { return err } try() на самом деле очень редок, в отличие от компилятора go, где он распространен. При всем уважении, но я думаю, что разработчики Go, которые просматривают исходный код компилятора Go гораздо чаще, чем другие кодовые базы, из-за этого переоценивают полезность try() .

@beoran tryhard на данный момент очень зачаточный. Есть ли у вас понимание наиболее распространенных причин, по которым try будет редкостью в вашей кодовой базе? Например, потому что вы украшаете ошибки? Потому что вы выполняете другую дополнительную работу перед возвращением? Что-то другое?

@rsc , @griesemer

Что касается примеров , я привел здесь два повторяющихся примера, которые пропустили tryHard, один, вероятно, останется как «если Err :=», другой может быть разрешен

что касается оформления ошибок , я вижу в коде два повторяющихся шаблона (я поместил их в один фрагмент кода):

if v, err := someFunction(vars...) ; err != nil {
        return fmt.Errorf("extra data to help with where did error occur and params are %s , %d , err : %v",
            strParam, intParam, err)
    } else if v2, err := passToAnotherFunc(v,vars ...);err != nil {
        extraData := DoSomethingAccordingTo(v2,err)
        return formatError(err,extraData)
    } else {

    }

И во многих случаях formatError является некоторым стандартом для приложения или когда-либо пересекающихся репозиториев, наиболее часто повторяющимся является форматирование DbError (одна функция во всех приложениях, используемая в десятках мест), в некоторых случаях (не вдаваясь в «это правильный шаблон"), сохраняя некоторые данные в журнал (неудачный запрос sql, который вы не хотели бы пропускать в стек) и некоторый другой текст ошибки.

Другими словами, если я хочу «сделать что-нибудь умное с дополнительными данными, такими как регистрация ошибки A и вызов ошибки B, в дополнение к моему упоминанию этих двух опций для расширения обработки ошибок
Это еще один вариант «больше, чем просто вернуть ошибку и позволить «кому-то другому» или «какой-то другой функции» справиться с ней».

Это означает, что try(), вероятно, чаще используется в «библиотеках», чем в «исполняемых программах», возможно, я попытаюсь провести сравнение Total/Errs/tryHard, чтобы отличить библиотеки от runnables («приложений»).

Я оказался именно в ситуации, описанной в https://github.com/golang/go/issues/32437#issuecomment -503297387
На каком-то уровне я оборачиваю ошибки по отдельности, я не буду менять это с помощью try , это нормально с if err!=nil .
На другом уровне я просто return err , сложно добавить один и тот же контекст для всех возвратов, тогда я буду использовать try и defer .
Я даже уже делаю это с помощью специального регистратора, который я использую в начале функции на случай ошибки. По мне try и украшение по функциям уже гоиш.

@пуддс

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

Go 1.13 еще даже не выпущен, поэтому я не могу его использовать. И, поскольку в моем проекте не используются модули Go, я не смогу перейти на Go 1.13. (Я считаю, что Go 1.13 потребует от всех использования модулей Go)

для сборки кода с try будет отображаться ошибка компиляции, подобная этой:

.\hello.go:3:16: undefined: try
note: module requires Go 1.15

(По крайней мере, насколько я понимаю, что было сказано о предложении о переходе).

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

Если попытка не определена, я бы искал ее. И ничего не найду. Что мне делать тогда?

И note: module requires Go 1.15 — худший помощник в этой ситуации. Почему module ? Почему Go 1.15 ?

@griesemer

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

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

Но вернемся к вашей точке зрения: программисты привыкают к тому, как язык программирования работает и ощущается. ...

Я согласен со всеми вашими пунктами. Но мы говорим о замене конкретной формы оператора if вызовом функции try. Это на языке, который гордится своей простотой и ортогональностью. Ко всему можно привыкнуть, но какой смысл? Чтобы сохранить пару строк кода?

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

Спустя много лет меня все еще обманывает тело defer и закрываются переменные. Но defer окупается с лихвой, когда дело доходит до управления ресурсами. Я не могу представить Go без defer . Но я не готов платить аналогичную цену за try , потому что не вижу здесь никакой выгоды.

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

Я попытался изменить свой небольшой проект (около 1200 строк кода). И это похоже на ваше изменение на https://go-review.googlesource.com/c/go/+/182717/1/src/cmd/link/internal/ld/macho_combine_dwarf.go . Я не вижу своего мнения. изменить об этом через неделю. Мой ум всегда чем-то занят, и я забуду.

... Но еще рано, это предложение всего две недели, ...

И я вижу, что только в этой ветке уже есть 504 сообщения об этом предложении. Если бы я был заинтересован в продвижении этого изменения, мне потребовались бы дни, если не недели, чтобы просто прочитать и понять все это. Я не завидую твоей работе.

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

Алекс

@griesemer Спасибо за замечательное предложение, и tryhard кажется более полезным, чем я ожидаю. Я тоже хочу оценить.

@rsc спасибо за четко сформулированный ответ и инструмент.

Я слежу за этой темой некоторое время, и следующие комментарии @beoran вызывают у меня мурашки по коже.

Сокрытие переменной ошибки и возврата не помогает понять ситуацию.

У меня уже было несколько bad written code , и я могу засвидетельствовать, что это худший кошмар для каждого разработчика.

Тот факт, что в документации сказано использовать лайки A , не означает, что этому будут следовать, факт остается фактом: если можно использовать AA , AB , то нет предела тому, как его можно использовать.

To my surprise, people already think the code below is cool ... Я думаю it's an abomination При всем уважении извиняюсь перед всеми, кого обидел.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

Подождите, пока вы не проверите AsCommit и увидите

func AsCommit() error(){
    return try(try(try(tail()).find()).auth())
}

Безумие продолжается, и, честно говоря, я не хочу верить, что это определение @robpike simplicity is complicated (Юмор)

На основе примера @rsc

// Example 1
headRef := try(r.Head())
parentObjOne := try(headRef.Peel(git.ObjectCommit))
parentObjTwo := try(remoteBranch.Reference.Peel(git.ObjectCommit))
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
treeOid := try(index.WriteTree())
tree := try(r.LookupTree(treeOid))

// Example 2 
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid)

// Example 3 
try (
    headRef := r.Head()
    parentObjOne := headRef.Peel(git.ObjectCommit)
    parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)
)
parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()
try (
    treeOid := index.WriteTree()
    tree := r.LookupTree(treeOid)
)

Я за Example 2 с небольшим else Обратите внимание, что это может быть не лучший подход.

  • легко увидеть ошибку
  • Наименее возможно мутировать в abomination , которых могут родить другие
  • try ведет себя не как обычная функция. дать ему функциональный синтаксис немного. go использует if , и если я могу просто изменить его на try tree := r.LookupTree(treeOid) else { , он будет более естественным
  • Ошибки могут быть очень и очень дорогими, им нужна как можно большая видимость, и я думаю, что это причина, по которой go не поддерживает традиционные try и catch
try headRef := r.Head()
try parentObjOne := headRef.Peel(git.ObjectCommit)
try parentObjTwo := remoteBranch.Reference.Peel(git.ObjectCommit)

parentCommitOne := parentObjOne.AsCommit()
parentCommitTwo := parentObjTwo.AsCommit()

try treeOid := index.WriteTree()
try tree := r.LookupTree(treeOid) else { 
    // Heal the world 
   // I may return with return keyword 
   // I may not return but set some values to 0 
   // I may remember I need to log only this 
   // I may send a mail to let the cute monkeys know the server is on fire 
}

Еще раз хочу извиниться за то, что был немного эгоистичен.

@josharian Я не могу здесь слишком много разглашать, однако причины весьма разнообразны. Как вы говорите, мы украшаем ошибки и/или также выполняем различную обработку, а также важным вариантом использования является то, что мы регистрируем их, где сообщение журнала отличается для каждой ошибки, которую может вернуть функция, или потому что мы используем if err := foo() ; err != nil { /* various handling*/ ; return err } форме или по другим причинам.

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

РЕДАКТИРОВАТЬ: Если try() будет реализован, то я думаю, что следующим шагом должно стать улучшение tryhard, чтобы его можно было широко использовать для обновления существующих баз кода.

@griesemer Я постараюсь решить все ваши проблемы один за другим из вашего последнего ответа .
Сначала вы спросили о том, что произойдет, если обработчик не вернет или не выйдет из функции каким-либо образом. Да, могут быть случаи, когда предложение emit / handle не будет возвращать функцию или выходить из нее, а продолжится с того места, где оно было остановлено. Например, в случае, когда мы пытаемся найти разделитель или что-то простое с помощью считывателя, и мы достигаем EOF , мы можем не захотеть возвращать ошибку, когда нажимаем это. Поэтому я построил этот краткий пример того, как это могло бы выглядеть:

func findDelimiter(r io.Reader) ([]byte, error) {
    emit err {
        // if this doesn't return then continue from where we left off
        // at the try function that was called last.
        if err != io.EOF {
            return nil, err
        }
    }

    bufReader := bufio.NewReader(r)

    token := try(bufReader.ReadSlice('|'))

    return token, nil
}

Или даже можно упростить до этого:

func findDelimiter(r io.Reader) ([]byte, error) {
    emit err != io.EOF {
        return nil, err
    }

    bufReader := bufio.NewReader(r)

    token := try(bufReader.ReadSlice('|'))

    return token, nil
}

Вторая проблема заключалась в нарушении потока управления. И да, это нарушило бы поток, но, честно говоря, большинство предложений несколько нарушают поток, чтобы иметь одну центральную функцию обработки ошибок и тому подобное. Это ничем не отличается, я считаю.
Затем вы спросили, использовали ли мы emit / handle более одного раза, и я сказал, что это переопределено.
Если вы используете emit более одного раза, он перезапишет последний и так далее. Если у вас их нет, то try будет иметь обработчик по умолчанию, который просто возвращает нулевые значения и ошибку. Это означает, что этот пример здесь:

func writeStuff(filename string) (io.ReadCloser, error) {
    emit err {
        return nil, err
    }

    f := try(os.Open(filename))

    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

Сделал бы то же самое, что и в этом примере:

func writeStuff(filename string) (io.ReadCloser, error) {
    // when not defining a handler then try's default handler kicks in to
    // return nil valued then error as usual.

    f := try(os.Open(filename))

    try(fmt.Fprintf(f, "stuff\n"))

    return f, nil
}

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

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

@velovix , re https://github.com/golang/go/issues/32437#issuecomment -503314834:

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

Это действительно хороший способ выразить это. Спасибо.

@olekukonko , re https://github.com/golang/go/issues/32437#issuecomment -503508478:

To my surprise, people already think the code below is cool ... Я думаю it's an abomination При всем уважении извиняюсь перед всеми, кого обидел.

parentCommitOne := try(try(try(r.Head()).Peel(git.ObjectCommit)).AsCommit())
parentCommitTwo := try(try(remoteBranch.Reference.Peel(git.ObjectCommit)).AsCommit())

Греппинг https://swtch.com/try.html , это выражение встречается в этой ветке три раза.
@goodwine назвал это плохим кодом, я согласился, а @velovix сказал: «Несмотря на его уродство… он лучше, чем то, что вы часто видите в языках try-catch… потому что вы все еще можете сказать, какие части кода могут отклонить поток управления из-за ошибки, а который не может».

Никто не говорил, что это «круто» или что-то вроде отличного кода. Опять же, всегда можно написать плохой код .

Я бы также просто сказал повторно

Ошибки могут стоить очень дорого, им нужна как можно большая видимость.

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

@alexbrainman , я прошу прощения за путаницу по поводу того, что произойдет, если старые версии кода сборки Go содержат try. Короткий ответ: это похоже на любой другой раз, когда мы меняем язык: старый компилятор отклонит новый код с бесполезным сообщением (в данном случае «undefined: try»). Сообщение бесполезно, потому что старый компилятор не знает о новом синтаксисе и не может быть более полезным. В этот момент люди, вероятно, выполнят поиск в Интернете по запросу «go undefined try» и узнают о новой функции.

В примере @thepudds код, использующий try, имеет go.mod, который содержит строку «go 1.15», что означает, что автор модуля говорит, что код написан для версии языка Go. Это служит сигналом для более старых команд go после ошибки компиляции предположить, что, возможно, бесполезное сообщение связано со слишком старой версией Go. Это явная попытка сделать сообщение немного более полезным, не заставляя пользователей прибегать к поиску в Интернете. Если это поможет, хорошо; если нет, то веб-поиск в любом случае кажется довольно эффективным.

@guybrand , re https://github.com/golang/go/issues/32437#issuecomment -503287670 и с извинениями за возможное опоздание на вашу встречу:

Одна общая проблема с функциями, которые возвращают не совсем ошибочные типы, заключается в том, что для не-интерфейсов преобразование в ошибку не сохраняет нулевую форму. Так, например, если у вас есть свой собственный конкретный тип *MyError (скажем, указатель на структуру) и вы используете err == nil в качестве сигнала успеха, это здорово, пока вы не

func f() (int, *MyError)
func g() (int, error) { x, err := f(); return x, err }

Если f возвращает nil *MyError, g возвращает то же значение, что и ненулевая ошибка, что, скорее всего, не то, что предполагалось. Если *MyError является интерфейсом, а не указателем на структуру, то преобразование сохраняет нуль, но даже в этом случае это тонкость.

Что касается try, вы можете подумать, что, поскольку try срабатывает только для значений, отличных от nil, нет проблем. Например, на самом деле это нормально, поскольку возвращает ненулевую ошибку при ошибке f, а также нормально возвращает nil-ошибку при успешном выполнении f:

func g() (int, error) {
    return try(f()), nil
}

Так что на самом деле это нормально, но потом вы можете увидеть это и подумать о том, чтобы переписать его на

func g() (int, error) {
    return f()
}

кажется, что это должно быть то же самое, но это не так.

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

Спасибо всем за отзывы . На данный момент кажется, что мы определили основные преимущества, проблемы и возможные положительные и отрицательные последствия использования try . Чтобы добиться прогресса, их необходимо дополнительно оценить, изучив, что try будет означать для реальных кодовых баз. Обсуждение в этот момент ходит по кругу и повторяет одни и те же пункты.

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

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

Блокировка начнется примерно в 1:00 по тихоокеанскому времени или в 4:00 по восточному поясному времени (примерно через 3 часа), чтобы дать людям возможность отправить отложенный пост. Мы вновь откроем этот вопрос для дальнейшего обсуждения 1 июля.

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

Эта вики-страница переполнена ответами на проверку/обработку. Я предлагаю вам начать новую страницу.

В любом случае, продолжать садоводство в вики у меня не будет времени.

@networkimprov , спасибо за вашу помощь в садоводстве. Я создал новый верхний раздел в https://github.com/golang/go/wiki/Go2ErrorHandlingFeedback. Я думаю, что это должно быть лучше, чем полностью новая страница.

Я также пропустил заметку Роберта 1p PDT / 4p EDT для блокировки, поэтому я ненадолго заблокировал ее слишком рано. Он снова открыт, но ненадолго.

Я планировал написать это и просто хотел закончить до того, как он будет заблокирован.

Я надеюсь, что команда go не увидит критику и не почувствует, что она свидетельствует о настроении большинства. Всегда есть тенденция к тому, чтобы красноречивое меньшинство подавляло разговор, и я чувствую, что это могло произойти здесь. Когда все идут по касательной, это обескураживает других, которые просто хотят говорить о предложении КАК ЕСТЬ.

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

У меня есть код, который уже использует defer для декорирования/аннотирования ошибок, даже для выплевывания трассировки стека, именно по этой причине.

Видеть:
https://github.com/ugorji/go-ndb/blob/master/ndb/ndb.go#L331
https://github.com/ugorji/go-serverapp/blob/master/app/baseapp.go#L129
https://github.com/ugorji/go-serverapp/blob/master/app/webrouter.go#L180

которые все вызывают errorutil.OnError(*error)

https://github.com/ugorji/go-common/blob/master/errorutil/errors.go#L193

Это похоже на хелперы отсрочки, о которых упоминали ранее Расс/Роберт.

Это шаблон, который я уже использую, FWIW. Это не магия. ИМХО вполне проходное.

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

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

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

Здесь обсуждались 2 режима расширения:

  • расширить попытаться взять метку, чтобы перейти к
  • расширить попытаться взять обработчик ошибки формы func (ошибка)

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

Решение о том, необходимо ли продление срока судебного разбирательства, и если да, то в каком направлении, не принято. Следовательно, первое направление состоит в том, чтобы попытаться устранить большую часть заикания «if err != nil {return err }», которое я всегда ненавидел, но считал ценой ведения бизнеса на ходу.

Я лично рад, что try — это функция, которую я могу вызвать встроенно, например, я могу написать

var u User = db.loadUser(try(strconv.Atoi(stringId)))

В отличие от:

var id int // i have to define this on its own if err is already defined in an enclosing block
id, err = strconv.Atoi(stringId)
if err != nil {
  return
}
var u User = db.loadUser(id)

Как видите, я просто сократил 6 строк до 1. И 5 из этих строк действительно шаблонные.
Это то, с чем я сталкивался много раз, и я написал много кода и пакетов для go — вы можете проверить мой github, чтобы увидеть некоторые из них, которые я разместил в Интернете, или мою библиотеку кодеков для go.

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

Я лично в восторге от появления try(...). И я понимаю причины, по которым try как функция является предпочтительным решением. Мне явно нравится, что здесь используется отсрочка, так как это имеет смысл.

Давайте вспомним один из основных принципов го — ортогональные концепции, которые можно хорошо комбинировать. В этом предложении используется множество ортогональных концепций go (отсрочка, именованные возвращаемые параметры, встроенные функции, позволяющие делать то, что невозможно с помощью пользовательского кода и т. д.), чтобы обеспечить ключевое преимущество, которое
go пользователи повсеместно запрашивали в течение многих лет, т.е. сокращение/устранение стандартного шаблона if err != nil { return err }. Опросы пользователей Go показывают, что это реальная проблема. Команда go осознает, что это реальная проблема. Я рад, что громкие голоса некоторых не слишком искажают позицию команды го.

У меня был один вопрос о попытке как неявном переходе, если err != nil.

Если мы решим, что это направление, будет ли трудно заменить «попытка выполнить возврат» на «попытка выполнить переход»,
учитывая, что goto определил семантику, что вы не можете пройти мимо нераспределенных переменных?

Спасибо за заметку, @ugorji.

У меня был один вопрос о попытке как неявном переходе, если err != nil.

Если мы решим, что это направление, будет ли трудно заменить «попытка выполнить возврат» на «попытка выполнить переход»,
учитывая, что goto определил семантику, что вы не можете пройти мимо нераспределенных переменных?

Да, точно. Есть некоторое обсуждение #26058.
Я думаю, что «try-goto» имеет по крайней мере три удара по нему:
(1) вы должны ответить на нераспределенные переменные,
(2) вы теряете информацию о стеке о том, какая попытка не удалась, которую, напротив, вы все еще можете получить в случае возврата + отсрочки, и
(3) все любят ненавидеть гото.

Да, try — это то, что нужно.
Однажды я пытался добавить try , и мне это понравилось.
Патч - https://github.com/ascheglov/go/pull/1
Тема на Reddit — https://www.reddit.com/r/golang/comments/6vt3el/the_try_keyword_proofofconcept/

@griesemer

Продолжая https://github.com/golang/go/issues/32825#issuecomment -507120860...

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

Разбивая это на части, кажется, что есть две формы выраженного потока управления путем ошибок: ручной и автоматический. Что касается переноса ошибок, то, по-видимому, выражаются три формы: прямая, косвенная и сквозная. В результате получается шесть «режимов» обработки ошибок.

Режимы Manual Direct и Automatic Direct кажутся приемлемыми:

wrap := func(err error) error {
  return fmt.Errorf("failed to process %s: %v", filename, err)
}

f, err := os.Open(filename)
if err != nil {
    return nil, wrap(err)
}
defer f.Close()

info, err := f.Stat()
if err != nil {
    return nil, wrap(err)
}
// in errors, named better, and optimized
WrapfFunc := func(format string, args ...interface{}) func(error) error {
  return func(err error) error {
    if err == nil {
      return nil
    }
    s := fmt.Sprintf(format, args...)
    return errors.Errorf(s+": %w", err)
  }
}

```иди
wrap := errors.WrapfFunc("не удалось обработать %s", имя файла)

f, ошибка := os.Open(имя файла)
попробовать (обернуть (ошибиться))
отложить f.Close()

информация, ошибка := f.Stat()
попробовать (обернуть (ошибиться))

Manual Pass-through, and Automatic Pass-through modes are also simple enough to be agreeable (despite often being a code smell):
```go
f, err := os.Open(filename)
if err != nil {
    return nil, err
}
defer f.Close()

info, err := f.Stat()
if err != nil {
    return nil, err
}
f := try(os.Open(filename))
defer f.Close()

info := try(f.Stat())

Однако режимы Manual Indirect и Automatic Indirect весьма неприятны из-за высокой вероятности незаметных ошибок:

defer errd.Wrap(&err, "failed to do X for %s", filename)

var f *os.File
f, err = os.Open(filename)
if err != nil {
    return
}
defer f.Close()

var info os.FileInfo
info, err = f.Stat()
if err != nil {
    return
}
defer errd.Wrap(&err, "failed to do X for %s", filename)

f := try(os.Open(filename))
defer f.Close()

info := try(f.Stat())

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

Попытка не должна быть функцией, чтобы избежать этого проклятого

info := try(try(os.Open(filename)).Stat())

утечка файлов.

Я имею в виду, что оператор try не позволяет создавать цепочки. И лучше смотреть в качестве бонуса. Хотя есть проблемы с совместимостью.

@sirkon Поскольку try является особенным, язык может запретить вложенные try , если это важно, даже если try выглядит как функция. Опять же, если это единственное препятствие для try , его можно легко устранить различными способами ( go vet или языковое ограничение). Давайте двигаться дальше - мы слышали это уже много раз. Спасибо.

Давайте двигаться дальше - мы слышали это много раз раньше

«Это так скучно, давай покончим с этим»

Есть еще хороший аналог:

- Ваша теория противоречит фактам!
- Тем хуже для фактов!

Гегель

Я имею в виду, что вы решаете проблему, которой на самом деле не существует. И некрасивый способ в этом.

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

IMO самая сложная проблема такого рода - сохранение согласованности данных в распределенных системах (и не так распределенных на самом деле). И обработка ошибок не была проблемой, с которой я боролся в Go, решая их. Отсутствие понимания срезов и карт, отсутствие суммы/алгебраических/дисперсионных/каких-либо типов было НАМНОГО более раздражающим.

Поскольку дискуссия здесь, кажется, не утихает, позвольте мне повторить еще раз:

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

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

Спасибо.

Я не хочу быть грубым здесь, и я ценю всю вашу модерацию, но сообщество очень сильно высказалось по поводу изменения обработки ошибок. Изменение вещей или добавление нового кода расстроят _всех_ людей, предпочитающих текущую систему. Вы не можете сделать всех счастливыми, поэтому давайте сосредоточимся на 88%, которые мы можем сделать счастливыми (число получено из приведенного ниже соотношения голосов).

На момент написания этой статьи тема «оставь это в покое» набрала 1322 голоса за и 158 против. Этот поток находится на 158 вверх и 255 вниз. Если это не является непосредственным завершением этой ветки обработки ошибок, то у нас должна быть очень веская причина продолжать продвигать эту проблему.

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

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

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

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

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

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

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

В любом случае, отстой, что я делаю это здесь - не стесняйтесь удалять этот комментарий. Я не обижусь. Но кто-то должен сказать это прямо!

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

Люди могут молчать, но они все еще ожидают Go 2. Я лично с нетерпением жду этого и остальных Go 2. Go 1 — отличный язык, и он хорошо подходит для разных типов программ. Я надеюсь, что Go 2 расширит это.

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

PS: Интересно, какая оппозиция будет, когда будут предлагать дженерики.

У нас есть около дюжины инструментов, написанных на ходу в нашей компании. Я запустил инструмент tryhard с нашей кодовой базой и нашел 933 потенциальных кандидата на попытку try(). Лично я считаю функцию try() блестящей идеей, потому что она решает больше, чем просто проблему шаблонного кода.

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

var file= try(parse())

func parse()(err, result) {
}

Он применяет один способ обработки ошибок вместо объявления переменной ошибки и свободного разрешения шаблона err!=nil err==nil, что затрудняет читаемость, увеличивает риск подверженного ошибкам кода в IMO:

func Foo() (err error) {
    var file, ferr = os.Open("file1.txt")
    if ferr == nil {
               defer file.Close()
        var parsed, perr = parseFile(file)
        if perr != nil {
            return
        }
        fmt.Printf("%s", parsed)
    }
    return nil
}

На мой взгляд, с try() код становится более читаемым, последовательным и безопасным:

func Foo() (err error) {
    var file = try(os.Open("file.txt"))
        defer file.Close()
    var parsed = try(parseFile(file))
    fmt.Printf(parsed)
    return
}

Я провел несколько экспериментов, подобных тому, что @lpar сделал со всеми незаархивированными репозиториями Go Heroku (общедоступными и частными).

Результаты приведены в этом списке: https://gist.github.com/freeformz/55abbe5da61a28ab94dbb662bfc7f763 .

копия @davecheney

@ubikenobi Твоя более безопасная функция ~is~ просочилась.

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

Наконец, хотя и не часто, err == nil служит законным тестом для некоторых ранних возвратов.

@Дэвид

Спасибо, что указали на утечку, я забыл добавить defer.Close() в оба примера. (обновлено сейчас).

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

Я рассматриваю случай err==nil как исключение, чем норму в большинстве случаев. Это может быть полезно в некоторых случаях, как вы упомянули, но что мне не нравится, так это непоследовательный выбор разработчиков без уважительной причины. К счастью, в нашей кодовой базе подавляющее большинство выражений имеют вид err!=nil, что может легко принести пользу функции try().

  • Я проверил tryhard на большом Go API, который я постоянно поддерживаю с командой из четырех других инженеров. В 45580 строках кода Go tryhard определил 301 ошибку для перезаписи (таким образом, это будет изменение +301/-903) или переписал около 2% кода, предполагая, что каждая ошибка занимает примерно 3 строки. Принимая во внимание комментарии, пробелы, импорт и т. д., это кажется мне существенным.
  • Я использовал инструмент линии от tryhard, чтобы изучить, как try изменит мою работу, и субъективно мне это очень нравится! Глагол try кажется мне более ясным, что что-то может пойти не так в вызывающей функции, и делает это компактно. Я очень привык писать if err != nil , и я не против, но и не против измениться. Запись и рефакторинг пустой переменной, предшествующей ошибке (т.е. создание пустого фрагмента/карты/переменной для возврата) повторно, вероятно, более утомительны, чем сам err .
  • Немного сложно следить за всеми ветками обсуждения, но мне любопытно, что это означает для переноса ошибок. Было бы неплохо, если бы try было вариативным, если вы хотите дополнительно добавить контекст, например try(json.Unmarshal(b, &accountBalance), "failed to decode bank account info for user %s", user) . Изменить: этот момент, вероятно, не по теме; однако, глядя на перезапись без попытки, это происходит именно здесь.
  • Я действительно ценю мысль и заботу, вложенные в это! Обратная совместимость и стабильность очень важны для нас, и работа над Go 2 на сегодняшний день была очень гладкой для поддержки проектов. Спасибо!

Разве это не должно быть сделано на исходном коде, который был проверен опытными Гоферами, чтобы убедиться, что замены являются рациональными? Какая часть переписанных «2%» должна была быть переписана с явной обработкой? Если мы этого не знаем, то LOC остается относительно бесполезной метрикой.

* Именно поэтому мой пост сегодня утром был посвящен «режимам» обработки ошибок. Легче и содержательнее обсудить способы обработки ошибок, которые облегчает try , а затем бороться с потенциальными опасностями кода, который мы, вероятно, напишем, чем запускать довольно произвольный счетчик строк.

@kingishb Сколько найденных точек _try_ находятся в публичных функциях из неосновных пакетов? Как правило, общедоступные функции должны возвращать собственные ошибки пакета (т. е. завернутые или оформленные)...

@networkimprov Это слишком упрощенная формула для моих чувств. В чем это звучит правдоподобно, так это в том, что поверхности API возвращают поддающиеся проверке ошибки. Обычно целесообразно добавлять контекст к сообщению об ошибке на основе релевантности контекста, а не его позиции в стеке вызовов.

Многие ложные срабатывания, вероятно, учитываются в текущих показателях. А как насчет промахов, которые происходят из-за следующих рекомендуемых практик (https://blog.golang.org/errors-are-values)? try , вероятно, сократит использование таких практик, и в этом смысле они являются основными объектами для замены (вероятно, один из немногих вариантов использования, которые действительно заинтриговали меня). Итак, опять же, кажется бессмысленным очищать существующий источник без должной осмотрительности.

Спасибо @ubikenobi , @freeformz и @kingishb за сбор ваших данных, большое спасибо! Кроме того, если вы запустите tryhard с параметром -err="" , if также попытается работать с кодом, в котором переменная ошибки называется иначе, чем err (например, e ). Это может привести к еще нескольким случаям, в зависимости от базы кода (но также, возможно, увеличить вероятность ложных срабатываний).

@griesemer на случай, если вам нужны дополнительные данные. Я провел tryhard против двух наших микросервисов и получил следующие результаты:

cloc v 1.82 / трихард
13280 строк кода Go / 148 идентифицировано для попытки (1%)

Другой сервис:
9768 строк кода Go / 50 идентифицировано для попытки (0,5%)

Впоследствии tryhard проинспектировал более широкий набор различных микросервисов:

314343 строки кода Go / 1563 идентифицированы для попытки (0,5%)

Делаем быстрый осмотр. Типы пакетов, которые try могут оптимизировать, обычно представляют собой адаптеры/оболочки службы, которые прозрачно возвращают ошибку (GRPC), возвращенную из упакованной службы.

Надеюсь это поможет.

Это абсолютно плохая идея.

  • Когда err var появляется для defer ? Как насчет «явное лучше, чем неявное»?
  • Мы используем простое правило: вы должны быстро найти ровно одно место, где у вас вернулась ошибка. Каждая ошибка обернута контекстом, чтобы понять, что и где идет не так. defer создаст много уродливого и трудного для понимания кода.
  • @davecheney написал отличный пост об ошибках, и предложение полностью противоречит всему в этом посте.
  • Наконец, если вы используете os.Exit , ваши ошибки не будут проверены.

Я только что запустил tryhard в пакете (с поставщиком), и он сообщил о 2478 , при этом количество кода упало с 873934 до 851178 , но я не уверен как интерпретировать это, потому что я не знаю, какая часть этого связана с избыточной оболочкой (с отсутствием поддержки stdlib для упаковки ошибок трассировки стека) или какая часть этого кода связана даже с обработкой ошибок.

Однако я точно знаю, что только на этой неделе я потратил позорное количество времени из-за копи-пасты типа if err != nil { return nil } и ошибок, которые выглядят как error: cannot process ....file: cannot parse ...file: cannot open ...file .

\ Я бы не стал придавать слишком большого значения количеству голосов, если только вы не думаете, что существует всего около 3000 разработчиков Go. Большое количество голосов по другому непредложению просто связано с тем, что проблема попала в топ HN и Reddit — сообщество Go точно не известно отсутствием догмы и / или отрицанием, так что нет - следует удивляться подсчету голосов.

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

Мы запустили tryhard -err="" на нашем самом большом (±163 тыс. строк кода, включая тесты) сервисе — он обнаружил 566 вхождений. Я подозреваю, что на практике это было бы даже больше, поскольку часть кода была написана с учетом if err != nil , поэтому он был разработан с учетом этого (на ум приходит статья Роба Пайка «Ошибки — это ценности» о том, как избежать повторения).

@griesemer Я добавил новый файл в суть. Он был сгенерирован с -err="". Я выборочно проверил, и есть несколько изменений. Сегодня утром я также обновил tryhard, поэтому использовалась и более новая версия.

@griesemer Я думаю, что tryhard был бы полезнее, если бы он мог подсчитывать:

а) количество сайтов вызова, дающих ошибку
б) количество однооператорных обработчиков if err != nil [&& ...] (кандидатов на on err #32611)
в) количество возвращающих что-либо (кандидатов на defer #32676)
г) количество тех, кто возвращает err (кандидаты на try() )
д) количество тех, которые находятся в экспортируемых функциях неосновных пакетов (вероятно ложное срабатывание)

Сравнение общего количества LoC с экземплярами типа return err не имеет контекста, IMO.

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

Вот статистика запуска tryhard по нашей внутренней кодовой базе (только наш код, а не зависимости):

До:

  • 882 .go файлы
  • 352434 местонахождение
  • 329909 непустая ячейка

После трихарда:

  • 2701 замена (в среднем 3,1 замены/файл)
  • 345364 лок (-2,0%)
  • 322838 непустых ячеек (-2,1%)

Редактировать: теперь, когда @griesemer обновил tryhard, чтобы включить сводную статистику, вот еще пара:

  • 39,2% операторов if составляют if <err> != nil
  • 69,6% из них являются кандидатами на try

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

Я также заметил некоторые места, которые tryhard не смог бы преобразовать, но получил бы большую пользу от try. Например, вот некоторый код, который у нас есть для декодирования сообщений в соответствии с простым проводным протоколом (отредактированный для простоты/ясности):

func (req *Request) Decode(r Reader) error {
    typ, err := readByte(r)
    if err != nil {
        return err
    }
    req.Type = typ
    req.Body, err = readString(r)
    if err != nil {
        return unexpected(err)
    }

    req.ID, err = readID(r)
    if err != nil {
        return unexpected(err)
    }
    n, err := binary.ReadUvarint(r)
    if err != nil {
        return unexpected(err)
    }
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err = readID(r)
        if err != nil {
            return unexpected(err)
        }
    }
    return nil
}

// unexpected turns any io.EOF into an io.ErrUnexpectedEOF.
func unexpected(err error) error {
    if err == io.EOF {
        return io.ErrUnexpectedEOF
    }
    return err
}

Без try мы просто написали unexpected в точках возврата, где это необходимо, поскольку нет большого улучшения, если обрабатывать его в одном месте. Однако с помощью try мы можем применить преобразование ошибки unexpected с отсрочкой, а затем значительно сократить код, сделав его более понятным и легким для просмотра:

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

@cespare Фантастический отчет!

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

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

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = wrapEOF(err) }()

    req.Type = try readByte(r)
    req.Body = try readString(r)
    req.ID = try readID(r)

    n := try binary.ReadUvarint(r)
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err = readID(r)
        try err
    }
    return nil
}

*Честно говоря, подсветка кода очень помогла бы, но это выглядит как дешевая помада.

Вы понимаете, что наибольшее преимущество вы получаете в случае действительно плохого кода?

Если вы используете unexpected() или возвращаете ошибку как есть, вы ничего не знаете о своем коде и своем приложении.

try не поможет вам написать лучший код, но может создать еще больше плохого кода.

@cespare Декодер также может быть структурой с типом ошибки внутри, с методами, проверяющими наличие err == nil перед каждой операцией и возвращающими логическое значение ok.

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

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

Если да, то можно взять код, который выглядит так:

a, b, c, err := someFn()
if err != nil {
  return ..., errors.Wrap(err, ...)
}

И превратите его в семантически идентичный[1] код, который выглядит так:

a, b, c, err := someFn()
try(errors.Wrap(err, ...))

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

(Ваш другой комментарий, по-видимому, также основан на примерах или псевдокоде в предложении, а не на сути того, что предлагается)

Я запустил tryhard в своей кодовой базе с 54K LOC, было найдено 1116 экземпляров.
Я видел diff, и я должен сказать, что у меня очень мало конструкций, которые очень выиграли бы от try, потому что почти все мое использование конструкции типа if err != nil представляет собой простой одноуровневый блок, который просто возвращает ошибка с добавленным контекстом. Я думаю, что нашел только пару случаев, когда try действительно изменило структуру кода.

Другими словами, я считаю, что try в его нынешнем виде дает мне:

  • меньше ввода (сокращение колоссальных ~ 30 символов на каждое вхождение, обозначенное «**» ниже)
-       **if err := **json.NewEncoder(&buf).Encode(in)**; err != nil {**
-               **return err**
-       **}**
+       try(json.NewEncoder(&buf).Encode(in))

в то время как это вводит эти проблемы для меня:

  • Еще один способ обработки ошибок
  • отсутствует визуальная подсказка для разделения пути выполнения

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

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

Просто некоторые мысли...

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

Теперь давайте представим, что try реализован, насколько полезной будет эта идиома для
этот вариант использования? Учитывая:

  • Try сохраняет реализацию ближе, а не распределяет по методам.
  • Программисты будут читать и писать код с помощью try гораздо чаще, чем с этим
    специфическая идиома (которая редко используется, кроме как для каждой конкретной задачи). А
    более используемая идиома становится более естественной и читаемой, если нет четкого
    недостатком, которого здесь явно нет, если мы сравним оба с
    открытый ум.

Так что, возможно , эта идиома будет считаться замененной попыткой.

Em ter, 2 июля 2019 г., 18:06, как уведомление на адрес github.com escreveu:

@cespare https://github.com/cespare Декодер также может быть структурой с
тип ошибки внутри него, с методами, проверяющими err == nil до
каждую операцию и возвращает логическое значение ok.

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


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=AAT5WM3YDDRZXVXOLDQXKH3P5O7L5A5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODZCRHXA-54#issuecomment4378 ,
или заглушить тему
https://github.com/notifications/unsubscribe-auth/AAT5WMYXLLO74CIM6H4Y2RLP5O7L5ANCNFSM4HTGCZ7Q
.

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

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

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

Или, возможно, debug.Try с тем же синтаксисом, что и try(), но с добавленной отладочной информацией? Таким образом, try() может быть столь же полезен для кода, использующего старые ошибки, не заставляя вас прибегать к старой обработке ошибок.

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

Изменить: после написания этого мне пришло в голову, что компилятор может определить, какой вариант try() использовать, основываясь на том, используют ли какие-либо операторы отсрочки эту функцию предоставления контекста, аналогичную «восстановлению». Хотя не уверен в сложности этого

@lestrrat Я бы не стал высказывать свое мнение в этом комментарии, но если есть возможность объяснить вам, как «попытка» может повлиять на нас хорошо, то в операторе if можно записать два или более токенов. Таким образом, если вы напишете 200 условий в операторе if, вы сможете сократить количество строк.

if try(foo()) == 1 && try(bar()) == 2 {
  // err
}
n1, err := foo()
if err != nil {
  // err
}
n2, err := bar()
if err != nil {
  // err
}
if n1 == 1 && n2 == 2 {
  // err
}

@mattn , в том-то и дело, что _теоретически_ ты абсолютно прав. Я уверен, что мы можем придумать случаи, когда try подошли бы просто замечательно.

Я просто предоставил данные, которые в реальной жизни, по крайней мере, _я_ почти не встречались с такими конструкциями, которые можно было бы извлечь из перевода, чтобы попробовать в _мом коде_.

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

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

n1 := try(foo())
n2 := try(bar())
if n1 == 1 && n2 == 2 {
   return errors.New(`boo`)
}

так что я все равно буду экономить примерно такое же количество ввода на каждый экземпляр этих n1/n2/....n(n)s

Зачем вообще нужно ключевое слово (или функция)?

Если вызывающий контекст ожидает n+1 значений, то все остается как прежде.

Если вызывающий контекст ожидает n значений, срабатывает поведение try.

(Это особенно полезно в случае n = 1, откуда и возникает весь этот ужасный беспорядок.)

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

@balasanjay Да, ошибки переноса имеют место. Но также у нас есть логирование, разные реакции на разные ошибки (что делать с переменными ошибок, например sql.NoRows ?), читабельный код и так далее. Мы пишем defer f.Close() сразу после открытия файла, чтобы читателям было понятно. Ошибки проверяем сразу по той же причине.

Самое главное, что это предложение нарушает правило « ошибки — это ценности ». Так устроен Go. И это предложение идет прямо против правила.

try(errors.Wrap(err, ...)) — еще один ужасный код, потому что он противоречит как этому предложению, так и текущему дизайну Go.

Я склонен согласиться с @lestrrat
Как обычно foo() и bar() на самом деле:
SomeFunctionWithGoodName(Parm1, Parms2)

тогда предлагаемый синтаксис @mattn будет таким:

if  try(SomeFunctionWithGoodName(Parm1, Parms2)) == 1 && try(package.SomeOtherFunction(Parm1, Parms2,Parm3))) == 2 {


} 

Читабельность обычно будет беспорядок.

рассмотрим возвращаемое значение:
someRetVal, err := SomeFunctionWithGoodName(Parm1, Parms2)
используется чаще, чем просто по сравнению с константами, такими как 1 или 2, и это не становится хуже, но требует функции двойного назначения:

if  a := try(SomeFunctionWithGoodName(Parm1, Parms2)) && b:= try(package.SomeOtherFunction(Parm1, Parms2,Parm3))) {


} 

Что касается всех вариантов использования («насколько мне помог tryhard»):

  1. Я думаю, вы увидите большую разницу между библиотеками и исполняемым файлом, было бы интересно посмотреть на других, получат ли они эту разницу.
  2. мое предложение состоит в том, чтобы сравнивать не %save строк в коде, а количество ошибок в коде и количество рефакторингов.
    (мой взгляд на это был
    $find /path/to/repo -name '*.go' -exec cat {} \; | grep "err :=" | wc -l
    )

@махов

это предложение нарушает правило "ошибки - это ценности"

Не совсем. Ошибки по-прежнему имеют значение в этом предложении. try() просто упрощает поток управления, являясь ярлыком для if err != nil { return ...,err } . Тип error уже является каким-то «особенным», поскольку является встроенным интерфейсным типом. Это предложение просто добавляет встроенную функцию, которая дополняет тип error . Здесь нет ничего экстраординарного.

@ngrilly Упрощает? Как?

func (req *Request) Decode(r Reader) error {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

Как я должен понять, что ошибка была возвращена внутри цикла? Почему он назначен переменной err , а не foo ?
Не проще ли иметь это в виду, а не держать в коде?

@daved

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

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

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

@махов

Упрощение?

Ok. Правильное слово - "сокращение".

try() сокращает наш код, заменяя шаблон if err != nil { return ..., err } вызовом встроенной функции try() .

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

У нас уже есть встроенные функции, такие как append(), которые мы могли бы заменить, написав код «in extenso» самостоятельно каждый раз, когда нам нужно что-то добавить к фрагменту. Но поскольку мы делаем это постоянно, это было интегрировано в язык. try() ничем не отличается.

Как я должен понять, что ошибка была возвращена внутри цикла?

try() в цикле действует точно так же, как try() в остальной части функции вне цикла. Если readID() возвращает ошибку, то функция возвращает ошибку (после оформления if).

Почему он назначен на err var, а не на foo?

Я не вижу переменной foo в вашем примере кода...

@makhov Я думаю, что фрагмент неполный, поскольку возвращаемая ошибка не названа (я быстро перечитал предложение, но не смог понять, является ли имя переменной err именем по умолчанию, если оно не установлено).

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

func (req *Request) Decode(r Reader) (err error) {
    defer func() { err = unexpected(err) }()

    req.Type = try(readByte(r))
    req.Body = try(readString(r))
    req.ID = try(readID(r))

    n := try(binary.ReadUvarint(r))
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i] = try(readID(r))
    }
    return nil
}

@pierrec , может быть, у нас может быть такая функция, как recover() , для извлечения ошибки, если она не указана в именованном параметре?
defer func() {err = unexpected(tryError())}

@makhov Вы можете сделать это более явным:

func (req *Request) Decode(r Reader) error {
    req.Type, err := readByte(r)
        try(err) // or add annotation like try(annotate(err, ...))
    req.Body, err := readString(r)
        try(err)
    req.ID, err := readID(r)
        try(err)

    n, err := binary.ReadUvarint(r)
        try(err)
    req.SubIDs = make([]ID, n)
    for i := range req.SubIDs {
        req.SubIDs[i], err := readID(r)
                try(err)
    }
    return nil
}

@pierrec Хорошо, давайте изменим это:

func (req *Request) Decode(r Reader) error {
        var errOne, errTwo error
    defer func() { err = unexpected(???) }()

    req.Type = try(readByte(r))
    …
}

@reusee И почему это лучше, чем это?

func (req *Request) Decode(r Reader) error {
    req.Type, err := readByte(r)
        if err != nil { return err }
        …
}

В какой момент мы все решили, что краткость лучше удобочитаемости?

@flibustenet Спасибо за понимание проблемы. Выглядит намного лучше, но я все еще не уверен, что для этого небольшого "улучшения" нам нужна сломанная обратная совместимость. Очень раздражает, если у меня есть приложение, которое перестает собираться на новой версии Go:

package main

func main() {
    // ...
   try("a", "b")
    // ...
}

func try(a, b string) {
    // ...
}

@makhov Я согласен, что это нужно уточнить: компилятор выдает ошибки, когда не может определить переменную? Я так и думал.
Может быть, в предложении нужно уточнить этот момент? Или я пропустил это в документе?

@flibustenet да, это один из способов использования try(), но мне кажется, что это не идиоматический способ использования try.

@cespare Из того, что вы написали, кажется, что изменение возвращаемых значений в отсрочке является функцией try , но вы уже можете это сделать.

https://play.golang.com/p/ZMauFmt9ezJ

(Извините, если я неправильно истолковал то, что вы сказали)

@jan-g Относительно https://github.com/golang/go/issues/32437#issuecomment -507961463: идея невидимой обработки ошибок возникала несколько раз. Проблема с таким неявным подходом заключается в том, что добавление возврата ошибки к вызываемой функции может привести к тому, что вызывающая функция будет молча и незаметно вести себя по-другому. Мы абсолютно хотим быть явными, когда проверяются ошибки. Неявный подход также противоречит общему принципу Go, согласно которому все является явным.

@griesemer

Я попробовал tryhand в одном из своих проектов (https://github.com/komuw/meli), и это не внесло никаких изменений.

gobin github.com/griesemer/tryhard
     Installed github.com/griesemer/[email protected] to ~/go/bin/tryhard

``` ударить
~/go/bin/tryhard -err "" -r
0

most of my err handling looks like;
```Go
import "github.com/pkg/errors"

func CreateDockerVolume(volName string) (string, error) {
    volume, err := VolumeCreate(volName)
    if err != nil {
        return "", errors.Wrapf(err, "unable to create docker volume %v", volName)
    }
    return volume.Name, nil
}

@komuw Прежде всего, обязательно укажите аргумент имени файла или каталога для tryhard , как в

tryhard -err="" -r .  // <<< note the dot
tryhard -err="" -r filename

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

func CreateDockerVolume(volName string) (string, error) {
    volume, err := VolumeCreate(volName)
    if err != nil {
        return "", errors.Wrapf(err, "unable to create docker volume %v", volName)
    }
    return volume.Name, nil
}

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

unable to create docker volume: VolumeName: could not create volume VolumeName: actual problem

Ответ заключается в том, что это не так, потому что функция VolumeCreate (из другого репо):

func (cli *Client) VolumeCreate(ctx context.Context, options volumetypes.VolumeCreateBody) (types.Volume, error) {
        var volume types.Volume
        resp, err := cli.post(ctx, "/volumes/create", nil, options, nil)
        defer ensureReaderClosed(resp)
        if err != nil {
                return volume, err
        }
        err = json.NewDecoder(resp.body).Decode(&volume)
        return volume, err
}

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

Возможно, функция VolumeCreate действительно должна украшать свои ошибки. Однако в этом случае мне не ясно, должна ли функция CreateDockerVolume добавлять дополнительные украшения, поскольку она не предоставляет никакой новой информации.

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

То же самое и с другими библиотеками - os.Open вполне может украсить имя файла, причину ошибки и т.д., но
func ReadConfigFile(...
func WriteDataFile(...
и т. д. - вызов os.Open - это фактические неисправные части, которые вы хотели бы видеть, чтобы регистрировать, отслеживать и обрабатывать ваши ошибки - особенно, но не только в рабочей среде.

@neild спасибо.

Не хочу портить эту тему, но...

Возможно, функция VolumeCreate действительно должна украшать свои ошибки.
В таком случае, однако, мне не ясно, что
Функция CreateDockerVolume
следует добавить дополнительное украшение,

Проблема в том, что как автор функции CreateDockerVolume я не могу
знать, украсил ли автор VolumeCreate свои ошибки, поэтому я
мой не надо украшать.
И даже если бы я знал, что они это сделали, они могли бы решить не украшать свои
функция в более поздней версии. И поскольку это изменение не является изменением API, они
выпустит его как патч/дополнительную версию, и теперь моя функция, которая была
зависит от их функции, имея оформленные ошибки, не имеет всех
информация мне нужна.
Так что обычно я украшаю/упаковываю, даже если библиотека, которую я
вызов уже завершён.

У меня возникла мысль, когда я говорил о try с коллегой. Возможно, try следует включать только для стандартной библиотеки в версии 1.14. @crawshaw и @jimmyfrasche сделали краткий обзор некоторых случаев и дали некоторую перспективу, но на самом деле было бы полезно переписать код стандартной библиотеки, используя как можно больше try .

Это дает команде Go время переписать нетривиальный проект с его использованием, а сообщество может получить отчет о том, как это работает. Мы бы знали, как часто он используется, как часто его нужно сочетать с defer , изменяет ли он читабельность кода, насколько полезен tryhard и т. д.

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

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

@jonbodner https://go-review.googlesource.com/c/go/+/182717 дает вам довольно хорошее представление о том, как это может выглядеть.

И забыл сказать: я участвовал в вашем опросе и голосовал за лучшую обработку ошибок, а не за это.

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

@jonbodner https://go-review.googlesource.com/c/go/+/182717 дает вам довольно хорошее представление о том, как это может выглядеть.

Подводить итоги:

  1. 1 строка универсально заменяет 4 строки (2 строки для тех, кто использует if ... { return err } )
  2. Оценка возвращаемых результатов может быть оптимизирована, но только на пути отказа.

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

Стал бы я в качестве мейнтейнера делать что-то подобное со своим собственным кодом? Нет, если только я сам не напишу инструмент замены. Что делает его правильным для репозитория golang/go .

PS Интересная оговорка в CL:

... Some transformations may be incorrect due to the limitations of the tool (see https://github.com/griesemer/tryhard)...

Как и xerrors , как насчет того, чтобы сделать первый шаг, чтобы использовать его в качестве стороннего пакета?

Например, попробуйте использовать пакет ниже.

https://github.com/junpayment/gotry

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

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

===

Кроме того, есть две вещи, которые меня беспокоят.

1. Есть мнение, что строчку можно опустить, но, похоже, здесь не рассматривается оговорка defer(или handler).

Например, когда подробно описана обработка ошибок.

foo, err: = Foo ()
if err! = nil {
  if err.Error () = "AAA" {
    some action for AAA
  } else if err.Error () = "BBB" {
    some action for BBB
  } else if err.Error () = "CCC" {
    some action for CCC
  } else {
    return err
  }
}

Если вы просто замените это на try, это будет следующим образом.

handler: = func (err error) {
  if err.Error () = "AAA" {
    some action for AAA
  } else if err.Error () = "BBB" {
    some action for BBB
  } else if err.Error () = "CCC" {
    some action for CCC
  } else {
    return err
  }
}
foo: = try (Foo (), handler)

2.Могут быть другие плохие пакеты, которые случайно реализовали интерфейс ошибок.

type Bad struct {}
func (bad * Bad) Error () {
  return "i really do not intend to be an error"
}

@junpayment Спасибо за ваш пакет gotry - я думаю, это один из способов получить представление о try , но будет немного раздражает необходимость вводить все Try получается из interface{} в реальном использовании.

По двум вашим вопросам:
1) Я не уверен, к чему ты клонишь. Вы предлагаете try принять обработчик, как в вашем примере? (и как у нас было в более ранней внутренней версии try ?)
2) Меня не слишком беспокоят функции, случайно реализующие интерфейс ошибок. Эта проблема не нова и, насколько нам известно, не вызывает серьезных проблем.

@jonbodner https://go-review.googlesource.com/c/go/+/182717 дает вам довольно хорошее представление о том, как это может выглядеть.

Спасибо за выполнение этого упражнения. Тем не менее, это подтверждает мои подозрения: в самом исходном коде есть много мест, где try() было бы полезно, потому что ошибка просто передается дальше. Однако, как я вижу из экспериментов с tryhard , которые я и другие представили выше, для многих других кодовых баз try() не будет очень полезным, потому что в коде приложения ошибки, как правило, обрабатываются на самом деле, а не просто прошел.

Я думаю, что разработчики Go должны помнить об этом: компилятор go и среда выполнения — это несколько «уникальный» код Go, отличный от кода приложения Go. Поэтому я думаю, что try() следует улучшить, чтобы он также был полезен в других случаях, когда ошибка действительно должна быть обработана, и где обработка ошибок с помощью инструкции отсрочки не очень желательна.

@griesemer

будет немного раздражать необходимость вводить все результаты Try из интерфейса {} в реальном использовании.

Ты прав. Этот метод требует, чтобы вызывающий объект привел тип.

Я не уверен, куда вы идете с этим. Вы предлагаете попробовать принять обработчик, как в вашем примере? (и как у нас было в более ранней внутренней версии try?)

Я допустил ошибку. Должно было быть объяснено с использованием отсрочки, а не обработчика. Мне жаль.

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

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

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

Меня не слишком беспокоят функции, случайно реализующие интерфейс ошибок. Эта проблема не нова и, насколько нам известно, не вызывает серьезных проблем.

Именно это редкий случай.

@beoran Я провел первоначальный анализ Go Corpus (https://github.com/rsc/corpus). Я считаю, что tryhard в его текущем состоянии может устранить 41,7% всех проверок err != nil в корпусе. Если я исключаю шаблон "_test.go", это число увеличивается до 51,1% ( tryhard работает только с функциями, которые возвращают ошибки, и, как правило, не находит многих из них в тестах). Осторожно, относитесь к этим числам с долей скепсиса, я получил знаменатель (то есть количество мест в коде, в котором мы выполняем проверки на err != nil ), используя взломанную версию tryhard , и в идеале мы бы подождали, пока tryhard сами не сообщат эту статистику.

Кроме того, если бы tryhard стал распознавать типы, он теоретически мог бы выполнять такие преобразования:

// Before.
a, err := foo()
if err != nil {
  return 0, nil, errors.Wrapf(err, "some message %v", b)
}

// After.
a, err := foo()
try(errors.Wrapf(err, "some message %v", b))

Это использует преимущества ошибок. Поведение Wrap, возвращающее nil , когда переданный аргумент ошибки равен nil . (github.com/pkg/errors также не уникален в этом отношении, внутренняя библиотека, которую я использую для переноса ошибок, также сохраняет ошибки nil и также будет работать с этим шаблоном, как и большинство библиотек обработки ошибок. post- try , я думаю). Новое поколение библиотек поддержки, вероятно, также будет называть эти помощники по распространению немного по-другому.

Учитывая, что это будет применяться к 50% нетестовых проверок err != nil из коробки, до какой-либо эволюции библиотеки для поддержки шаблона не похоже, что компилятор Go и среда выполнения уникальны, как вы предлагаете .

Про пример с CreateDockerVolume https://github.com/golang/go/issues/32437#issuecomment -508199875
Я нашел точно такое же использование. В lib я оборачиваю ошибку контекстом при каждой ошибке, при использовании lib я хотел бы использовать try и добавить контекст в defer для всей функции.

Я попытался имитировать это, добавив функцию обработчика ошибок в начале, она работает нормально:

func MyLib() error {
    return errors.New("Error from my lib")
}
func MyOtherLib() error {
    return errors.New("Error from my otherLib")
}

func Caller(a, b int) error {
    eh := func(err error) error {
        return fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, err)
    }

    err := MyLib()
    if err != nil {
        return eh(err)
    }

    err = MyOtherLib()
    if err != nil {
        return eh(err)
    }

    return nil
}

Это будет выглядеть хорошо и идиоматично с try+defer

func Caller(a, b int) (err error) {
    defer fmt.Errorf("From Caller with %d and %d i found this error: %v", a, b, &err)

    try(MyLib())
    try(MyOtherLib())

    return nil
}

@griesemer

В настоящее время в проектной документации есть следующие утверждения:

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

Это означает, что эта программа будет печатать 1 вместо 0: https://play.golang.org/p/KenN56iNVg7.

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

Учитывая, что это свойство голых возвратов (нелокальность) обычно не нравится, что вы думаете о том, чтобы try всегда возвращал нулевые значения аргументов без ошибок (если он вообще возвращается)?

Некоторые соображения:

Это может привести к тому, что некоторые шаблоны, включающие использование именованных возвращаемых значений, не смогут использовать try . Например, для реализаций io.Writer , которые должны возвращать количество записанных байтов даже в ситуации частичной записи. Тем не менее, кажется, что try в любом случае подвержен ошибкам в этом случае (например, n += try(wrappedWriter.Write(...)) не устанавливает правильное число n в случае возврата ошибки). Мне кажется нормальным, что try станет непригодным для таких вариантов использования, поскольку сценарии, в которых нам нужны и значения, и ошибка, по моему опыту, довольно редки.

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

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

@jonbodner https://go-review.googlesource.com/c/go/+/182717 дает вам довольно хорошее представление о том, как это может выглядеть.

Спасибо за выполнение этого упражнения. Тем не менее, это подтверждает мои подозрения: в самом исходном коде есть много мест, где try() было бы полезно, потому что ошибка просто передается дальше. Однако, как я вижу из экспериментов с tryhard , которые я и другие представили выше, для многих других кодовых баз try() не будет очень полезным, потому что в коде приложения ошибки, как правило, обрабатываются на самом деле, а не просто прошел.

Я бы интерпретировал это иначе.

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

Для меня есть 2 шаблона, которые я использовал в коде для обработки ошибок.

  1. использовать паники внутри пакета и восстанавливать панику и возвращать результат ошибки в нескольких экспортируемых методах
  2. выборочно использовать отложенный обработчик в некоторых методах, чтобы я мог украшать ошибки богатой информацией о ПК/номере строки стека и дополнительным контекстом

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

И последнее замечание: украшать ошибки в каждой строке вашего кода может быть слишком сложно. В некоторых местах есть смысл украшать ошибки, а в некоторых нет. Поскольку раньше у нас не было отличных руководств, люди решили, что имеет смысл всегда декорировать ошибки. Но это может не иметь большого значения, чтобы всегда украшать каждый раз, когда файл не открывается, поскольку в пакете может быть достаточно просто иметь ошибку «невозможно открыть файл: conf.json», а не: «невозможно чтобы получить имя пользователя: невозможно получить соединение с базой данных: невозможно загрузить системный файл: невозможно открыть файл: conf.json".

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

  • ошибка будет простой, например, «невозможно открыть файл: conf.json»
  • кадр ошибки может быть присоединен, включая контекст: GetUserName --> GetConnection --> LoadSystemFile.
  • Если это добавляет к контексту, вы можете несколько обернуть эту ошибку, например, MyAppError {error}

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

  1. уменьшите шаблон if err != nil { return err } для мест, где имеет смысл распространять ошибку, чтобы она была обработана выше по стеку
  2. Разрешить упрощенное использование возвращаемых значений, где err == nil
  3. позволить позже расширить решение, чтобы, например, добавить больше ошибок на сайт, перейти к обработчику ошибок, использовать goto вместо семантики возврата и т. д.
  4. Разрешить обработку ошибок, чтобы не загромождать логику кодовой базы, то есть отложить ее в сторону с помощью своего рода обработчика ошибок.

У многих людей все еще есть 1). Многие люди работали вокруг 1), потому что раньше не существовало лучших руководств. Но это не значит, что после того, как они начнут его использовать, их отрицательная реакция не изменится на более положительную.

Многие люди могут использовать 2). Могут возникнуть разногласия по поводу того, насколько сильно, но я привел пример, где это значительно упрощает мой код.

var u user = try(db.LoadUser(try(strconv.ParseInt(stringId)))

В java, где исключения являются нормой, у нас было бы:

User u = db.LoadUser(Integer.parseInt(stringId)))

Никто не посмотрит на этот код и не скажет, что мы должны сделать это в 2 строки, т.е.

int id = Integer.parseInt(stringId)
User u = db.LoadUser(id))

Мы не должны делать этого здесь, в соответствии с правилом, что try НЕ ДОЛЖЕН вызываться встроенным и всегда ДОЛЖЕН быть на отдельной строке .

Кроме того, сегодня большая часть кода будет делать такие вещи, как:

var u user
var err error
var id int
id, err = strconv.ParseInt(stringId)
if err != nil {
  return u, errors.Wrap("cannot load userid from string: %s: %v", stringId, err)
}
u, err = db.LoadUser(id)
if err != nil {
  return u, errors.Wrap("cannot load user given user id: %d: %v", id, err)
}
// now work with u

Теперь кто-то, кто читает это, должен проанализировать эти 10 строк, которые в java были бы 1 строкой и которые могли бы быть 1 строкой с предложением здесь. Я визуально должен мысленно попытаться увидеть, какие строки здесь действительно уместны, когда я читаю этот код. Шаблон делает этот код трудным для чтения и вкрапления.

Я помню, как в прошлой жизни работал над/с аспектно-ориентированным программированием в java. Там целью было

Это позволяет добавлять в программу поведение, не являющееся центральным для бизнес-логики (например, ведение журнала), не загромождая код, являющийся ядром функциональности. (цитата из википедии https://en.wikipedia.org/wiki/Аспектно-ориентированное_программирование).
Обработка ошибок не имеет решающего значения для бизнес-логики, но важна для корректности. Идея та же - мы не должны загромождать наш код вещами, не относящимися к бизнес-логике, потому что " но обработка ошибок очень важна ". Да, и да, мы можем отложить это в сторону.

Что касается 4), во многих предложениях предлагались обработчики ошибок, которые представляют собой дополнительный код, который обрабатывает ошибки, но не загромождает бизнес-логику. В первоначальном предложении есть ключевое слово handle для него, и люди предлагали другие вещи. В этом предложении говорится, что мы можем использовать для этого механизм отсрочки и просто ускорить то, что раньше было его ахиллесовой пятой. Я знаю - я много раз поднимал шум о работе механизма отсрочки команде go.

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

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

Объедините это с Error Formatter, и теперь мы можем детально изучить кадр ошибки и саму ошибку и красиво отформатировать сообщение для пользователей, без сложного для чтения стиля a: b: c: d: e: underlying error , который сделали многие люди, а мы нет. были отличные рекомендации для.

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

Наконец, учитывая 3) выше, трудно использовать ключевое слово для решения этой проблемы. По определению ключевое слово не позволяет расширению в будущем передавать обработчик по имени, разрешать оформление ошибки на месте или поддерживать семантику перехода (вместо семантики возврата). С ключевым словом мы должны сначала иметь в виду полное решение. И ключевое слово не имеет обратной совместимости. Команда go заявила, когда Go 2 запускалась, что они хотели максимально сохранить обратную совместимость. Функция try поддерживает это, и если позже мы увидим, что расширение не требуется, простое исправление может легко изменить код, заменив функцию try на ключевое слово.

Мои 2 цента снова!

04.07.19 Санджай Менакуру, [email protected] , написал:

@griesemer

[ ... ]
Как мне указали в Твиттере, это заставляет try вести себя как голый
return, где возвращаемые значения являются неявными; выяснить, что
возвращаются фактические значения, может потребоваться посмотреть код на
значительное расстояние от вызова до самого try .

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

Необработанные возвраты разрешены только в том случае, если возвращаемые аргументы названы. Это
кажется, что try следует другому правилу?

Мне нравится общая идея повторного использования defer для решения проблемы. Однако мне интересно, является ли ключевое слово try правильным способом сделать это. Что, если бы мы могли повторно использовать уже существующий шаблон. То, что все уже знают из импорта:

Явная обработка

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

Явное игнорирование

res, _ := doSomething()

Отложенная обработка

Поведение аналогично тому, что собирается делать try .

res, . := doSomething()

@piotrkowalczuk
Это может быть более приятный синтаксис для него, но я не знаю, насколько легко было бы адаптировать Go, чтобы сделать это законным, как в Go, так и в средствах подсветки синтаксиса.

@balasanjay (и @lootch): Согласно вашему комментарию здесь , да, программа https://play.golang.org/p/KenN56iNVg7 напечатает 1.

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

[изменить: обратите внимание, что этот вопрос (о том, следует ли очищать результаты без ошибок при обнаружении ошибки) не относится к предложению try . Любая из предложенных альтернатив, не требующих явного указания return , должна будет отвечать на тот же вопрос.]

Что касается вашего примера с писателем n += try(wrappedWriter.Write(...)) : Да, в ситуации, когда вам нужно увеличить n даже в случае ошибки, нельзя использовать try - даже если try не обнуляет безошибочные значения результата. Это связано с тем, что try возвращает что-либо только в том случае, если нет ошибки: try ведет себя точно как функция (но функция, которая может возвращать не вызывающему объекту, а вызывающему объекту). См. использование временных объектов в реализации try .

Но в таких случаях, как ваш пример, также нужно быть осторожным с оператором if и обязательно включать возвращаемое количество байтов в n .

Но, возможно, я неправильно понимаю ваше беспокойство.

@griesemer : я предлагаю, чтобы другие возвращаемые значения были равны их нулевым значениям, потому что тогда становится ясно, что будет делать try , просто проверив сайт вызова. Он либо а) ничего не сделает, либо б) вернется из функции с нулевыми значениями и аргументом для попытки.

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

Это та же проблема с голым возвратом (необходимо сканировать всю функцию, чтобы увидеть, какое значение возвращается), и, предположительно, это было причиной регистрации https://github.com/golang/go/issues/21291. Для меня это означает, что try в большой функции с именованными возвращаемыми значениями не рекомендуется использовать на тех же основаниях, что и голые возвраты (https://github.com/golang/go/wiki/CodeReviewComments). #названные-параметры-результата). Вместо этого я предлагаю указать try , чтобы всегда возвращать нулевые значения аргумента, не являющегося ошибкой.

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

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

я бы также призвал команду go сделать try встроенным вариативным, если это легко сделать

try(outf.Seek(linkstart, 0))
try(io.Copy(outf, exef))

становится

try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))

следующей подробной вещью могут быть эти последовательные вызовы try .

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

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

Я думаю, что есть лучшее решение, чем try , и я лучше не тороплюсь и найду это решение, чем остановлюсь на этом.

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

На мой взгляд, я хочу попробовать блочный код, теперь try как функция обработки ошибок

Читая это обсуждение (и обсуждения на Reddit), я не всегда чувствовал, что все согласны.

Поэтому я написал небольшой пост в блоге, демонстрирующий, как можно использовать try : https://faiface.github.io/post/how-to-use-try/.

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

Если я пропустил что-то важное, пожалуйста, дайте мне знать!

@faiface Я почти уверен, что ты сможешь заменить

if err != nil {
    return resps, err
}

с try(err) .

В остальном - отличная статья!

@DmitriyMV Верно! Но, пожалуй, оставлю как есть, чтобы был хотя бы один пример классического if err != nil , пусть и не очень удачного.

У меня две проблемы:

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

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

Более мелкая стилистическая проблема заключается в том, что, к сожалению, много строк кода теперь будет заключено в try(actualThing()) . Я могу себе представить, что большинство строк кодовой базы завернуты в try() . Это прискорбно.

Я думаю, что эти проблемы будут решены с помощью настройки:

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

check() будет вести себя так же, как try() , но в общем откажется от передачи значений, возвращаемых функцией, и вместо этого предоставит возможность добавлять контекст. Это все равно вызовет возврат.

Это сохранит многие преимущества try() :

  • это встроенный
  • следует за существующим потоком управления WRT, чтобы отложить
  • это хорошо согласуется с существующей практикой добавления контекста к ошибкам
  • он согласуется с текущими предложениями и библиотеками для переноса ошибок, такими как errors.Wrap(err, "context message")
  • это приводит к чистому сайту вызова: в строке a, b, err := myFunc() нет шаблона
  • описание ошибок с помощью defer fmt.HandleError(&err, "msg") по-прежнему возможно, но это не нужно поощрять.
  • сигнатура check немного проще, потому что ей не нужно возвращать произвольное количество аргументов из функции, которую она обертывает.

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

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

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

Итак, как насчет того, чтобы вместо этого сделать что-то вроде общего макроса C, чтобы открыть для большей гибкости?

Так:

define returnIf(err error, desc string, args ...interface{}) {
    if (err != nil) {
        return fmt.Errorf("%s: %s: %+v", desc, err, args)
    }
}

func CopyFile(src, dst string) error {
    r, err := os.Open(src)
    :returnIf(err, "Error opening src", src)
    defer r.Close()

    w, err := os.Create(dst)
    :returnIf(err, "Error Creating dst", dst)
    defer w.Close()

    ...
}

По сути, returnIf будет заменен/встроен на то, что определено выше. Гибкость заключается в том, что вам решать, что он делает. Отладка этого может быть немного странной, если только редактор не заменяет его в редакторе каким-то хорошим способом. Это также делает его менее волшебным, так как вы можете четко прочитать определение. Кроме того, это позволяет вам иметь одну строку, которая потенциально может вернуться в случае ошибки. И может иметь разные сообщения об ошибках в зависимости от того, где это произошло (контекст).

Изменить: также добавлено двоеточие перед макросом, чтобы предположить, что, возможно, это можно сделать, чтобы уточнить, что это макрос, а не вызов функции.

@nvictor

я бы также призвал команду go сделать try встроенным вариативным

Что вернет try(foo(), bar()) , если foo и bar не вернут одно и то же?

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

Итак, как насчет того, чтобы вместо этого сделать что-то вроде общего макроса C, чтобы открыть для большей гибкости?

@Chillance , ИМХО, я думаю, что гигиеничная макросистема, такая как Rust (и многие другие языки), даст людям возможность поиграть с такими идеями, как try или дженерики, а затем, после приобретения опыта, лучшие идеи могут стать часть языка и библиотек. Но я также думаю, что очень мало шансов, что такая штука будет добавлена ​​в Go.

@jonbodner в настоящее время есть предложение добавить гигиенические макросы в Go. Пока нет предлагаемого синтаксиса или чего-то еще, однако не так много _против_ идеи добавления гигиенических макросов. №32620

@Allenyn , относительно более раннего предложения @buchanae , которое вы только что процитировали :

a, b, err := myFunc()
check(err, "calling myFunc on %v and %v", a, b)

Судя по тому, что я видел в обсуждении, я предполагаю, что семантика fmt во встроенной функции маловероятна. (См., например, ответ @josharian ).

Тем не менее, на самом деле это не нужно, в том числе потому, что разрешение функции обработчика может обойти семантику fmt непосредственно во встроенной функции. Один из таких подходов был предложен @eihigh примерно в первый день обсуждения здесь, что похоже по духу на предложение @buchanae , и в котором предлагалось настроить встроенную функцию try , чтобы вместо этого иметь следующую подпись:

func try(error, optional func(error) error)

Поскольку эта альтернатива try ничего не возвращает, эта подпись подразумевает:

  • он не может быть вложен в другой вызов функции
  • он должен быть в начале строки

Я не хочу, чтобы это имя вызывало отказ от велосипеда, но эта форма try может лучше читаться с альтернативным именем, таким как check . Можно было бы представить стандартные вспомогательные библиотеки, которые могли бы сделать необязательную аннотацию на месте удобной, в то время как defer мог бы оставаться опцией для унифицированной аннотации, когда это необходимо.

Были некоторые связанные предложения, созданные позже в #32811 ( catch как встроенная функция) и #32611 (ключевое слово on для разрешения on err, <statement> ). Это может быть подходящим местом для дальнейшего обсуждения, или для того, чтобы выразить одобрение или отрицание, или предложить возможные корректировки этих предложений.

@jonbodner в настоящее время есть предложение добавить гигиенические макросы в Go. Пока нет предлагаемого синтаксиса или чего-то еще, однако не так много _против_ идеи добавления гигиенических макросов. №32620

Это здорово, что есть предложение, но я подозреваю, что основная команда Go не собирается добавлять макросы. Однако я был бы рад ошибиться в этом, поскольку это положило бы конец всем спорам об изменениях, которые в настоящее время требуют модификации ядра языка. Цитируя известную марионетку: «Делай. Или не делай. Нет никакой попытки».

@jonbodner Я не думаю, что добавление гигиенических макросов положит конец спору. Наоборот. Распространенной критикой является то, что try «скрывает» возврат. Макросы с этой точки зрения были бы строго хуже, потому что в макросе было бы возможно все. И даже если бы Go разрешил определяемые пользователем гигиенические макросы, нам все равно пришлось бы спорить, должен ли try быть встроенным макросом, предварительно объявленным в блоке юниверса, или нет. Было бы логично, чтобы противники try были еще больше против гигиенических макросов ;-)

@ngrilly есть несколько способов убедиться, что макросы выделяются и их легко увидеть. Rust делает это так, что макросы всегда начинаются с ! (т.е. try!(...) и println!(...) ).

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

@thepudds Я согласен, что добавление необязательного параметра типа func(error) error может быть полезным (эта возможность обсуждается в предложении с некоторыми проблемами, которые необходимо решить), но я не вижу смысла в try ничего не возвращает. try , предложенный командой Go, является более общим инструментом.

@deanveloper Да, ! в конце макросов в Rust — это умно. Напоминает экспортируемые идентификаторы, начинающиеся с заглавной буквы в Go :-)

Я бы согласился иметь гигиенические макросы в Go, если и только если мы сможем сохранить скорость компиляции и решить сложные проблемы, связанные с инструментами (инструменты рефакторинга должны будут расширить макросы, чтобы понять семантику кода, но должны генерировать код с нерасширенными макросами) . Это тяжело. А пока, может быть, try можно переименовать в try! ? ;-)

Облегченная идея: если тело конструкции if/for содержит один оператор, фигурные скобки не нужны, если этот оператор находится на той же строке, что и if или for . Пример:

fd, err := os.Open("foo")
if err != nil return err

Обратите внимание, что в настоящее время тип error является обычным типом интерфейса. Компилятор не считает это чем-то особенным. try меняет это. Если компилятору разрешено рассматривать error как особое, я бы предпочел /bin/sh , вдохновленный || :

fd, err := os.Open("foo") || return err

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

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

@bakul В if err != nil return err как узнать, где заканчивается выражение err != nil и где начинается выражение return err ? Ваша идея будет серьезным изменением грамматики языка, намного большим, чем то, что предлагается с try .

Ваша вторая идея выглядит как catch |err| return err в Zig . Лично я не "отшатываюсь от ужаса", и я бы сказал, почему бы и нет? Но следует отметить, что Zig также имеет ключевое слово try , которое является сокращением для catch |err| return err и почти эквивалентно тому, что команда Go предлагает здесь в качестве встроенной функции. Так может быть, достаточно try и нам не нужно ключевое слово catch ? ;-)

@ngrilly , в настоящее время <expr> <statement> недействительно, поэтому я не думаю, что это изменение сделает грамматику более неоднозначной, но может быть немного более хрупкой.

Это будет генерировать точно такой же код, как и предложение try, но а) возврат здесь явный, б) вложенность невозможна, как в случае с попыткой, и в) это будет знакомый синтаксис для пользователей оболочки (которых намного больше, чем пользователей zig). Здесь нет catch .

Я привел это как альтернативу, но, честно говоря, меня вполне устраивают все, что решают разработчики основного языка go.

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

$ tryhard $HOME/go/src
...
--- stats ---
  55620 (100.0% of   55620) function declarations
  14936 ( 26.9% of   55620) functions returning an error
 116539 (100.0% of  116539) statements
  27327 ( 23.4% of  116539) if statements
   7636 ( 27.9% of   27327) if <err> != nil statements
    119 (  1.6% of    7636) <err> name is different from "err" (use -l flag to list file positions)
   6037 ( 79.1% of    7636) return ..., <err> blocks in if <err> != nil statements
   1599 ( 20.9% of    7636) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
     17 (  0.2% of    7636) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
   5907 ( 77.4% of    7636) try candidates (use -l flag to list file positions)

Еще многое предстоит сделать, но это дает более ясную картину. В частности, 28% всех операторов if предназначены для проверки ошибок; это подтверждает наличие значительного количества повторяющегося кода. Из этих проверок ошибок 77% поддаются try .

$ попробуй .
--- статистика ---
2930 (100,0% от 2930) объявлений функций
1408 (48,1% от 2930) функций возвращают ошибку
10497 (100,0% из 10497) утверждений
2265 ( 21,6% из 10497) операторов if
1383 (61,1% от 2265), если!= нулевые операторы
0 (0,0% от 1383)имя отличается от "err" (используйте флаг -l
чтобы перечислить позиции файла)
645 (46,6% от 1383) возврат...,блокируется, если!= ноль
заявления
738 ( 53,4% от 1383) более сложный обработчик ошибок в if!= ноль
заявления; запретить использование try (используйте флаг -l для отображения позиций в файле)
1 ( 0,1% от 1383) непустое else блокируется, если!= ноль
заявления; запретить использование try (используйте флаг -l для отображения позиций в файле)
638 ( 46,1% из 1383) попробовать кандидатов (используйте флаг -l для отображения файла
позиции)
$ идти поставщик модов
$ торговец
--- статистика ---
37757 (100,0% от 37757) объявлений функций
12557 ( 33,3% от 37757) функций, возвращающих ошибку
88919 (100,0% от 88919) утверждений
20143 ( 22,7% из 88919) if утверждений
6555 (32,5% от 20143) если!= нулевые операторы
109 (1,7% от 6555)имя отличается от "err" (используйте флаг -l
чтобы перечислить позиции файла)
5545 (84,6% от 6555) возврат...,блокируется, если!= ноль
заявления
1010 (15,4% от 6555) более сложный обработчик ошибок в if!= ноль
заявления; запретить использование try (используйте флаг -l для отображения позиций в файле)
12 (0,2% от 6555) непустое else блокируется, если!= ноль
заявления; запретить использование try (используйте флаг -l для отображения позиций в файле)
5427 ( 82,8% из 6555) попробовать кандидатов (используйте флаг -l для отображения списка файлов
позиции)

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

@mirtchovski

$ tryhard .
--- stats ---
   2930 (100.0% of    2930) function declarations
   1408 ( 48.1% of    2930) functions returning an error
  10497 (100.0% of   10497) statements
   2265 ( 21.6% of   10497) if statements
   1383 ( 61.1% of    2265) if <err> != nil statements
      0 (  0.0% of    1383) <err> name is different from "err" (use -l flag to list file positions)
    645 ( 46.6% of    1383) return ..., <err> blocks in if <err> != nil statements
    738 ( 53.4% of    1383) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
      1 (  0.1% of    1383) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
    638 ( 46.1% of    1383) try candidates (use -l flag to list file
positions)
$ go mod vendor
$ tryhard vendor
--- stats ---
  37757 (100.0% of   37757) function declarations
  12557 ( 33.3% of   37757) functions returning an error
  88919 (100.0% of   88919) statements
  20143 ( 22.7% of   88919) if statements
   6555 ( 32.5% of   20143) if <err> != nil statements
    109 (  1.7% of    6555) <err> name is different from "err" (use -l flag to list file positions)
   5545 ( 84.6% of    6555) return ..., <err> blocks in if <err> != nil statements
   1010 ( 15.4% of    6555) more complex error handler in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
     12 (  0.2% of    6555) non-empty else blocks in if <err> != nil statements; prevent use of try (use -l flag to list file positions)
   5427 ( 82.8% of    6555) try candidates (use -l flag to list file
positions)
$

@av86743 ,

извините, не учел, что «Ответы по электронной почте не поддерживают Markdown»

Некоторые люди отмечают, что нечестно учитывать сторонний код в результатах tryhard . Например, в стандартной библиотеке вендорный код включает в себя сгенерированные пакеты syscall , которые содержат много проверок ошибок и могут искажать общую картину. Новейшая версия tryhard теперь по умолчанию исключает пути к файлам, содержащие "vendor" (этим также можно управлять с помощью нового флага -ignore ). Применяется к стандартной библиотеке в наконечнике:

tryhard $HOME/go/src
/Users/gri/go/src/cmd/go/testdata/src/badpkg/x.go:1:1: expected 'package', found pkg
/Users/gri/go/src/cmd/go/testdata/src/notest/hello.go:6:1: expected declaration, found Hello
/Users/gri/go/src/cmd/go/testdata/src/syntaxerror/x_test.go:3:11: expected identifier
--- stats ---
  45424 (100.0% of   45424) func declarations
   8346 ( 18.4% of   45424) func declarations returning an error
  71401 (100.0% of   71401) statements
  16666 ( 23.3% of   71401) if statements
   4812 ( 28.9% of   16666) if <err> != nil statements
     86 (  1.8% of    4812) <err> name is different from "err" (-l flag lists details)
   3463 ( 72.0% of    4812) return ..., <err> blocks in if <err> != nil statements
   1349 ( 28.0% of    4812) complex error handler in if <err> != nil statements; cannot use try (-l flag lists details)
     17 (  0.4% of    4812) non-empty else blocks in if <err> != nil statements; cannot use try (-l flag lists details)
   3345 ( 69.5% of    4812) try candidates (-l flag lists details)

Теперь 29% (28,9%) всех операторов if предназначены для проверки ошибок (то есть немного больше, чем раньше), а из них около 70% являются кандидатами на try (немного больше). меньше, чем раньше).

Изменение https://golang.org/cl/185177 упоминает эту проблему: src: apply tryhard -err="" -ignore="vendor" -r $GOROOT/src

@griesemer вы посчитали «сложные обработчики ошибок», но не «обработчики ошибок с одним оператором».

Если большинство «сложных» обработчиков представляют собой один оператор, то on err #32611 даст примерно такую ​​же экономию шаблонов, как и try() — 2 строки против 3 строк x 70%. И on err добавляет преимущество последовательного шаблона для подавляющего большинства ошибок.

@nvictor

try — это чистое и понятное решение конкретной проблемы, которую он пытается решить:
многословие в обработке ошибок.

Многословие в обработке ошибок — это не проблема, это сильная сторона Go.

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

Ваш _opt-in_ во время написания является _must_ для всех читателей, включая вас в будущем.

очевидные преимущества

Если запутанный поток управления можно назвать «преимуществом», то да.

try , ради привычек экспатов, использующих Java и C++, вводит магию, которую должны понимать все Гоферы. В то же время экономя меньшинство нескольких строк, чтобы написать в нескольких местах (как показали прогоны tryhard ).

Я бы сказал, что мой более простой макрос onErr избавит от написания большего количества строк, и для большинства:

x, err = fa()
onErr break

r, err := fb(x)
onErr return 0, nil, err

if r, err := fc(x); onErr && triesleft > 0 {
  triesleft--
  continue retry
}

_ (обратите внимание, что я нахожусь в лагере «оставьте if err!= nil покое», и вышеприведенное встречное предложение было опубликовано, чтобы показать более простое решение, которое может осчастливить больше нытиков.)_

Редактировать:

я бы также призвал команду go сделать try встроенным вариативным, если это легко сделать
try(outf.Seek(linkstart, 0)), io.Copy(outf, exef)))

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

Я был неправ. На самом деле переменная try была бы намного лучше, чем гнезда, поскольку мы могли бы записать ее построчно:

try( outf.Seek(linkstart, 0),
 io.Copy(outf, exef),
)

и вернуть try(…) после первой ошибки.

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

Я бы предложил что-то вроде Эликсира с заявлением: https://www.openmymind.net/Elixirs-With-Statement/

Что-то вроде этого ниже в golang:

switch a, b, err1 := go_func_01(),
       apple, banana, err2 := go_func_02(),
       fans, dissman, err3 := go_func_03()
{
   normal_func()
else
   err1 -> handle_err1()
   err2 -> handle_err2()
   _ -> handle_other_errs()
}

Является ли это своего рода нарушением «Go предпочитает меньше функций» и «добавление функций в Go не сделает его лучше, а больше»? Я не уверена...

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

if err != nil {
    return …, err
}

И уж точно не хочу читать код, написанный другими с помощью try ... Причина может быть двоякой:

  1. иногда сложно догадаться, что внутри с первого взгляда
  2. try могут быть вложенными, т. е try( ... try( ... try ( ... ) ... ) ... ) трудно читаемы

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

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

Разве производительность отсрочки не является проблемой с этим предлагаемым решением? Я сравнивал функции с отсрочкой и без нее, и было отмечено значительное влияние на производительность. Я только что погуглил кого-то, кто провел такой тест, и нашел 16-кратную стоимость. Я не помню, чтобы мой был таким плохим, но в 4 раза медленнее звонит в колокол. Как что-то, что может удвоить или ухудшить время выполнения множества функций, может считаться жизнеспособным общим решением?

Производительность @eric-hawthorne Defers — это отдельная тема. Try по своей сути не требует отсрочки и не удаляет возможность обработки ошибок без нее.

@fabian-f Но это предложение может способствовать замене кода, в котором кто-то украшает ошибки отдельно для каждой встроенной ошибки в рамках блока if err != nil. Это будет существенной разницей в производительности.

@eric-hawthorne Цитируя дизайн-документ:

В: Не будет ли использование отложенного переноса ошибок медленным?

О: В настоящее время оператор отсрочки относительно дорог по сравнению с обычным потоком управления. Тем не менее, мы считаем, что можно сделать общие варианты использования отсрочки для обработки ошибок сопоставимыми по производительности с текущим «ручным» подходом. См. также CL 171758, который, как ожидается, улучшит производительность отсрочки примерно на 30%.

Вот интересное выступление Rust, связанное с Reddit. Самая актуальная часть начинается с 47:55.

Я попробовал tryhard в своем крупнейшем публичном репозитории https://github.com/dpinela/mflg и получил следующее:

--- stats ---
    309 (100.0% of     309) func declarations
     36 ( 11.7% of     309) func declarations returning an error
    305 (100.0% of     305) statements
     73 ( 23.9% of     305) if statements
     29 ( 39.7% of      73) if <err> != nil statements
      0 (  0.0% of      29) <err> name is different from "err"
     19 ( 65.5% of      29) return ..., <err> blocks in if <err> != nil statements
     10 ( 34.5% of      29) complex error handler in if <err> != nil statements; cannot use try
      0 (  0.0% of      29) non-empty else blocks in if <err> != nil statements; cannot use try
     15 ( 51.7% of      29) try candidates

Большая часть кода в этом репозитории управляет внутренним состоянием редактора и не выполняет каких-либо операций ввода-вывода, поэтому имеет мало проверок на ошибки, поэтому места, где можно использовать try, относительно ограничены. Я пошел дальше и вручную переписал код, чтобы использовать try там, где это возможно; git diff --stat возвращает следующее:

 application.go                  | 42 +++++++++++-------------------------------
 internal/atomicwrite/write.go   | 35 ++++++++++++++---------------------
 internal/clipboard/clipboard.go | 17 +++--------------
 internal/config/config.go       | 15 +++++++--------
 internal/termesc/term.go        |  5 +----
 render.go                       |  8 ++------
 6 files changed, 38 insertions(+), 84 deletions(-)

(Полная разница здесь .)

Из 10 обработчиков, о которых tryhard сообщает как о «сложных», 5 имеют ложноотрицательные результаты в файле internal/atomicwrite/write.go; они использовали pkg/errors.WithMessage для переноса ошибки. Обёртка была одинаковой для всех, поэтому я переписал эту функцию, чтобы использовать обработчики try и deferred. Я закончил с этим diff (+14, -21 строк):

@@ -20,21 +20,20 @@ const (
 // The file is created with mode 0644 if it doesn't already exist; if it does, its permissions will be
 // preserved if possible.
 // If some of the directories on the path don't already exist, they are created with mode 0755.
-func Write(filename string, contentWriter func(io.Writer) error) error {
+func Write(filename string, contentWriter func(io.Writer) error) (err error) {
+       defer func() { err = errors.WithMessage(err, errString(filename)) }()
+
        dir := filepath.Dir(filename)
-       if err := os.MkdirAll(dir, defaultDirPerms); err != nil {
-               return errors.WithMessage(err, errString(filename))
-       }
-       tf, err := ioutil.TempFile(dir, "mflg-atomic-write")
-       if err != nil {
-               return errors.WithMessage(err, errString(filename))
-       }
+       try(os.MkdirAll(dir, defaultDirPerms))
+       tf := try(ioutil.TempFile(dir, "mflg-atomic-write"))
        name := tf.Name()
-       if err = contentWriter(tf); err != nil {
-               os.Remove(name)
-               tf.Close()
-               return errors.WithMessage(err, errString(filename))
-       }
+       defer func() {
+               if err != nil {
+                       tf.Close()
+                       os.Remove(name)
+               }
+       }()
+       try(contentWriter(tf))
        // Keep existing file's permissions, when possible. This may race with a chmod() on the file.
        perms := defaultPerms
        if info, err := os.Stat(filename); err == nil {
@@ -42,14 +41,8 @@ func Write(filename string, contentWriter func(io.Writer) error) error {
        }
        // It's better to save a file with the default TempFile permissions than not save at all, so if this fails we just carry on.
        tf.Chmod(perms)
-       if err = tf.Close(); err != nil {
-               os.Remove(name)
-               return errors.WithMessage(err, errString(filename))
-       }
-       if err = os.Rename(name, filename); err != nil {
-               os.Remove(name)
-               return errors.WithMessage(err, errString(filename))
-       }
+       try(tf.Close())
+       try(os.Rename(name, filename))
        return nil
 }

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

Два других «сложных» обработчика были в реализациях ReadFrom и WriteTo:

var line string
line, err = br.ReadString('\n')
b.lines = append(b.lines, line)
if err != nil {
  if err == io.EOF {
    err = nil
  }
  return
}
func (b *Buffer) WriteTo(w io.Writer) (int64, error) {
    var n int64
    for _, line := range b.lines {
        nw, err := w.Write([]byte(line))
        n += int64(nw)
        if err != nil {
            return n, err
        }
    }
    return n, nil
}

Эти действительно не поддавались попытке, поэтому я оставил их в покое.

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

n, err := strconv.ParseInt(s[1:], 16, 32)
if err != nil {
    return Color{}, errors.WithMessage(err, fmt.Sprintf("color: parse %q", s))
}

Последний был в функции для загрузки файла конфигурации, которая всегда возвращает (ненулевую) конфигурацию, даже если есть ошибка. У него была только одна проверка на ошибку, поэтому он не получил большой пользы от try:

-func Load() (*Config, error) {
-       c := Config{
+func Load() (c *Config, err error) {
+       defer func() { err = errors.WithMessage(err, "error loading config file") }()
+
+       c = &Config{
                TabWidth:    4,
                ScrollSpeed: 1,
                Lang:        make(map[string]LangConfig),
        }
-       f, err := basedir.Config.Open(filepath.Join("mflg", "config.toml"))
-       if err != nil {
-               return &c, errors.WithMessage(err, "error loading config file")
-       }
+       f := try(basedir.Config.Open(filepath.Join("mflg", "config.toml")))
        defer f.Close()
-       _, err = toml.DecodeReader(f, &c)
+       _, err = toml.DecodeReader(f, c)
        if c.TextStyle.Comment == (Style{}) {
                c.TextStyle.Comment = Style{Foreground: &color.Color{R: 0, G: 200, B: 0}}
        }
        if c.TextStyle.String == (Style{}) {
                c.TextStyle.String = Style{Foreground: &color.Color{R: 0, G: 0, B: 200}}
        }
-       return &c, errors.WithMessage(err, "error loading config file")
+       return c, err
 }

На самом деле, полагаться на поведение try по сохранению значений возвращаемых параметров — например, на голый возврат — кажется, на мой взгляд, немного сложнее следовать; если бы я не добавил больше проверок ошибок, я бы придерживался if err != nil в этом конкретном случае.

TL;DR: try полезен только в довольно небольшом проценте (по количеству строк) этого кода, но там, где он помогает, он действительно помогает.

(нуб здесь). Еще одна идея для нескольких аргументов. Как насчет:

package trytest

import "fmt"

func errorInner() (string, error) {
   return "", fmt.Errorf("inner error")
}

func errorOuter() (string, error) {
   tryreturn errorInner()
   return "", nil
}

func errorOuterWithArg() (string, error) {
   var toProcess string
   tryreturn toProcess, _ = errorOuter()
   return toProcess + "", nil
}

func errorOuterWithArgStretch() (bool, string, error) {
   var toProcess string
   tryreturn false, ( toProcess,_ = errorOuterWithArg() )
   return true, toProcess + "", nil
}

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

Принципы, с которыми я согласен:
-

  • Обработка ошибок при вызове функции заслуживает отдельной строки. Go преднамеренно явный в потоке управления, и я думаю, что упаковывание этого в выражение противоречит его явности.
  • Было бы полезно иметь метод обработки ошибок, умещающийся в одну строку. (И в идеале требуется только одно слово или несколько шаблонных символов перед фактической обработкой ошибок). 3 строки обработки ошибок для каждого вызова функции — это точка трения в языке, которая заслуживает любви и внимания.
  • Любая возвращаемая встроенная функция (например, предложенная try ) должна быть как минимум оператором, и в идеале содержать в себе слово return. Опять же, я думаю, что поток управления в Go должен быть явным.
  • Ошибки Go наиболее полезны, когда в них включен дополнительный контекст (я почти всегда добавляю контекст к своим ошибкам). Решение этой проблемы также должно поддерживать код обработки ошибок с добавлением контекста.

Синтаксис, который я поддерживаю:
-

  • оператор reterr _x_ (синтаксический сахар для if err != nil { return _x_ } , явно названный, чтобы указать, что он вернется)

Таким образом, общие случаи могут быть одной красивой короткой явной строкой:

func foo() error {
    a, err := bar()
    reterr err

    b, err := baz(a)
    reterr fmt.Errorf("getting the baz of %v: %v", a, err)

    return nil
}

Вместо 3 строк они теперь:

func foo() error {
    a, err := bar()
    if err != nil {
        return err
    }

    b, err := baz()
    if err != nil {
        return fmt.Errorf("getting the baz of %v: %v", a, err)
    }

    return nil
}

Вещи, с которыми я не согласен:



    • «Это слишком маленькое изменение, чтобы стоило менять язык»

      Я не согласен, это изменение качества жизни, которое устраняет самый большой источник трений, который у меня возникает при написании кода Go. При вызове функции требуется 4 строки

  • «Лучше дождаться более общего решения»
    Я не согласен, я думаю, что эта проблема достойна отдельного специального решения. Обобщенная версия этой проблемы заключается в сокращении стандартного кода, а обобщенный ответ — макросы, что противоречит духу явного кода Go. Если Go не собирается предоставлять общие средства макросов, то вместо этого он должен предоставлять некоторые специфические, очень широко используемые макросы, такие как reterr (каждый человек, который пишет Go, выиграет от reterr).

@Qhesz Это не сильно отличается от try:

func foo() error {
    a, err := bar()
    try(err)

    b, err := baz(a)
    try(wrap(err, "getting the baz of %v", a))

    return nil
}

@reusee Я ценю это предложение, я не знал, что его можно использовать таким образом. Хотя мне это кажется немного раздражающим, я пытаюсь понять, почему.

Я думаю, что «попытаться» — странное слово для такого использования. «try(action())» имеет смысл на английском языке, тогда как «try(value)» на самом деле не имеет смысла. Я бы смирился с этим, если бы это было другое слово.

Также try(wrap(...)) #$ сначала оценивает wrap(...) , верно? Как вы думаете, сколько из этого оптимизируется компилятором? (По сравнению с запуском if err != nil ?)

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

@Qhesz Я согласен с вами по поводу названия. Возможно, check более уместно, так как хорошо читается либо «проверить (действие ())», либо «проверить (ошибиться)».

@reusee Что немного иронично, поскольку в исходном черновом дизайне использовалось check .

06.07.19 [email protected] написал:

$ попробуй .
--- статистика ---
2930 (100,0% от 2930) объявлений функций
1408 (48,1% от 2930) функций возвращают ошибку
[ ... ]

Я не могу не озорничать: это "функции, возвращающие
ошибка в качестве последнего аргумента"?

Лусио.

Последняя мысль по моему вопросу выше: я бы все же предпочел синтаксис try(err, wrap("getting the baz of %v: %v", a, err)) , где wrap() выполняется только в том случае, если err не равен нулю. Вместо try(wrap(err, "getting the baz of %v", a)) .

@Qhesz Возможной реализацией wrap может быть:

func wrap(err error, format string, args ...interface{}) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}

Если компилятор может встроить wrap , тогда нет никакой разницы в производительности между предложениями wrap и if err != nil .

@reusee Я думаю, ты имел в виду if err == nil ;)

@Qhesz Возможной реализацией wrap может быть:

func wrap(err error, format string, args ...interface{}) error {
  if err == nil {
      return nil
  }
  return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err)
}

Если компилятор может встроить wrap , тогда нет никакой разницы в производительности между предложениями wrap и if err != nil .

%w недопустимый глагол go

(Я предполагаю, что он имел в виду %v...)

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

Думаю, я поддержу это предложение, если

  • это было check вместо try
  • какая-то часть инструментов Go навязала его, его можно было использовать только как оператор (т.е. относиться к нему как к встроенному «оператору», а не как к встроенной «функции». Это только встроенная функция по соображениям практичности, она пытается быть оператором, не будучи реализовано языком.) Например, если он ничего не возвращал, поэтому он никогда не был действителен внутри выражения, например panic() .
  • ~ возможно, какой-то индикатор того, что это макрос и влияет на поток управления, что-то, что отличает его от вызова функции. (например, check!(...) , как это делает Rust, но у меня нет четкого мнения о конкретном синтаксисе)~ Передумал

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

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

@deanveloper исправлено, спасибо.

@olekukonko @Qhesz %w недавно добавлен в подсказку: https://tip.golang.org/pkg/fmt/#Errorf

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

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

В первом случае в блоке if-err действительно должна быть какая-то логика, и переход к конструкции в стиле try препятствует этой хорошей практике, усложняя добавление дополнительной логики.

Во втором случае плохой код часто имеет вид:

..., _ := might_error()

или просто

might_error()

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

..., XXX := might_error()

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

Конечно, это не исключает решения типа returnif handle(err) .

В целом я против попытки, с комплиментами участникам за красивый минималистичный дизайн. Я не большой эксперт по Go, но я был одним из первых пользователей, и у меня есть код в производстве. Я работаю в группе Serverless в AWS, и похоже, что в конце этого года мы выпустим сервис на основе Go, первая проверка которого была написана мной. Я очень старый парень, мой путь пролегал через C, Perl, Java и Ruby. Мои вопросы уже появлялись в очень полезном обзоре дебатов, но я все же думаю, что их стоит повторить.

  1. Go — это небольшой, простой язык, поэтому он добился непревзойденной удобочитаемости. Я рефлекторно против добавления чего-либо к этому, если только выгода не будет действительно качественно существенной. Обычно человек не замечает скользкой дорожки, пока не окажется на ней, так что давайте не будем делать первый шаг.
  2. На меня довольно сильно повлиял приведенный выше аргумент об облегчении отладки. Мне нравится визуальный ритм в низкоуровневом инфраструктурном коде, состоящем из небольших строф кода типа «Сделай А. Проверь, сработало ли это. Сделайте B. Проверьте, сработало ли это… и т. д.» Потому что строки «Проверить» — это то место, где вы ставите printf или точку останова. Может быть, все остальные умнее, но в конечном итоге я регулярно использую эту идиому точки останова.
  3. Предполагая именованные возвращаемые значения, "try" примерно эквивалентна if err != nil { return } (я так думаю?) Мне лично нравятся именованные возвращаемые значения, и, учитывая преимущества декораторов ошибок, я подозреваю, что доля именованных возвращаемых значений err будет монотонно возрастать; что ослабляет преимущества try.
  4. Сначала мне понравилось предложение, чтобы gofmt благословил однострочник в строке выше, но в целом IDE, несомненно, примут эту идиому отображения в любом случае, и однострочник принесет в жертву преимущество отладки здесь.
  5. Кажется весьма вероятным, что некоторые формы вложенности выражений, содержащие «try», откроют дверь для усложнителей в нашей профессии, чтобы сеять такой же хаос, как и с потоками Java, разделителями и т. д. Go более успешно, чем большинство других языков, лишает умных из нас возможности продемонстрировать свои навыки.

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

За последние несколько лет я провел значительное количество времени, читая незнакомые библиотеки или фрагменты кода. Несмотря на скуку, if err != nil представляет собой очень легко читаемую, хотя и многословную по вертикали идиому. Дух того, что try() пытается достичь, благороден, и я действительно думаю, что что-то нужно сделать, но эта функция кажется неправильно расставленной по приоритетам, и что предложение увидит свет слишком рано (т.е. оно должно появиться после xerr и у дженериков была возможность промариноваться в стабильной версии в течение 6-12 месяцев).

Введение try() кажется благородным и стоящим предложением (например, 29% - ~40% утверждений if предназначены для проверки if err != nil ). На первый взгляд кажется, что сокращение шаблонов, связанных с обработкой ошибок, улучшит опыт разработчиков. Компромисс от введения try() приходит в виде когнитивной нагрузки от полутонких особых случаев. Одним из самых больших достоинств Go является то, что он прост и требует очень небольшой когнитивной нагрузки, чтобы что-то сделать (по сравнению с C++, где спецификация языка обширна и полна нюансов). Уменьшение одной количественной метрики (LoC if err != nil ) в обмен на увеличение количественной метрики умственной сложности — это трудная пилюля (т. е. умственный налог на самый ценный ресурс, который у нас есть, — мощность мозга).

В частности, новые специальные случаи для того, как try() обрабатывается с помощью go , defer и именованных возвращаемых переменных, делают try() достаточно волшебными, чтобы сделать код менее явно, так что все авторы или читатели кода Go должны будут знать эти новые особые случаи, чтобы правильно читать или писать Go, и раньше такого бремени не существовало. Мне нравится, что для этих ситуаций есть явные особые случаи — особенно по сравнению с введением некоторой формы неопределенного поведения, но тот факт, что они должны существовать в первую очередь, указывает на то, что на данный момент это неполный. Если бы особые случаи касались чего-либо, кроме обработки ошибок, это могло бы быть приемлемым, но если мы уже говорим о чем-то, что может повлиять на до 40% всех LoC, эти особые случаи необходимо будет обучить всему сообществу и это поднимает стоимость когнитивной нагрузки этого предложения до достаточно высокого уровня, чтобы вызывать беспокойство.

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

ИМНСХО, предложениям по xerr и дженерикам нужно время, чтобы отработать в производстве в течение 6-12 месяцев, прежде чем пытаться победить шаблон с if err != nil . Обобщения, вероятно, проложат путь для более богатой обработки ошибок и нового идиоматического способа обработки ошибок. Как только начинает появляться идиоматическая обработка ошибок с помощью дженериков, тогда и только тогда имеет смысл вернуться к обсуждению try() или чего-то подобного.

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

Меня беспокоят следующие моменты:

  1. try() сама по себе не сложна, но это когнитивные накладные расходы, которых раньше не существовало.
  2. Встраивая err != nil в предполагаемое поведение try() , язык предотвращает использование err в качестве способа передачи состояния вверх по стеку.
  3. Эстетически try() выглядит как вынужденная хитрость, но недостаточно умна, чтобы пройти явный и очевидный тест, которым пользуется большая часть языка Go. Как и большинство вещей, связанных с субъективными критериями, это вопрос личного вкуса и опыта, и его трудно измерить.
  4. Обработка ошибок с операторами switch / case и переносом ошибок кажется нетронутой этим предложением, и это упущенная возможность, что заставляет меня поверить, что это предложение не позволяет сделать неизвестное-неизвестное известным. -известный (или, в худшем случае, известный-неизвестный).

Наконец, предложение try() кажется новым прорывом плотины, которая сдерживала поток специфичных для языка нюансов, таких как то, что мы избежали, оставив C++ позади.

TL;DR: это не столько #nevertry ответ, сколько «не сейчас, пока нет, и давайте рассмотрим это снова в будущем после того, как xerr и дженерики созреют в экосистеме. "

Приведенный выше #32968 не является полным встречным предложением, но он основан на моем несогласии с опасной способностью вложения, которой обладает макрос try . В отличие от № 32946, это серьезное предложение, в котором, я надеюсь, отсутствуют серьезные недостатки (конечно, вы можете его увидеть, оценить и прокомментировать). Выдержка:

  • _Макрос check не является однострочным: он лучше всего помогает там, где много повторяющихся
    проверки с использованием одного и того же выражения должны выполняться в непосредственной близости._
  • _Его неявная версия уже компилируется на площадке._

Конструктивные ограничения (выполнены)

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

пример использования

// built-in 'check' macro signature: 
func check(Condition bool) {}

check(err != nil) // explicit catch: label.
{
    ucred, err := getUserCredentials(user)
    remote, err := connectToApi(remoteUri)
    err, session, usertoken := remote.Auth(user, ucred)
    udata, err := session.getCalendar(usertoken)

  catch:               // sad path
    ucred.Clear()      // cleanup passwords
    remote.Close()     // do not leak sockets
    return nil, 0, err // dress before leaving
}
// happy path

// implicit catch: label is above last statement
check(x < 4) 
  {
    x, y = transformA(x, z)
    y, z = transformB(x, y)
    x, y = transformC(y, z)
    break // if x was < 4 after any of above
  }

Надеюсь, это поможет, наслаждайтесь!

Я прочитал все, что мог, чтобы понять эту тему. Я за то, чтобы оставить все как есть.

Мои причины:

  1. Я и никто из тех, кого я учил Go, никогда не понимал обработку ошибок.
  2. Я ловлю себя на том, что никогда не пропускаю ловушки ошибок, потому что так легко сделать это сразу и там.

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

В этих случаях (и я сделал это сам) добавляются несколько блоков try. что удлиняет код и затмевает реализацию.

Если реализация try в Go отличается от реализации в других языках, возникнет еще большая путаница.

Мое предложение - оставить обработку ошибок такой, какая она есть.

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

Часть спецификации, которая меня больше всего беспокоит, это два запроса:

Поэтому мы предлагаем запретить try как вызываемую функцию в операторе go.
...
Поэтому мы предлагаем запретить try как вызываемую функцию и в операторе defer.

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

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

Я знаю, что @griesemer не хочет, чтобы эстетические мнения об этом предложении были включены в обсуждение, но я действительно думаю, что одна из причин, по которой люди находят это предложение эстетически отвратительным, заключается в том, что они чувствуют, что оно не совсем подходит как функция.

В предложении говорится:

Мы предлагаем добавить новую встроенную функцию, называемую try с сигнатурой (псевдокодом).

func try(expr) (T1, T2, … Tn)

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

  1. Что означает для функции принимать обобщенное выражение в качестве аргумента, не говоря уже о вызываемом выражении. Каждый раз, когда слово «выражение» используется в спецификации, оно означает что-то вроде невызванной функции. Как получается, что «вызываемая» функция может рассматриваться как выражение, когда в любом другом контексте ее возвращаемые значения являются семантически активными. IE мы думаем о вызываемой функции как о ее возвращаемых значениях. Исключениями являются go и defer , которые являются необработанными токенами, а не встроенными функциями.

  2. Кроме того, это предложение имеет неверную сигнатуру собственной функции или, по крайней мере, не имеет смысла, фактическая сигнатура:

func try(R1, R2, ... Rn) ((R|T)1, (R|T)2, ... (R|T)(n-1), ?Rn) 
// where T is the return params of the function that try is being called from
// where `R` is a return value from a function, `Rn` must be an error
// try will return the R values if Rn is nil and not return Tn at all
// if Rn is not nil then the T values will be returned as well as Rn at the end 
  1. Предложение не включает в себя то, что происходит в ситуациях, когда try вызывается с аргументами. Что происходит, если try вызывается с аргументами:
try(arg1, arg2,..., err)

Я думаю, что причина, по которой это не рассматривается, заключается в том, что try пытается принять аргумент expr , который на самом деле представляет n число возвращаемых аргументов из функции плюс что-то еще, что еще раз иллюстрирует этот факт. что это предложение нарушает семантическую связность того, что такое функция.

Моя последняя претензия к этому предложению заключается в том, что оно еще больше нарушает семантическое значение встроенных функций. Я не безразличен к идее, что встроенные функции иногда должны быть освобождены от семантических правил «обычных» функций (например, невозможность присваивать их переменным и т. д.), но это предложение создает большой набор исключений из « нормальные" правила, которые управляют функциями внутри golang.

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

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

f, err := os.Open("/dev/stdout")
throw err

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

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

@nathanjsweet предложение, которое вы ищете, # 32611 :-)

@nathanjsweet Кое-что из того, что вы говорите, оказывается не так. Язык не позволяет использовать defer или go с предварительно объявленными функциями append cap complex imag len make new real . Он также не допускает defer или go с определенными в спецификации функциями unsafe.Alignof unsafe.Offsetof unsafe.Sizeof .

Спасибо @nathanjsweet за обширный комментарий — @ianlancetaylor уже указал, что ваши аргументы технически неверны. Позвольте мне немного расширить:

1) Вы упомянули, что та часть спецификации, которая запрещает try с go и defer беспокоит вас больше всего, потому что try будет первым встроенным где это правда. Это неправильно. Компилятор уже не разрешает, например, defer append(a, 1) . То же самое верно и для других встроенных функций, которые производят результат, который затем падает на пол. Это самое ограничение также будет применяться к try в этом отношении (за исключением случаев, когда try не возвращает результат). (Причина, по которой мы даже упомянули эти ограничения в дизайн-документе, заключается в том, чтобы быть как можно более подробными - они действительно не имеют значения на практике. Кроме того, если вы внимательно прочитали дизайн-документ, в нем не говорится, что мы не можем сделать try работать с go или $# try defer - просто предлагает запретить это, в основном в качестве практической меры. try работают с go и defer , хотя это практически бесполезно.)

2) Вы предполагаете, что некоторые люди находят try «эстетически отвратительными», потому что технически это не функция, а затем вы концентрируетесь на специальных правилах для подписи. Рассмотрим new , make , append , unsafe.Offsetof : все они имеют специальные правила, которые мы не можем выразить с помощью обычной функции Go. Посмотрите на unsafe.Offsetof , у которого точно такое же синтаксическое требование к аргументу (это должно быть поле структуры!), которое мы требуем от аргумента для try (это должно быть одно значение типа error или вызов функции, возвращающий error в качестве последнего результата). Мы не выражаем эти сигнатуры формально в спецификации ни для одной из этих встроенных функций, потому что они не вписываются в существующий формализм — если бы они были, они не должны были бы быть встроенными. Вместо этого мы выражаем их правила в прозе. Именно поэтому они являются встроенными модулями, которые с самого первого дня являются спасательным люком в Go. Также обратите внимание, что в дизайн-документе это очень подробно описано.

3) Предложение также касается того, что происходит, когда try вызывается с аргументами (более одного): это не разрешено. В документации по дизайну прямо указано , что try принимает (одно) выражение входящего аргумента.

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

Спасибо.

@griesemer

Также обратите внимание, что в дизайн-документе это очень подробно описано.

Можете ли вы указать это. Я был удивлен, прочитав это.

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

Я думаю, что это справедливое замечание. Тем не менее, я думаю, что есть то, что прописано в документации по дизайну, и что-то похожее на «вперед» (о чем много говорит Роб Пайк). Я думаю, будет справедливо сказать, что предложение try расширяет способы, которыми встроенные функции нарушают правила, по которым мы ожидаем поведения функций, и я признаю, что понимаю, почему это необходимо для других встроенных функций. , но я думаю, что в этом случае расширение нарушения правил:

  1. В некотором смысле контринтуитивно. Это первая функция, которая изменяет логику потока управления таким образом, чтобы не разворачивать стек (как это делают panic и os.Exit )
  2. Новое исключение из того, как работают соглашения о вызовах функций. Вы привели пример unsafe.Offsetof как случай, когда существует синтаксическое требование для вызова функции (на самом деле меня удивляет, что это вызывает ошибку времени компиляции, но это другой вопрос), но синтаксическое требование , в этом случае - это другое синтаксическое требование, чем то, которое вы указали. Для unsafe.Offsetof требуется один аргумент, тогда как для try требуется выражение, которое в любом другом контексте выглядело бы как значение, возвращаемое функцией (например, try(os.Open("/dev/stdout")) ), и его можно было бы с уверенностью предположить в любом другом контексте возвращать только одно значение (если выражение не выглядит как try(os.Open("/dev/stdout")...) ).

@nathanjsweet написал:

Также обратите внимание, что в дизайн-документе это очень подробно описано.

Можете ли вы указать это. Я был удивлен, прочитав это.

Это в разделе «Выводы» предложения:

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

Я удивлен, что ты пропустил это ;-)

@ngrilly Я не имею в виду это предложение, я имею в виду спецификацию языка go. У меня сложилось впечатление, что @griesemer говорил, что спецификация языка go вызывает встроенные функции как особенно полезный механизм для нарушения синтаксических соглашений.

@nathanjsweet

В некотором смысле контринтуитивно. Это первая функция, которая изменяет логику потока управления таким образом, чтобы не разворачивать стек (как это делают panic и os.Exit).

Я не думаю, что os.Exit раскручивает стек в каком-либо полезном смысле. Он немедленно завершает программу без запуска каких-либо отложенных функций. Мне кажется, что os.Exit здесь странная, так как и panic , и try запускают отложенные функции и перемещаются вверх по стеку.

Я согласен с тем, что os.Exit — это странно, но так и должно быть. os.Exit останавливает все горутины; не имеет смысла запускать только отложенные функции только горутины, которая вызывает os.Exit . Он должен либо запускать все отложенные функции, либо ничего. И гораздо проще не запускать ни одного.

Выполнил tryhard в нашей кодовой базе и вот что мы получили:

--- stats ---
  15298 (100.0% of   15298) func declarations
   3026 ( 19.8% of   15298) func declarations returning an error
  33941 (100.0% of   33941) statements
   7765 ( 22.9% of   33941) if statements
   3747 ( 48.3% of    7765) if <err> != nil statements
    131 (  3.5% of    3747) <err> name is different from "err"
   1847 ( 49.3% of    3747) return ..., <err> blocks in if <err> != nil statements
   1900 ( 50.7% of    3747) complex error handler in if <err> != nil statements; cannot use try
     19 (  0.5% of    3747) non-empty else blocks in if <err> != nil statements; cannot use try
   1789 ( 47.7% of    3747) try candidates

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

Я был в лагере "да! давайте сделаем это", и я думаю, что это будет интересный эксперимент для бета -версий 1.13 или 1.14, но меня беспокоят _" 47,7% ... пробных кандидатов"_. Теперь это означает, что есть 2 способа делать вещи, которые мне не нравятся. Однако есть также 2 способа создания указателя ( new(Foo) против &Foo{} ), а также 2 способа создания среза или карты с помощью make([]Foo) и []Foo{} .

Теперь я в лагере "давайте _попробуем_ это" :^) и посмотрю, что думает сообщество. Возможно, мы изменим наши шаблоны кодирования, чтобы они были ленивыми, и перестанем добавлять контекст, но, возможно, это нормально, если ошибки получают лучший контекст из импликации xerrors , которая все равно появится.

Спасибо, @Goodwine за предоставление более конкретных данных!

(Кроме того, вчера вечером я внес небольшое изменение в tryhard , чтобы он разделил счетчик «сложных обработчиков ошибок» на два: сложные обработчики и возвраты вида return ..., expr , где последний значение результата не равно <err> . Это должно дать дополнительную информацию.)

Как насчет изменения предложения, чтобы оно было вариативным вместо этого странного аргумента-выражения?

Это бы решило много проблем. В случае, когда люди хотят просто вернуть ошибку, единственное, что изменится, — это явный вариативный ... . НАПРИМЕР:

try(os.Open("/dev/stdout")...)

однако люди, которым нужна более гибкая ситуация, могут сделать что-то вроде:

f, err := os.Open("/dev/stdout")
try(WrapErrorf(err, "whatever wrap does: %v"))

Эта идея делает слово try менее подходящим, но сохраняет обратную совместимость.

@nathanjsweet написал:

Я не имею в виду это предложение, я имею в виду спецификацию языка go.

Вот выдержки, которые вы искали в спецификации языка:

В разделе «Выражения»:

Следующие встроенные функции не разрешены в контексте инструкции: append cap complex imag len make new real unsafe.Alignof unsafe.Offsetof unsafe.Sizeof

В разделах «Переходные операторы» и «Отложенные операторы»:

Вызовы встроенных функций ограничены, как и для выражений.

В разделе «Встроенные функции»:

У встроенных функций нет стандартных типов Go, поэтому они могут появляться только в выражениях вызова; их нельзя использовать в качестве значений функций.

@nathanjsweet написал:

У меня сложилось впечатление, что @griesemer говорил, что спецификация языка go вызывает встроенные функции как особенно полезный механизм для нарушения синтаксического соглашения .

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

@nathanjsweet Это уже рассматривалось (на самом деле это был недосмотр), но это делает try не расширяемым. См. https://go-review.googlesource.com/c/proposal/+/181878 .

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

@griesemer спасибо за работу над этим и за то, что нашли время ответить на вопросы сообщества. Я уверен, что вы уже ответили на многие одни и те же вопросы. Я понимаю, что очень сложно решить эти проблемы и в то же время поддерживать обратную совместимость. Спасибо!

@nathanjsweet Что касается вашего комментария здесь :

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

Что касается ваших комментариев о том, что try расширяет встроенные функции различными способами: да, требование, которое unsafe.Offsetof предъявляет к своему аргументу, отличается от требования try . Но оба ожидают синтаксически выражения. Оба имеют некоторые дополнительные ограничения на это выражение. Требование try так легко вписывается в синтаксис Go, что ни один из внешних инструментов синтаксического анализа не нуждается в настройке. Я понимаю, что это кажется вам непривычным, но это не то же самое, что техническая причина против этого.

@griesemer последний _tryhard_ считает «сложные обработчики ошибок», но не «обработчики ошибок с одним оператором». Можно ли это добавить?

@networkimprov Что такое обработчик ошибок с одним оператором? Блок if , содержащий один невозвратный оператор?

@griesemer , обработчик ошибок с одним оператором представляет собой блок if err != nil , который содержит _любой_ одиночный оператор, включая возврат.

@networkimprov Готово. «сложные обработчики» теперь разделены на «один оператор, затем ветвь» и «сложные, а затем ветвь».

Тем не менее, обратите внимание, что эти подсчеты могут вводить в заблуждение: например, эти подсчеты включают в себя любой оператор if , который проверяет любую переменную на nil (если -err="" , который теперь используется по умолчанию для tryhard ). Я должен исправить это. Короче говоря, as is tryhard сильно переоценивает количество возможностей обработчиков сложных или одиночных операторов. Например, см. archive/tar/common.go , строка 701.

@networkimprov tryhard теперь предоставляет более точные подсчеты того, почему проверка ошибок не является кандидатом на try . Общее количество подсчетов try не изменилось, но количество возможностей для более одиночных и сложных обработчиков теперь является более точным (и примерно на 50% меньше, чем раньше, потому что до появления сложных then ветвь оператора if считалась до тех пор, пока if содержала проверку <varname> != nil , независимо от того, включала она проверку ошибок или нет).

Если кто-то хочет попробовать try чуть более практичным способом, я создал здесь игровую площадку WASM с реализацией прототипа:

https://ccbrown.github.io/wasm-go-playground/experimental/try-builtin/

И если кто-то действительно заинтересован в локальной компиляции кода с помощью try, у меня есть вилка Go с полностью функциональной/актуальной реализацией здесь: https://github.com/ccbrown/go/pull/1 .

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

Я провел tryhard против небольшого внутреннего проекта, над которым работал больше года назад. В рассматриваемом каталоге есть код для 3 серверов («микросервисы», я полагаю), сканер, который периодически запускается как задание cron, и несколько инструментов командной строки. Он также имеет довольно полные модульные тесты. (FWIW, различные части работают бесперебойно уже более года, и оказалось, что отлаживать и решать любые возникающие проблемы несложно)

Вот статистика:

--- stats ---
    370 (100.0% of     370) func declarations
    115 ( 31.1% of     370) func declarations returning an error
   1159 (100.0% of    1159) statements
    258 ( 22.3% of    1159) if statements
    123 ( 47.7% of     258) if <err> != nil statements
     64 ( 52.0% of     123) try candidates
      0 (  0.0% of     123) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     54 ( 43.9% of     123) { return ... zero values ..., expr }
      2 (  1.6% of     123) single statement then branch
      3 (  2.4% of     123) complex then branch; cannot use try
      1 (  0.8% of     123) non-empty else branch; cannot use try

Некоторые комментарии:
1) 50% всех операторов if в этой кодовой базе выполняли проверку ошибок, а try могли заменить примерно половину из них. Это означает, что четверть всех операторов if в этой (небольшой) кодовой базе являются напечатанной версией try .

2) Должен отметить, что для меня это неожиданно много, потому что за несколько недель до начала этого проекта мне довелось прочитать о семействе внутренних вспомогательных функций ( status.Annotate ), которые аннотируют сообщение об ошибке, но сохраняют Код состояния gRPC. Например, если вы вызываете RPC, и он возвращает ошибку со связанным кодом состояния PERMISSION_DENIED, возвращенная ошибка из этой вспомогательной функции по-прежнему будет иметь связанный код состояния PERMISSION_DENIED (и теоретически, если этот связанный код состояния распространялся на все путь до обработчика RPC, то RPC завершится с ошибкой с соответствующим кодом состояния). Я решил использовать эти функции для всего в этом новом проекте. Но судя по всему, в 50% случаев я просто распространял ошибку без аннотаций. (Перед запуском tryhard я прогнозировал 10%).

3) status.Annotate случается, чтобы сохранить ошибки nil (т.е. status.Annotatef(err, "some message: %v", x) вернет nil iff err == nil ). Я просмотрел всех не пробных кандидатов первой категории, и кажется, что все они поддаются следующему переписыванию:

```
// Before
enc, err := keymaster.NewEncrypter(encKeyring)                                                     
if err != nil {                                                                                    
  return status.Annotate(err, "failed to create encrypter")                                        
}

// After
enc, err := keymaster.NewEncrypter(encKeyring)                                                                                                                                                                  
try(status.Annotate(err, "failed to create encrypter"))
```

To be clear, I'm not saying this transformation is always necessarily a good idea, but it seemed worth mentioning since it boosts the count significantly to a bit under half of all `if` statements.

4) Если честно, аннотация ошибки на основе defer кажется несколько ортогональной try , поскольку она будет работать как с try , так и без нее. Но просматривая код для этого проекта, поскольку я внимательно изучал обработку ошибок, я заметил несколько случаев, когда ошибки, генерируемые вызываемым пользователем, имели бы больше смысла. Например, я заметил несколько экземпляров кода, вызывающего клиенты gRPC следующим образом:

```
resp, err := s.someClient.SomeMethod(ctx, req)
if err != nil {
  return ..., status.Annotate(err, "failed to call SomeMethod")
}
```

This is actually a bit redundant in retrospect, since gRPC already prefixes its errors with something like "/Service.Method to [ip]:port : ".

There was also code that called standard library functions using the same pattern:

```
hreq, err := http.NewRequest("GET", targetURL, nil)
if err != nil {
  return status.Annotate(err, "http.NewRequest failed")
}
```

In retrospect, this code demonstrates two issues: first, `http.NewRequest` isn't calling a gRPC API, so using `status.Annotate` was unnecessary, and second, assuming the standard library also return errors with callee context, this particular use of error annotation was unnecessary (although I am fairly certain the standard library does not consistently follow this pattern).

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

Одна вещь, @griesemer : есть ли у tryhard правильный знаменатель для «кандидатов без попыток»?
Изменить: ответил ниже, я неправильно прочитал статистику.

РЕДАКТИРОВАТЬ: То, что должно было быть отзывом, превратилось в предложение, которое нас прямо просили не делать здесь. Я переместил свой комментарий в суть .

@balasanjay Спасибо за ваш комментарий, основанный на фактах; это очень полезно.

Что касается вашего вопроса о tryhard : «кандидаты без попытки» (лучшее предложение названия приветствуется) — это просто количество случаев, когда оператор if удовлетворял всем критериям «проверки ошибок» (т.е. , у нас было то, что выглядело как присваивание переменной ошибки <err> , за которой следовала проверка if <err> != nil в исходном коде), но где мы не можем легко использовать try из-за код в блоках if . В частности, в порядке появления в выходных данных «кандидаты без попыток» это операторы if , которые имеют оператор return , который возвращает что-то еще, кроме <err> в конце, Операторы if с одним более сложным оператором return (или другим), операторы if с несколькими операторами в ветви «тогда» и операторы if с непустая ветка else . Некоторые из этих операторов if могут иметь несколько условий, удовлетворяющих одновременно, поэтому эти числа не просто складываются. Они предназначены для того, чтобы дать представление о том, что пошло не так, чтобы try можно было использовать.

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

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

Если у вас есть лучшее предложение, как и / или что измерять, я был бы рад это услышать. Я внес несколько корректировок на основе отзывов сообщества. Спасибо.

@subfuzion Спасибо за ваш комментарий, но мы не ищем альтернативных предложений. См. https://github.com/golang/go/issues/32437#issuecomment -501878888. Спасибо.

В интересах подсчета, независимо от результата:

Я и моя команда придерживаемся мнения, что, хотя фреймворк try , предложенный Робом, является разумной и интересной идеей, он не достигает того уровня, на котором он был бы уместным в качестве встроенного. Стандартный библиотечный пакет был бы гораздо более подходящим подходом, пока шаблоны использования не установлены на практике. Если бы try появилось в языке таким образом, мы бы использовали его в разных местах.

В более общем плане, сочетание очень стабильного базового языка Go и очень богатой стандартной библиотеки заслуживает сохранения. Чем медленнее языковая команда работает над изменениями основного языка, тем лучше. Конвейер x -> stdlib остается сильным подходом для такого рода вещей.

@griesemer А, извини. Я неправильно прочитал статистику, в качестве знаменателя используется счетчик «if err != nil statement» (123), а не счетчик «кандидаты попыток» (64) в качестве знаменателя. Я задам этот вопрос.

Спасибо!

@mattpalmer Модели использования зарекомендовали себя около десяти лет. Именно эти шаблоны использования напрямую повлияли на дизайн try . Какие модели использования вы имеете в виду?

@griesemer Извините, это моя вина - то, что началось в моей голове с объяснения того, что меня беспокоит в try , превратилось в собственное предложение, чтобы я не добавлял его. Это явно противоречило заявленным основным правилам (не говоря уже о том, что в отличие от этого предложения о новой встроенной функции здесь вводится новый оператор). Было бы полезно удалить комментарий, чтобы упростить беседу (или это считается дурным тоном)?

@subfuzion Я бы не стал об этом беспокоиться. Это спорное предложение, а предложений много. Многие диковинные

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

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

@я и

Я спросил об этом у @rsc некоторое время назад (https://github.com/golang/go/issues/32437#issuecomment-503245958):

@rsc
Не будет недостатка в местах, где можно разместить это удобство. Какая метрика, помимо этого, ищется, которая докажет сущность механизма? Есть ли список классифицированных случаев обработки ошибок? Как будет извлекаться ценность из данных, когда большая часть общественного процесса управляется настроениями?

Ответ был преднамеренным, но скучным и лишенным содержания (https://github.com/golang/go/issues/32437#issuecomment-503295558):

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

Было предложено дополнительное мнение (https://github.com/golang/go/issues/32437#issuecomment-503408184):

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

В конце концов, я сам ответил на свой вопрос: «Есть ли список классифицированных случаев обработки ошибок?». По сути, будет 6 режимов обработки ошибок: ручное прямое, ручное сквозное, ручное косвенное, автоматическое прямое, автоматическое сквозное, автоматическое косвенное. В настоящее время обычно используются только 2 из этих режимов. Непрямые способы, для облегчения которых прилагаются значительные усилия, кажутся большинству опытных сусликов крайне запретительными, и это беспокойство, по-видимому, игнорируется. (https://github.com/golang/go/issues/32437#issuecomment-507332843).

Кроме того, я предложил проверять автоматические преобразования перед преобразованием, чтобы попытаться обеспечить ценность результатов (https://github.com/golang/go/issues/32437#issuecomment-507497656). К счастью, со временем все больше предлагаемых результатов, похоже, имеют более качественную ретроспективу, но это все еще не рассматривает влияние непрямых методов трезво и согласованно. В конце концов (на мой взгляд), точно так же, как к пользователям следует относиться враждебно, к разработчикам следует относиться как к ленивым.

Также было указано на неспособность текущего подхода упустить ценных кандидатов (https://github.com/golang/go/issues/32437#issuecomment-507505243).

Я думаю, что стоит шуметь из-за того, что этот процесс вообще отсутствует и особенно глух к тону.

@iand Ответ , данный @rsc , все еще действителен. Я не уверен, какая часть этого ответа «не хватает содержания» или что нужно, чтобы быть «вдохновляющим». Но позвольте мне попытаться добавить больше «вещества»:

Цель процесса оценки предложения состоит в том, чтобы в конечном итоге определить, «принесло ли изменение ожидаемые выгоды или создало какие-либо непредвиденные затраты» (этап 5 в процессе).

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

В настоящее время мы находимся на этапе 2, поэтому до окончательного решения еще далеко. Шаг 2 предназначен для сбора отзывов и проблем, которых, кажется, предостаточно. Но чтобы внести ясность: до сих пор был только один комментарий, указывающий на _технический_ недостаток в дизайне, который мы исправили . Также было довольно много комментариев с конкретными данными, основанными на реальном коде, которые указывали на то, что try действительно уменьшит количество шаблонов и упростит код; и было несколько комментариев — также основанных на данных реального кода — которые показали, что try не сильно поможет. Такая конкретная обратная связь, основанная на фактических данных или указывающая на технические недостатки, действенна и очень полезна. Мы обязательно примем это во внимание.

А потом было огромное количество комментариев, которые по сути являются личными чувствами. Это менее действенно. Это не значит, что мы игнорируем его. Но то, что мы придерживаемся процесса, не означает, что мы «глухие».

Относительно этих замечаний: Явных противников этого предложения может быть два, может быть, три десятка - сами знаете кто. Они доминируют в этом обсуждении с частыми сообщениями, иногда по несколько в день. Из этого можно извлечь мало новой информации. Увеличение количества сообщений также не отражает «более сильного» настроения сообщества; это просто означает, что эти люди более красноречивы, чем другие.

@iand Ответ , данный @rsc , все еще действителен. Я не уверен, какая часть этого ответа «не хватает содержания» или что нужно, чтобы быть «вдохновляющим». Но позвольте мне попытаться добавить больше «вещества»:

@griesemer Я уверен, что это было непреднамеренно, но я хотел бы отметить, что ни одно из процитированных вами слов не было моим, а принадлежало более позднему комментатору.

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

@iand Действительно - это была просто моя оплошность. Мои извенения.

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

@griesemer

Какие модели использования вы имеете в виду?

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

Все это, а также соображения о долгосрочной стабильности основного языка наводят меня на мысль, что введение этой функции на уровне пакетов x или стандартной библиотеки (либо в виде пакета errors/try , либо в виде пакета $# errors.Try() ) было бы предпочтительнее, чем вводить его как встроенный.

@mattparlmer Поправьте меня, если я ошибаюсь, но я считаю, что это предложение должно быть в среде выполнения Go, чтобы использовать g, m (необходимо переопределить поток выполнения).

@фабиан-ф

@mattparlmer Поправьте меня, если я ошибаюсь, но я считаю, что это предложение должно быть в среде выполнения Go, чтобы использовать g, m (необходимо переопределить поток выполнения).

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

Это возможно, потому что семантика try может быть полностью выражена в терминах if и return ; на самом деле он не «отменяет поток выполнения» больше, чем if и return .

Вот отчет tryhard из кода моей компании, состоящего из 300 тыс. строк кода Go:

Первоначальный запуск:

--- stats ---
  13879 (100.0% of   13879) func declarations
   4381 ( 31.6% of   13879) func declarations returning an error
  38435 (100.0% of   38435) statements
   8028 ( 20.9% of   38435) if statements
   4496 ( 56.0% of    8028) if <err> != nil statements
    453 ( 10.1% of    4496) try candidates
      4 (  0.1% of    4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   3066 ( 68.2% of    4496) { return ... zero values ..., expr }
    356 (  7.9% of    4496) single statement then branch
    345 (  7.7% of    4496) complex then branch; cannot use try
     63 (  1.4% of    4496) non-empty else branch; cannot use try

У нас есть соглашение об использовании пакета errgo juju (https://godoc.org/github.com/juju/errgo) для маскировки ошибок и добавления к ним информации о трассировке стека, что предотвратит большинство перезаписей. Это означает, что мы вряд ли примем try по той же причине, по которой мы обычно избегаем возврата голых ошибок.

Поскольку кажется, что это может быть полезной метрикой, я удалил вызовы errgo.Mask() (которые возвращают ошибку без аннотации) и повторно запустил tryhard . Это оценка того, сколько проверок ошибок можно было бы переписать, если бы мы не использовали errgo:

--- stats ---
  13879 (100.0% of   13879) func declarations
   4381 ( 31.6% of   13879) func declarations returning an error
  38435 (100.0% of   38435) statements
   8028 ( 20.9% of   38435) if statements
   4496 ( 56.0% of    8028) if <err> != nil statements
   3114 ( 69.3% of    4496) try candidates
      7 (  0.2% of    4496) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
    381 (  8.5% of    4496) { return ... zero values ..., expr }
    358 (  8.0% of    4496) single statement then branch
    345 (  7.7% of    4496) complex then branch; cannot use try
     63 (  1.4% of    4496) non-empty else branch; cannot use try

Таким образом, я предполагаю, что ~ 70% возвратов ошибок в противном случае были бы совместимы с try .

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

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

В настоящее время предельные затраты на добавление некоторого контекста к ошибке очень низки; это чуть больше, чем ввод строки формата. Если это предложение будет принято, я беспокоюсь, что инженеры все больше предпочли бы эстетику, предлагаемую try , потому что это делает их код «более гладким» (что, к сожалению, является соображением для некоторых людей, по моему опыту), и теперь требуется дополнительный блок для добавления контекста. Они могли бы оправдать это, основываясь на аргументе «удобочитаемости», поскольку добавление контекста расширяет метод еще на 3 строки и отвлекает читателя от главного. Я думаю, что корпоративные кодовые базы отличаются от стандартной библиотеки Go в том смысле, что облегчение выполнения правильных действий, вероятно, оказывает ощутимое влияние на итоговое качество кода, проверки кода бывают разного качества, а командные практики различаются независимо друг от друга. . В любом случае, как вы сказали ранее, мы никогда не могли принять try для нашей кодовой базы.

Спасибо за внимание

@mattparlmer

Все это, а также соображения о долгосрочной стабильности основного языка наводят меня на мысль, что введение этой функции на уровне пакетов x или стандартной библиотеки (либо в виде пакета errors/try , либо в виде errors.Try() ) было бы предпочтительнее, чем вводить его как встроенный.

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

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

Тем не менее, он может быть реализован — и был реализован дважды — как препроцессор исходного кода: см. https://github.com/rhysd/trygo и https://github.com/lunixbochs/og.

Похоже, что около 60% кодовой базы tegola смогут использовать эту функцию.

Вот вывод tryhard для проекта tegola: (http://github.com/go-spatial/tegola)

--- try candidates ---
      1  tegola/atlas/atlas.go:84
      2  tegola/atlas/map.go:232
      3  tegola/atlas/map.go:238
      4  tegola/atlas/map.go:248
      5  tegola/atlas/map.go:253
      6  tegola/basic/geometry_math.go:248
      7  tegola/basic/geometry_math.go:251
      8  tegola/basic/geometry_math.go:268
      9  tegola/basic/geometry_math.go:276
     10  tegola/basic/json_marshal.go:33
     11  tegola/basic/json_marshal.go:153
     12  tegola/basic/json_marshal.go:276
     13  tegola/cache/azblob/azblob.go:54
     14  tegola/cache/azblob/azblob.go:61
     15  tegola/cache/azblob/azblob.go:67
     16  tegola/cache/azblob/azblob.go:74
     17  tegola/cache/azblob/azblob.go:80
     18  tegola/cache/azblob/azblob.go:105
     19  tegola/cache/azblob/azblob.go:109
     20  tegola/cache/azblob/azblob.go:204
     21  tegola/cache/azblob/azblob.go:259
     22  tegola/cache/file/file.go:42
     23  tegola/cache/file/file.go:56
     24  tegola/cache/file/file.go:110
     25  tegola/cache/file/file.go:116
     26  tegola/cache/file/file.go:129
     27  tegola/cache/redis/redis.go:41
     28  tegola/cache/redis/redis.go:46
     29  tegola/cache/redis/redis.go:51
     30  tegola/cache/redis/redis.go:56
     31  tegola/cache/redis/redis.go:70
     32  tegola/cache/redis/redis.go:79
     33  tegola/cache/redis/redis.go:84
     34  tegola/cache/s3/s3.go:85
     35  tegola/cache/s3/s3.go:102
     36  tegola/cache/s3/s3.go:112
     37  tegola/cache/s3/s3.go:118
     38  tegola/cache/s3/s3.go:123
     39  tegola/cache/s3/s3.go:138
     40  tegola/cache/s3/s3.go:164
     41  tegola/cache/s3/s3.go:172
     42  tegola/cache/s3/s3.go:179
     43  tegola/cache/s3/s3.go:284
     44  tegola/cache/s3/s3.go:340
     45  tegola/cmd/tegola/cmd/cache/format.go:97
     46  tegola/cmd/tegola/cmd/cache/seed_purge.go:94
     47  tegola/cmd/tegola/cmd/cache/seed_purge.go:103
     48  tegola/cmd/tegola/cmd/cache/seed_purge.go:170
     49  tegola/cmd/tegola/cmd/cache/tile_list.go:51
     50  tegola/cmd/tegola/cmd/cache/tile_list.go:64
     51  tegola/cmd/tegola/cmd/cache/tile_name.go:35
     52  tegola/cmd/tegola/cmd/cache/tile_name.go:43
     53  tegola/cmd/tegola/cmd/root.go:58
     54  tegola/cmd/tegola/cmd/root.go:61
     55  tegola/cmd/xyz2svg/cmd/draw.go:62
     56  tegola/cmd/xyz2svg/cmd/draw.go:70
     57  tegola/cmd/xyz2svg/cmd/draw.go:214
     58  tegola/config/config.go:96
     59  tegola/internal/env/parse.go:30
     60  tegola/internal/env/parse.go:69
     61  tegola/internal/env/parse.go:116
     62  tegola/internal/env/parse.go:174
     63  tegola/internal/env/parse.go:221
     64  tegola/internal/env/types.go:67
     65  tegola/internal/env/types.go:86
     66  tegola/internal/env/types.go:105
     67  tegola/internal/env/types.go:124
     68  tegola/internal/env/types.go:143
     69  tegola/maths/makevalid/main.go:189
     70  tegola/maths/makevalid/main.go:207
     71  tegola/maths/makevalid/main.go:221
     72  tegola/maths/makevalid/main.go:295
     73  tegola/maths/makevalid/main.go:504
     74  tegola/maths/makevalid/makevalid.go:77
     75  tegola/maths/makevalid/makevalid.go:89
     76  tegola/maths/makevalid/makevalid.go:118
     77  tegola/maths/makevalid/makevalid_test.go:93
     78  tegola/maths/makevalid/makevalid_test.go:163
     79  tegola/maths/makevalid/plyg/ring.go:518
     80  tegola/maths/triangle.go:1023
     81  tegola/mvt/layer.go:73
     82  tegola/mvt/layer.go:79
     83  tegola/mvt/vector_tile/vector_tile.pb.go:64
     84  tegola/provider/gpkg/gpkg.go:138
     85  tegola/provider/gpkg/gpkg.go:223
     86  tegola/provider/gpkg/gpkg_register.go:46
     87  tegola/provider/gpkg/gpkg_register.go:51
     88  tegola/provider/gpkg/gpkg_register.go:186
     89  tegola/provider/gpkg/gpkg_register.go:227
     90  tegola/provider/gpkg/gpkg_register.go:240
     91  tegola/provider/gpkg/gpkg_register.go:245
     92  tegola/provider/gpkg/gpkg_register.go:256
     93  tegola/provider/gpkg/gpkg_register.go:377
     94  tegola/provider/postgis/postgis.go:112
     95  tegola/provider/postgis/postgis.go:117
     96  tegola/provider/postgis/postgis.go:122
     97  tegola/provider/postgis/postgis.go:127
     98  tegola/provider/postgis/postgis.go:136
     99  tegola/provider/postgis/postgis.go:142
    100  tegola/provider/postgis/postgis.go:148
    101  tegola/provider/postgis/postgis.go:153
    102  tegola/provider/postgis/postgis.go:158
    103  tegola/provider/postgis/postgis.go:163
    104  tegola/provider/postgis/postgis.go:181
    105  tegola/provider/postgis/postgis.go:198
    106  tegola/provider/postgis/postgis.go:264
    107  tegola/provider/postgis/postgis.go:441
    108  tegola/provider/postgis/postgis.go:446
    109  tegola/provider/postgis/postgis.go:529
    110  tegola/provider/postgis/postgis.go:559
    111  tegola/provider/postgis/postgis.go:603
    112  tegola/provider/postgis/util.go:31
    113  tegola/provider/postgis/util.go:36
    114  tegola/provider/postgis/util.go:200
    115  tegola/server/bindata/bindata.go:89
    116  tegola/server/bindata/bindata.go:109
    117  tegola/server/bindata/bindata.go:129
    118  tegola/server/bindata/bindata.go:149
    119  tegola/server/bindata/bindata.go:169
    120  tegola/server/bindata/bindata.go:189
    121  tegola/server/bindata/bindata.go:209
    122  tegola/server/bindata/bindata.go:229
    123  tegola/server/bindata/bindata.go:370
    124  tegola/server/bindata/bindata.go:374
    125  tegola/server/bindata/bindata.go:378
    126  tegola/server/bindata/bindata.go:382
    127  tegola/server/bindata/bindata.go:386
    128  tegola/server/bindata/bindata.go:402
    129  tegola/server/middleware_gzip.go:71
    130  tegola/server/middleware_gzip.go:78
    131  tegola/server/server_test.go:85

--- <err> name is different from "err" ---
      1  tegola/basic/json_marshal.go:276

--- { return ... zero values ..., expr } ---
      1  tegola/basic/geometry_math.go:214
      2  tegola/basic/geometry_math.go:222
      3  tegola/basic/geometry_math.go:230
      4  tegola/cache/azblob/azblob.go:131
      5  tegola/cache/azblob/azblob.go:140
      6  tegola/cache/azblob/azblob.go:149
      7  tegola/cache/azblob/azblob.go:171
      8  tegola/cache/file/file.go:47
      9  tegola/cache/s3/s3.go:92
     10  tegola/cmd/internal/register/maps.go:108
     11  tegola/cmd/tegola/cmd/cache/flags.go:20
     12  tegola/cmd/tegola/cmd/cache/tile_name.go:51
     13  tegola/cmd/tegola/cmd/cache/worker.go:112
     14  tegola/cmd/tegola/cmd/cache/worker.go:123
     15  tegola/cmd/tegola/cmd/root.go:73
     16  tegola/cmd/tegola/cmd/root.go:78
     17  tegola/cmd/xyz2svg/cmd/root.go:60
     18  tegola/provider/gpkg/gpkg.go:90
     19  tegola/provider/gpkg/gpkg.go:95
     20  tegola/provider/gpkg/gpkg_register.go:264
     21  tegola/provider/gpkg/gpkg_register.go:297
     22  tegola/provider/gpkg/gpkg_register.go:302
     23  tegola/provider/gpkg/gpkg_register.go:313
     24  tegola/provider/gpkg/gpkg_register.go:328
     25  tegola/provider/postgis/postgis.go:193
     26  tegola/provider/postgis/postgis.go:208
     27  tegola/provider/postgis/postgis.go:222
     28  tegola/provider/postgis/postgis.go:228
     29  tegola/provider/postgis/postgis.go:234
     30  tegola/provider/postgis/postgis.go:243
     31  tegola/provider/postgis/postgis.go:249
     32  tegola/provider/postgis/postgis.go:255
     33  tegola/provider/postgis/postgis.go:304
     34  tegola/provider/postgis/postgis.go:315
     35  tegola/provider/postgis/postgis.go:319
     36  tegola/provider/postgis/postgis.go:364
     37  tegola/provider/postgis/postgis.go:456
     38  tegola/provider/postgis/postgis.go:520
     39  tegola/provider/postgis/postgis.go:534
     40  tegola/provider/postgis/postgis.go:565
     41  tegola/provider/postgis/util.go:108
     42  tegola/provider/postgis/util.go:113
     43  tegola/server/bindata/bindata.go:29
     44  tegola/server/bindata/bindata.go:245
     45  tegola/server/bindata/bindata.go:271
     46  tegola/server/bindata/bindata.go:396

--- single statement then branch ---
      1  tegola/cache/azblob/azblob.go:241
      2  tegola/cache/file/file.go:87
      3  tegola/cache/s3/s3.go:321
      4  tegola/cmd/internal/register/caches.go:18
      5  tegola/cmd/internal/register/providers.go:43
      6  tegola/cmd/internal/register/providers.go:62
      7  tegola/cmd/internal/register/providers.go:75
      8  tegola/config/config.go:192
      9  tegola/config/config.go:207
     10  tegola/config/config.go:217
     11  tegola/internal/env/dict.go:43
     12  tegola/internal/env/dict.go:121
     13  tegola/internal/env/dict.go:197
     14  tegola/internal/env/dict.go:273
     15  tegola/internal/env/dict.go:348
     16  tegola/internal/env/parse.go:79
     17  tegola/internal/env/parse.go:126
     18  tegola/internal/env/parse.go:184
     19  tegola/internal/env/parse.go:231
     20  tegola/maths/makevalid/plyg/ring.go:541
     21  tegola/maths/maths.go:239
     22  tegola/maths/validate/validate.go:49
     23  tegola/maths/validate/validate.go:53
     24  tegola/maths/validate/validate.go:59
     25  tegola/maths/validate/validate.go:69
     26  tegola/mvt/feature.go:94
     27  tegola/mvt/feature.go:99
     28  tegola/mvt/feature.go:592
     29  tegola/mvt/feature.go:603
     30  tegola/mvt/layer.go:90
     31  tegola/mvt/tile.go:48
     32  tegola/provider/postgis/postgis.go:570
     33  tegola/provider/postgis/postgis.go:586
     34  tegola/tile.go:172

--- complex then branch; cannot use try ---
      1  tegola/cache/azblob/azblob.go:226
      2  tegola/cache/file/file.go:78
      3  tegola/cache/file/file.go:122
      4  tegola/cache/s3/s3.go:195
      5  tegola/cache/s3/s3.go:206
      6  tegola/cache/s3/s3.go:219
      7  tegola/cache/s3/s3.go:307
      8  tegola/provider/gpkg/gpkg.go:39
      9  tegola/provider/gpkg/gpkg.go:45
     10  tegola/provider/gpkg/gpkg.go:131
     11  tegola/provider/gpkg/gpkg.go:154
     12  tegola/provider/gpkg/gpkg_register.go:171
     13  tegola/provider/gpkg/gpkg_register.go:195

--- stats ---
   1294 (100.0% of    1294) func declarations
    246 ( 19.0% of    1294) func declarations returning an error
   2693 (100.0% of    2693) statements
    551 ( 20.5% of    2693) if statements
    238 ( 43.2% of     551) if <err> != nil statements
    131 ( 55.0% of     238) try candidates
      1 (  0.4% of     238) <err> name is different from "err"
--- non-try candidates ---
     46 ( 19.3% of     238) { return ... zero values ..., expr }
     34 ( 14.3% of     238) single statement then branch
     13 (  5.5% of     238) complex then branch; cannot use try
      0 (  0.0% of     238) non-empty else branch; cannot use try

И сопутствующий проект: (http://github.com/go-spatial/geom)

--- try candidates ---
      1  geom/bbox.go:202
      2  geom/encoding/geojson/geojson.go:152
      3  geom/encoding/geojson/geojson.go:157
      4  geom/encoding/wkb/internal/tcase/symbol/symbol.go:73
      5  geom/encoding/wkb/internal/tcase/tcase.go:161
      6  geom/encoding/wkb/internal/tcase/tcase.go:172
      7  geom/encoding/wkb/wkb.go:50
      8  geom/encoding/wkb/wkb.go:110
      9  geom/encoding/wkt/internal/token/token.go:176
     10  geom/encoding/wkt/internal/token/token.go:252
     11  geom/internal/parsing/parsing.go:44
     12  geom/internal/parsing/parsing.go:85
     13  geom/internal/rtreego/rtree_test.go:110
     14  geom/multi_line_string.go:34
     15  geom/multi_polygon.go:35
     16  geom/planar/clip/linestring.go:82
     17  geom/planar/clip/linestring.go:181
     18  geom/planar/clip/point.go:23
     19  geom/planar/intersect/xsweep.go:106
     20  geom/planar/makevalid/makevalid.go:92
     21  geom/planar/makevalid/makevalid.go:191
     22  geom/planar/makevalid/setdiff/polygoncleaner.go:283
     23  geom/planar/makevalid/setdiff/polygoncleaner.go:345
     24  geom/planar/makevalid/setdiff/polygoncleaner.go:543
     25  geom/planar/makevalid/setdiff/polygoncleaner.go:554
     26  geom/planar/makevalid/setdiff/polygoncleaner.go:572
     27  geom/planar/makevalid/setdiff/polygoncleaner.go:578
     28  geom/planar/simplify/douglaspeucker.go:84
     29  geom/planar/simplify/douglaspeucker.go:88
     30  geom/planar/simplify.go:13
     31  geom/planar/triangulate/constraineddelaunay/triangle.go:186
     32  geom/planar/triangulate/constraineddelaunay/triangulator.go:134
     33  geom/planar/triangulate/constraineddelaunay/triangulator.go:138
     34  geom/planar/triangulate/constraineddelaunay/triangulator.go:142
     35  geom/planar/triangulate/constraineddelaunay/triangulator.go:173
     36  geom/planar/triangulate/constraineddelaunay/triangulator.go:176
     37  geom/planar/triangulate/constraineddelaunay/triangulator.go:203
     38  geom/planar/triangulate/constraineddelaunay/triangulator.go:248
     39  geom/planar/triangulate/constraineddelaunay/triangulator.go:396
     40  geom/planar/triangulate/constraineddelaunay/triangulator.go:466
     41  geom/planar/triangulate/constraineddelaunay/triangulator.go:553
     42  geom/planar/triangulate/constraineddelaunay/triangulator.go:583
     43  geom/planar/triangulate/constraineddelaunay/triangulator.go:667
     44  geom/planar/triangulate/constraineddelaunay/triangulator.go:672
     45  geom/planar/triangulate/constraineddelaunay/triangulator.go:677
     46  geom/planar/triangulate/constraineddelaunay/triangulator.go:814
     47  geom/planar/triangulate/constraineddelaunay/triangulator.go:818
     48  geom/planar/triangulate/constraineddelaunay/triangulator.go:823
     49  geom/planar/triangulate/constraineddelaunay/triangulator.go:865
     50  geom/planar/triangulate/constraineddelaunay/triangulator.go:870
     51  geom/planar/triangulate/constraineddelaunay/triangulator.go:875
     52  geom/planar/triangulate/constraineddelaunay/triangulator.go:897
     53  geom/planar/triangulate/constraineddelaunay/triangulator.go:901
     54  geom/planar/triangulate/constraineddelaunay/triangulator.go:907
     55  geom/planar/triangulate/constraineddelaunay/triangulator.go:1107
     56  geom/planar/triangulate/constraineddelaunay/triangulator.go:1146
     57  geom/planar/triangulate/constraineddelaunay/triangulator.go:1157
     58  geom/planar/triangulate/constraineddelaunay/triangulator.go:1202
     59  geom/planar/triangulate/constraineddelaunay/triangulator.go:1206
     60  geom/planar/triangulate/constraineddelaunay/triangulator.go:1216
     61  geom/planar/triangulate/delaunaytriangulationbuilder.go:66
     62  geom/planar/triangulate/incrementaldelaunaytriangulator.go:46
     63  geom/planar/triangulate/incrementaldelaunaytriangulator.go:78
     64  geom/planar/triangulate/quadedge/lastfoundquadedgelocator.go:65
     65  geom/planar/triangulate/quadedge/quadedgesubdivision.go:976
     66  geom/slippy/tile.go:133

--- { return ... zero values ..., expr } ---
      1  geom/internal/parsing/parsing.go:125
      2  geom/planar/triangulate/constraineddelaunay/triangulator.go:428
      3  geom/planar/triangulate/constraineddelaunay/triangulator.go:447
      4  geom/planar/triangulate/constraineddelaunay/triangulator.go:460

--- single statement then branch ---
      1  geom/bbox.go:259
      2  geom/encoding/wkb/internal/decode/decode.go:29
      3  geom/encoding/wkb/internal/decode/decode.go:55
      4  geom/encoding/wkb/internal/decode/decode.go:63
      5  geom/encoding/wkb/internal/decode/decode.go:70
      6  geom/encoding/wkb/internal/decode/decode.go:79
      7  geom/encoding/wkb/internal/decode/decode.go:84
      8  geom/encoding/wkb/internal/decode/decode.go:93
      9  geom/encoding/wkb/internal/decode/decode.go:99
     10  geom/encoding/wkb/internal/decode/decode.go:105
     11  geom/encoding/wkb/internal/decode/decode.go:114
     12  geom/encoding/wkb/internal/decode/decode.go:119
     13  geom/encoding/wkb/internal/decode/decode.go:135
     14  geom/encoding/wkb/internal/decode/decode.go:140
     15  geom/encoding/wkb/internal/decode/decode.go:149
     16  geom/encoding/wkb/internal/decode/decode.go:155
     17  geom/encoding/wkb/internal/decode/decode.go:161
     18  geom/encoding/wkb/internal/decode/decode.go:170
     19  geom/encoding/wkb/internal/decode/decode.go:176
     20  geom/encoding/wkb/internal/tcase/token/token.go:162
     21  geom/encoding/wkt/internal/token/token.go:136

--- complex then branch; cannot use try ---
      1  geom/encoding/wkb/internal/tcase/tcase.go:74
      2  geom/encoding/wkt/internal/symbol/symbol.go:125
      3  geom/planar/intersect/xsweep.go:165
      4  geom/planar/makevalid/makevalid.go:85
      5  geom/planar/makevalid/makevalid.go:172
      6  geom/planar/makevalid/triangulate.go:19
      7  geom/planar/makevalid/triangulate.go:28
      8  geom/planar/makevalid/triangulate.go:36
      9  geom/planar/makevalid/triangulate.go:58
     10  geom/planar/triangulate/constraineddelaunay/triangulator.go:358
     11  geom/planar/triangulate/constraineddelaunay/triangulator.go:373
     12  geom/planar/triangulate/constraineddelaunay/triangulator.go:453
     13  geom/planar/triangulate/constraineddelaunay/triangulator.go:1237
     14  geom/planar/triangulate/constraineddelaunay/triangulator.go:1243
     15  geom/planar/triangulate/constraineddelaunay/triangulator.go:1249

--- stats ---
    820 (100.0% of     820) func declarations
    146 ( 17.8% of     820) func declarations returning an error
   1715 (100.0% of    1715) statements
    391 ( 22.8% of    1715) if statements
    111 ( 28.4% of     391) if <err> != nil statements
     66 ( 59.5% of     111) try candidates
      0 (  0.0% of     111) <err> name is different from "err"
--- non-try candidates ---
      4 (  3.6% of     111) { return ... zero values ..., expr }
     21 ( 18.9% of     111) single statement then branch
     15 ( 13.5% of     111) complex then branch; cannot use try
      0 (  0.0% of     111) non-empty else branch; cannot use try

По поводу непредвиденных расходов, репост с #32611...

Я вижу три класса затрат:

  1. стоимость по спецификации, которая уточняется в конструкторской документации.
  2. стоимость инструментов (т.е. пересмотр программного обеспечения), также рассмотренная в проектной документации.
  3. стоимость экосистемы, которую сообщество подробно описало выше и в #32825.

Ре № 1 и 2, затраты try() скромны.

Чтобы упростить нет. 3, большинство комментаторов считают, что try() повредит наш код и/или экосистему кода, от которой мы зависим, и тем самым снизит нашу производительность и качество продукта. Это широко распространенное, хорошо аргументированное восприятие не следует сбрасывать со счетов как «фактуальное» или «эстетическое».

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

@griesemer явно несправедливо утверждать, что «три дюжины громких противников» составляют основную часть оппозиции. Здесь и в #32825 прокомментировали сотни людей. Вы сказали мне 12 июня: «Я признаю, что около 2/3 респондентов недовольны этим предложением». С тех пор более 2000 человек проголосовали за «оставьте err != nil покое» с 90% голосов.

@gdey , не могли бы вы изменить свой пост, чтобы включить только _статистику и кандидатов, не попробовавших_?

@robfig , @gdey Спасибо за предоставленные данные, особенно за сравнение до/после.

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

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

@networkimprov Извините за неясность. То, что я сказал, было:

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

Я имел в виду настоящие комментарии (например, «частые сообщения»), а не смайлики. Только относительно небольшое количество людей пишет здесь _неоднократно_, что я считаю правильным. Я также не говорил о #32825; Я говорил об этом предложении.

Что касается смайликов, то ситуация практически не изменилась по сравнению с месячной давностью: 1/3 смайликов указывают на положительное мнение, а 2/3 — на отрицательное.

@griesemer

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

switch x {
case rand.Int():
  a()
case 5, try(strconv.Atoi(y)):
  b()
}

Учитывая порядок оценки switch , я не вижу, как тривиально убрать strconv.Atoi(y) из предложения case , сохранив предполагаемую семантику; лучшее, что я мог придумать, это переписать switch как эквивалентную цепочку операторов if / else , например:

if x == rand.Int() {
  a()
} else if x == 5 {
  b()
} else if _v, _err := strconv.Atoi(y); _err != nil {
  return _err
} else if x == _v {
  b()
}

(Есть и другие ситуации, когда это может произойти, но это один из самых простых примеров и первое, что приходит на ум.)

Фактически, до того, как вы опубликовали это предложение, я работал над транслятором AST для реализации оператора check из проекта проекта и столкнулся с этой проблемой. Однако я использовал взломанную версию пакетов stdlib go/* ; возможно, внешний интерфейс компилятора устроен таким образом, чтобы упростить эту задачу? Или я что-то пропустил, и действительно есть простой способ сделать это?

См. также https://github.com/rhysd/trygo; согласно README, он не реализует выражения try и отмечает , по сути, ту же проблему, которую я поднимаю здесь; Я подозреваю, что может быть поэтому автор не реализовал эту функцию.

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

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

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

@dpinela Компилятор уже переводит оператор switch , такой как ваш, как последовательность if-else-if , поэтому я не вижу здесь проблемы. Кроме того, "синтаксическое дерево", которое использует компилятор, не является синтаксическим деревом "go/ast". Внутреннее представление компилятора позволяет создавать гораздо более гибкий код, который не всегда можно перевести обратно в Go.

@griesemer
Да, конечно, все, что вы говорите, имеет под собой основу. Однако серая область не так проста, как вы ее себе представляете. Те из нас, кто обучает других (мы, кто стремится развивать/продвигать сообщество), обычно относятся к голым возвращениям с большой осторожностью. Я ценю, что stdlib замусорил его повсюду. Но при обучении других всегда подчеркивается явная отдача. Пусть человек достигнет своей зрелости, чтобы обратиться к более «причудливому» подходу, но поощрение его с самого начала, несомненно, будет способствовать развитию трудночитаемого кода (т. е. плохих привычек). Это, опять же, тональная глухота, которую я пытаюсь выявить.

Лично я не хочу запрещать голые возвраты или манипулирование отложенными значениями. Когда они действительно подходят, я рад, что эти возможности доступны (хотя другие опытные пользователи могут занять более жесткую позицию). Тем не менее, поощрение применения этих менее распространенных и, как правило, хрупких функций таким всепроникающим образом — это совершенно противоположное направление, которое я когда-либо представлял себе в Го. Является ли явная перемена в характере воздержания от магии и ненадежных форм косвенности преднамеренным сдвигом? Должны ли мы также начать подчеркивать использование DIC и других сложных для отладки механизмов?

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

Добавление нескольких комментариев к моему отрицательному голосу.

Для конкретного предложения под рукой:

1) Я бы предпочел, чтобы это было ключевое слово, а не встроенная функция, по ранее сформулированным причинам потока управления и читабельности кода.

2) Семантически «попытаться» — громоотвод. И, если не возникнет исключение, «try» лучше переименовать во что-то вроде guard или ensure .

3) Помимо этих двух моментов, я думаю, что это лучшее предложение, которое я видел для такого рода вещей.

Еще пара комментариев, формулирующих мое возражение против любого добавления концепции try/guard/ensure вместо того, чтобы оставить if err != nil покое:

1) Это противоречит одному из первоначальных мандатов golang (по крайней мере, как я это воспринимал) быть явным, легко читаемым/понимаемым, с очень небольшим количеством «магии».

2) Это будет стимулировать лень именно в тот момент, когда требуется подумать: «Что лучше всего сделать моему коду в случае этой ошибки?». Существует много ошибок, которые могут возникнуть при выполнении «шаблонных» действий, таких как открытие файлов, передача данных по сети и т. д. Хотя вы можете начать с кучи «попыток», которые игнорируют нестандартные сценарии сбоев, в конечном итоге многие из них « trys" исчезнет, ​​так как вам, возможно, потребуется реализовать свои собственные задачи отсрочки/повторения, регистрации/отслеживания и/или очистки. «Маловероятные события» гарантированы в любом масштабе.

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

Первые 20 «Популярных пакетов» на godoc.org

Это репозитории, соответствующие первым 20 популярным пакетам на https://godoc.org , отсортированные по процентному соотношению попыток. При этом используются настройки по умолчанию tryhard , которые теоретически должны исключать каталоги vendor .

Среднее значение для пробных кандидатов в этих 20 репозиториях составляет 58%.

| проект | место | если стмц | если != ноль (% от если) | пробные кандидаты (% от if != nil) |
|---------|-----|----------------|----------------- -----|---------------
| github.com/google/uuid | 1714 | 12 | 16,7% | 0,0% |
| github.com/pkg/errors | 1886 | 10 | 0,0% | 0,0% |
| github.com/aws/aws-sdk-go | 1911309 | 32015 | 9,4% | 8,9% |
| github.com/jinzhu/gorm | 15246 | 44 | 11,4% | 20,0% |
| github.com/robfig/cron | 1911 | 20 | 35,0% | 28,6% |
| github.com/gorilla/websocket | 6959 | 212 | 32,5% | 39,1% |
| github.com/dgrijalva/jwt-go | 3270 | 118 | 29,7% | 40,0% |
| github.com/gomodule/redigo | 7119 | 187 | 34,8% | 41,5% |
| github.com/unixpickle/kahoot-hack | 1743 | 52 | 75,0% | 43,6% |
| github.com/lib/pq | 13396 | 239 | 30,1% | 55,6% |
| github.com/sirupsen/logrus | 5063 | 29 | 17,2% | 60,0% |
| github.com/prometheus/client_golang | 17791 | 194 | 49,0% | 62,1% |
| github.com/go-redis/redis | 21182 | 326 | 42,6% | 73,4% |
| github.com/mongodb/монго-го-драйвер | 86605 | 2097 | 37,8% | 73,9% |
| github.com/uber-go/zap | 15363 | 84 | 36,9% | 74,2% |
| github.com/golang/protobuf | 42959 | 685 | 22,9% | 77,1% |
| github.com/gin-gonic/джин | 14574 | 96 | 53,1% | 86,3% |
| github.com/go-pg/pg | 26369 | 831 | 37,7% | 86,9% |
| github.com/Shopify/сарама | 36427 | 1369 | 68,2% | 91,0% |
| github.com/stretchr/testify | 13496 | 32 | 43,8% | 92,9% |

Столбец « if stmts » подсчитывает только операторы if в функциях, возвращающих ошибку, именно так tryhard сообщает об этом, и это, мы надеемся, объясняет, почему он такой низкий для чего-то вроде gorm .

10 разное «большие» проекты Go

Учитывая, что популярные пакеты на godoc.org, как правило, являются пакетами библиотек, я также хотел проверить статистику по некоторым более крупным проектам.

Это разное. большие проекты, которые оказались для меня главными (т.е. за этими 10 нет реальной логики). Это снова отсортировано по проценту попыток кандидатов.

Среднее значение для пробных кандидатов в этих 10 репозиториях составляет 59%.

| проект | место | если стмц | если != ноль (% от если) | пробные кандидаты (% от if != nil) |
|---------|-----|----------------|----------------- -----|---------------------------------|
| github.com/juju/juju | 1026473 | 26904 | 51,9% | 17,5% |
| github.com/go-kit/комплект | 38949 | 467 | 57,0% | 51,9% |
| github.com/boltdb/bolt | 12426 | 228 | 46,1% | 53,3% |
| github.com/hashicorp/consul | 249369 | 5477 | 47,6% | 54,5% |
| github.com/докер/докер | 251152 | 8690 | 48,7% | 56,8% |
| github.com/istio/istio | 429636 | 7564 | 40,4% | 61,9% |
| github.com/gohugoio/хьюго | 94875 | 1853 | 42,4% | 64,8% |
| github.com/etcd-io/etcd | 209603 | 4657 | 38,3% | 65,5% |
| github.com/kubernetes/kubernetes | 1789172 | 40289 | 43,3% | 66,5% |
| github.com/cockroachdb/таракан | 1038529 | 22018 | 39,9% | 74,0% |


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

@thepudds , это не похоже на самый последний _tryhard_, который дает «кандидатов без попыток».

@networkimprov Я могу подтвердить, что по крайней мере для gorm это результаты последнего tryhard . «Кандидаты без попыток» просто не указаны в приведенных выше таблицах.

@daved Во-первых, позвольте мне заверить вас, что я/мы слышим вас громко и ясно. Хотя мы все еще находимся на ранней стадии процесса, и многое может измениться. Давайте не будем спешить.

Я понимаю (и ценю), что при обучении го можно выбрать более консервативный подход. Спасибо.

@griesemer К вашему сведению, вот результаты запуска этой последней версии tryhard на 233 тысячах строк кода, в которых я участвовал, большая часть из которых не является открытым исходным кодом:

--- stats ---
   8760 (100.0% of    8760) functions (function literals are ignored)
   2942 ( 33.6% of    8760) functions returning an error
  22991 (100.0% of   22991) statements in functions returning an error
   5548 ( 24.1% of   22991) if statements
   2929 ( 52.8% of    5548) if <err> != nil statements
    163 (  5.6% of    2929) try candidates
      0 (  0.0% of    2929) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   2213 ( 75.6% of    2929) { return ... zero values ..., expr }
    167 (  5.7% of    2929) single statement then branch
    253 (  8.6% of    2929) complex then branch; cannot use try
     14 (  0.5% of    2929) non-empty else branch; cannot use try

Большая часть кода использует идиому, похожую на:

 if err != nil {
     return ... zero values ..., errors.Wrap(err)
 }

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

Вот статистика небольшого вспомогательного инструмента GCP для автоматизации создания пользователей и проектов:

$ tryhard -r .
--- stats ---
    129 (100.0% of     129) functions (function literals are ignored)
     75 ( 58.1% of     129) functions returning an error
    725 (100.0% of     725) statements in functions returning an error
    164 ( 22.6% of     725) if statements
     93 ( 56.7% of     164) if <err> != nil statements
     64 ( 68.8% of      93) try candidates
      0 (  0.0% of      93) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     17 ( 18.3% of      93) { return ... zero values ..., expr }
      7 (  7.5% of      93) single statement then branch
      1 (  1.1% of      93) complex then branch; cannot use try
      0 (  0.0% of      93) non-empty else branch; cannot use try

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

Сбор err с

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

var errs []error
for _, p := range toDelete {
    fmt.Println("delete:", p.ProjectID)
    if err := s.DeleteProject(ctx, p.ProjectID); err != nil {
        errs = append(errs, err)
    }
}

Ответственность за оформление ошибок

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

func run() error {
    key := "MY_ENV_VAR"
    client, err := ClientFromEnvironment(key)
    if err != nil {
        // "github.com/pkg/errors"
        return errors.Wrap(err, key)
    }
    // do something with `client`
}

func ClientFromEnvironment(key string) (*http.Client, error) {
    filename, ok := os.LookupEnv(key)
    if !ok {
        return nil, errors.New("environment variable not set")
    }
    return ClientFromFile(filename)
}

Цитирование важной части из блога Go здесь снова для ясности:

Ответственность за обобщение контекста лежит на реализации ошибки. Ошибка, возвращаемая форматами os.Open, как «открыть /etc/passwd: разрешение отклонено», а не просто «отказано в разрешении». В ошибке, возвращаемой нашим Sqrt, отсутствует информация о недопустимом аргументе.

Имея это в виду, приведенный выше код теперь выглядит так:

func run() error {
    key := "MY_ENV_VAR"
    client := try(ClientFromEnvironment(key))
    // do something with `client`
}

func ClientFromEnvironment(key string) (*http.Client, error) {
    filename, ok := os.LookupEnv(key)
    if !ok {
        return nil, fmt.Errorf("environment variable not set: %s", key)
    }
    return ClientFromFile(filename)
}

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

Заключительные примечания

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

  1. Ключевое слово может «чувствовать себя» лучше, поскольку try меняет поток управления.
  2. Использование try означает, что вы больше не можете поставить блокировку отладки в случае return err .

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

Обновлять

Исправлена ​​сигнатура функции, которая раньше не возвращала error . Спасибо @magical , что заметили это!

func main() {
    key := "MY_ENV_VAR"
    client := try(ClientFromEnvironment(key))
    // do something with `client`
}

@mrkanister Придирки, но на самом деле вы не можете использовать try в этом примере, потому что main не возвращает error .

Это благодарственный комментарий;
спасибо @griesemer за садоводство и все, что вы делаете по этому вопросу, а также в других местах.

Если у вас много таких строк (из https://github.com/golang/go/issues/32437#issuecomment-509974901):

if !ok {
    return nil, fmt.Errorf("environment variable not set: %s", key)
}

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

try(condErrorf(!ok, "environment variable not set: %s", key))

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

Компактный однолинейный, если

В дополнение к однострочному предложению if @zeebo и других, оператор if может иметь компактную форму, которая удаляет != nil и фигурные скобки:

if err return err
if err return errors.Wrap(err, "foo: failed to boo")
if err return fmt.Errorf("foo: failed to boo: %v", err)

Я думаю, что это просто, легко и читабельно. Есть две части:

  1. Пусть операторы if неявно проверяют значения ошибок на nil (или, возможно, интерфейсы в более общем смысле). ИМХО, это улучшает читаемость за счет уменьшения плотности, и поведение вполне очевидно.
  2. Добавить поддержку if variable return ... . Поскольку return так близко к левой части, кажется, что все еще довольно легко просмотреть код - дополнительная сложность при этом является одним из основных аргументов против однострочных ifs (?) В Go также уже есть прецедент упрощения синтаксиса, например, путем удаления скобок из оператора if.

Текущий стиль:

a, err := BusinessLogic(state)
if err != nil {
   return nil, err
}

Однострочный, если:

a, err := BusinessLogic(state)
if err != nil { return nil, err }

Однострочный компакт, если:

a, err := BusinessLogic(state)
if err return nil, err
a, err := BusinessLogic(state)
if err return nil, errors.Wrap(err, "some context")
func (c *Config) Build() error {
    pkgPath, err := c.load()
    if err return nil, errors.WithMessage(err, "load config dir")

    b := bytes.NewBuffer(nil)
    err = templates.ExecuteTemplate(b, "main", c)
    if err return nil, errors.WithMessage(err, "execute main template")

    buf, err := format.Source(b.Bytes())
    if err return nil, errors.WithMessage(err, "format main template")

    target := fmt.Sprintf("%s.go", filename(pkgPath))
    err = ioutil.WriteFile(target, buf, 0644)
    if err return nil, errors.WithMessagef(err, "write file %s", target)

    // ...
}

@eug48 см. #32611

Вот статистика tryhard для монорепозитория (строк кода go, исключая код поставщика: 2 282 731):

--- stats ---
 117551 (100.0% of  117551) functions (function literals are ignored)
  35726 ( 30.4% of  117551) functions returning an error
 263725 (100.0% of  263725) statements in functions returning an error
  50690 ( 19.2% of  263725) if statements
  25042 ( 49.4% of   50690) if <err> != nil statements
  12091 ( 48.3% of   25042) try candidates
     36 (  0.1% of   25042) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
   3561 ( 14.2% of   25042) { return ... zero values ..., expr }
   3304 ( 13.2% of   25042) single statement then branch
   4966 ( 19.8% of   25042) complex then branch; cannot use try
    296 (  1.2% of   25042) non-empty else branch; cannot use try

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

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

Если люди хотят принять участие, вот ссылка, сокращенная для совместного использования:

https://forms.gle/gaCBgxKRE4RMCz7c7

Все, кто участвует, должны иметь возможность видеть сводные результаты. Возможно, это поможет сфокусировать дискуссию?

if err := os.Setenv("GO111MODULE", "on"); err != nil {
    return err
}

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

О, и, пожалуйста, представьте try , здесь тоже есть много вариантов использования.

--- stats ---
    929 (100.0% of     929) functions (function literals are ignored)
    230 ( 24.8% of     929) functions returning an error
   1480 (100.0% of    1480) statements in functions returning an error
    320 ( 21.6% of    1480) if statements
    206 ( 64.4% of     320) if <err> != nil statements
    109 ( 52.9% of     206) try candidates
      2 (  1.0% of     206) <err> name is different from "err"
--- non-try candidates (-l flag lists file positions) ---
     53 ( 25.7% of     206) { return ... zero values ..., expr }
     18 (  8.7% of     206) single statement then branch
     17 (  8.3% of     206) complex then branch; cannot use try
      2 (  1.0% of     206) non-empty else branch; cannot use try

@lpar Вы можете обсуждать альтернативы , но не делайте этого в этом выпуске. Речь идет о предложении try . Лучшим местом на самом деле был бы один из списков рассылки, например, go-nuts. Трекер проблем действительно лучше всего подходит для отслеживания и обсуждения конкретной проблемы, а не общего обсуждения. Спасибо.

@fabstu Обработчик defer будет отлично работать в вашем примере как с try , так и без него. Расширение вашего кода с помощью закрывающей функции:

func f() (err error) {
    defer func() {
       if err != nil {
          err = decorate(err, "msg") // here you can modify the result error as you please
       }
    }()
    ...
    if err := os.Setenv("GO111MODULE", "on"); err != nil {
        return err
    }
    ...
}

(обратите внимание, что результат err будет установлен return err ; а err , используемый return , будет объявлен локально с if — это обычные правила определения области действия).

Или с помощью try , что устранит необходимость в локальной переменной err :

func f() (err error) {
    defer func() {
       if err != nil {
          err = decorate(err, "msg") // here you can modify the result error as you please
       }
    }()
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

И, скорее всего, вы захотите использовать одну из предложенных функций errors/errd :

func f() (err error) {
    defer errd.Wrap(&err, ... )
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

И если вам не нужна упаковка, это будет просто:

func f() error {
    ...
    try(os.Setenv("GO111MODULE", "on"))
    ...
}

@fastu И, наконец, вы можете использовать errors/errd и без try , и тогда вы получите:

func f() (err error) {
    defer errd.Wrap(&err, ... )
    ...
    if err := os.Setenv("GO111MODULE", "on"); err != nil {
        return err
    }
    ...
}

Чем больше я думаю, тем больше мне нравится это предложение.
Единственное, что меня беспокоит, это везде использовать named return. Это, наконец, хорошая практика, и я должен использовать ее (никогда не пробовал)?

В любом случае, прежде чем менять весь мой код, будет ли он работать так?

func f() error {
  var err error
  defer errd.Wrap(&err,...)
  try(...)
}

@flibustenet Именованные параметры результата сами по себе не являются плохой практикой; обычная проблема с именованными результатами заключается в том, что они включают naked returns ; т. е. можно просто написать return без необходимости указывать фактические результаты _с помощью return _. В общем (но не всегда!) такая практика затрудняет чтение и анализ кода, потому что нельзя просто посмотреть на оператор return и сделать вывод о результате. Нужно сканировать код для параметров результата. Можно пропустить установку значения результата и так далее. Таким образом, в некоторых кодовых базах голые возвраты просто не приветствуются.

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

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

Относительно вашего примера кода: это не будет работать должным образом, потому что try _всегда_ устанавливает _result error_ (которая в данном случае не имеет имени). Но вы объявляете другую локальную переменную err , и errd.Wrap работает с ней. Это не будет установлено try .

Краткий отчет: я пишу обработчик HTTP-запросов, который выглядит так:

func Handler(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        id := chi.URLParam(r, "id")

        var err error
        // starts as bad request, then it's an internal server error after we parse inputs
        var statusCode = http.StatusBadRequest

        defer func() {
            if err != nil {
                wrap := xerrors.Errorf("handler fail: %w", err)
                logger.With(zap.Error(wrap)).Error("error")
                http.Error(w, wrap.Error(), statusCode)
            }
        }()
        var c Thingie
        err = unmarshalBody(r, &c)
        if err != nil {
            return
        }
        statusCode = http.StatusInternalServerError
        s, err := DoThing(ctx, c)
        if err != nil {
            return
        }
        d, err := DoThingWithResult(ctx, id, s)
        if err != nil {
            return
        }
        data, err := json.Marshal(detail)
        if err != nil {
            return
        }
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        _, err = w.Write(data)
        if err != nil {
            return
        }
}

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

@jonbodner

Это работает (https://play.golang.org/p/NaBZe-QShpu):

package main

import (
    "errors"
    "fmt"

    "golang.org/x/xerrors"
)

func main() {
    var err error
    defer func() {
        filterCheck(recover())
        if err != nil {
            wrap := xerrors.Errorf("app fail (at count %d): %w", ct, err)
            fmt.Println(wrap)
        }
    }()

    check(retNoErr())

    n, err := intNoErr()
    check(err)

    n, err = intErr()
    check(err)

    check(retNoErr())

    check(retErr())

    fmt.Println(n)
}

func check(err error) {
    if err != nil {
        panic(struct{}{})
    }
}

func filterCheck(r interface{}) {
    if r != nil {
        if _, ok := r.(struct{}); !ok {
            panic(r)
        }
    }
}

var ct int

func intNoErr() (int, error) {
    ct++
    return 0, nil
}

func retNoErr() error {
    ct++
    return nil
}

func intErr() (int, error) {
    ct++
    return 0, errors.New("oops")
}

func retErr() error {
    ct++
    return errors.New("oops")
}

О, первый минус! Хорошо. Позвольте прагматизму течь через вас.

Запустил tryhard на некоторых моих кодовых базах. К сожалению, некоторые из моих пакетов имеют 0 кандидатов на попытки, несмотря на то, что они довольно большие, потому что методы в них используют собственную реализацию ошибки. Например, при создании серверов мне нравится, чтобы мои методы уровня бизнес-логики выдавали только SanitizedError s, а не error s, чтобы во время компиляции гарантировать, что такие вещи, как пути к файловой системе или системная информация, не будут просачиваться пользователям в сообщениях об ошибках.

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

func (a *App) GetFriendsOfUser(userId model.Id) ([]*model.User, SanitizedError) {
    if user, err := a.GetUserById(userId); err != nil {
        // (*App).GetUserById returns (*model.User, SanitizedError)
        // This could be a try() candidate.
        return err
    } else if user == nil {
        return NewUserError("The specified user doesn't exist.")
    }

    friends, err := a.Store.GetFriendsOfUser(userId)
    // (*Store).GetFriendsOfUser returns ([]*model.User, error)
    // This could be a SQL error or a network error or who knows what.
    return friends, NewInternalError(err)
}

Есть ли какая-либо причина, по которой мы не можем ослабить текущее предложение, чтобы оно работало до тех пор, пока последнее возвращаемое значение как включающей функции, так и выражения функции try реализует ошибку и имеют один и тот же тип? Это по-прежнему позволит избежать какой-либо конкретной путаницы с интерфейсом nil ->, но позволит попробовать в ситуациях, подобных приведенной выше.

Спасибо, @jonbodner , за ваш пример . Я бы написал этот код следующим образом (несмотря на ошибки перевода):

func Handler(w http.ResponseWriter, r *http.Request) {
    statusCode, err := internalHandler(w, r)
    if err != nil {
        wrap := xerrors.Errorf("handler fail: %w", err)
        logger.With(zap.Error(wrap)).Error("error")
        http.Error(w, wrap.Error(), statusCode)
    }
}

func internalHandler(w http.ResponseWriter, r *http.Request) (statusCode int, err error) {
    ctx := r.Context()
    id := chi.URLParam(r, "id")

    // starts as bad request, then it's an internal server error after we parse inputs
    statusCode = http.StatusBadRequest
    var c Thingie
    try(unmarshalBody(r, &c))

    statusCode = http.StatusInternalServerError
    s := try(DoThing(ctx, c))
    d := try(DoThingWithResult(ctx, id, s))
    data := try(json.Marshal(detail))

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    try(w.Write(data))

    return
}

Он использует две функции, но он намного короче (29 строк против 40 строк) — и я использовал хороший интервал — и этот код не нуждается в defer . В частности, defer вместе с изменением statusCode на пути вниз и использованием в defer делает исходный код более трудным для понимания, чем это необходимо. Новый код, хотя и использует именованные результаты и голый возврат (вы можете легко заменить его на return statusCode, nil , если хотите), проще, потому что четко отделяет обработку ошибок от «бизнес-логики».

Просто сделайте репост моего комментария в другом выпуске https://github.com/golang/go/issues/32853#issuecomment -510340544

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

func foo() error {
    handler := func(err error, funcname string) error {
        return fmt.Errorf("%s: %v", funcname, err) // wrap something
        //return nil // or dismiss
    }

    a, b := try(bar1(), handler) 
    c, d := try(bar2(), handler) 
}

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

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

В связи с этим мы с самого начала задавались вопросом, можно ли обобщить try , чтобы он работал не только для типов error , но и для любого типа, и тогда тест err != nil стать x != zero , где x — эквивалент err (последний результат), а zero — соответствующее нулевое значение для типа x . К сожалению, это не работает для общего случая логических значений (и почти любого другого базового типа), потому что нулевое значение bool равно false , а ok != false равно противоположное тому, что мы хотели бы проверить.

@lunny Предлагаемая версия try не поддерживает функцию обработчика.

@griesemer О. Какая жалость! В противном случае я могу удалить github.com/pkg/errors и все errors.Wrap .

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

@griesemer Возврат error вместо объемлющей функции позволит забыть классифицировать каждую ошибку как внутреннюю ошибку, ошибку пользователя, ошибку авторизации и т. д. Как есть, компилятор улавливает это и использует try не стоило бы обменивать эти проверки во время компиляции на проверки во время выполнения.

Я хотел бы сказать, что мне нравится дизайн try , но в обработчике defer if еще есть оператор if , пока вы используете try . Я не думаю, что это было бы проще, чем операторы if без обработчиков try и defer . Возможно, только использование try было бы намного лучше.

@ccbrown Понял. Оглядываясь назад, я думаю, что предложенное вами расслабление не должно быть проблемой. Я считаю, что мы могли бы ослабить try для работы с любым типом интерфейса (и соответствующим типом результата), если на то пошло, а не только с error , пока соответствующий тест остается x != nil . Что-то думать о. Это можно было бы сделать заранее или задним числом, так как я считаю, что это будет обратно совместимое изменение.

Пример @jonbodner и то, как его переписал @griesemer , — это именно тот код, который у меня есть, где я действительно хотел бы использовать try .

Никого не беспокоит такое использование try:

данные: = попытка (json.Marshal (деталь))

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

@griesemer Я не планировал обсуждать здесь альтернативы. Тот факт, что все продолжают предлагать альтернативы, именно поэтому я думаю, что опрос, чтобы выяснить, чего на самом деле хотят люди, был бы хорошей идеей. Я просто написал об этом здесь, чтобы попытаться привлечь как можно больше людей, заинтересованных в возможности улучшения обработки ошибок Go.

@trende-jp Я действительно завишу от контекста этой строки кода - сама по себе она не может быть оценена каким-либо осмысленным образом. Если это единственный вызов json.Marshal и вы хотите усилить ошибку, лучше всего использовать оператор if . Если есть много вызовов json.Marshal , добавление контекста к ошибке может быть хорошо выполнено с помощью defer ; или, возможно, обернув все эти вызовы внутри локального замыкания, которое возвращает ошибку. Существует множество способов, как это можно учесть, если это необходимо (например, если в одной и той же функции много таких вызовов). «Ошибки — это значения» верно и здесь: используйте код, чтобы сделать обработку ошибок управляемой.

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

@trende-jp

Разве это не может быть решено с помощью defer ?

defer fmt.HandleErrorf(&err, "decoding %q", path)

Номера строк в сообщениях об ошибках также можно решить, как я показал в своем блоге: Как использовать 'try' .

@trende-jp @faiface В дополнение к номеру строки вы можете сохранить строку декоратора в переменной. Это позволит вам изолировать конкретный вызов функции, который не работает.

Я действительно думаю, что это абсолютно не должно быть встроенной функцией .

Несколько раз упоминалось, что panic() и recover() также изменяют поток управления. Очень хорошо, не будем добавлять больше.

@networkimprov написал https://github.com/golang/go/issues/32437#issuecomment -498960081:

Это не читается как Go.

Я не мог не согласиться.

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

Как бы вы себя чувствовали, если бы go func() превратились в go(func()) ?

Как насчет использования bang(!) вместо функции try . Это может сделать возможной цепочку функций:

func foo() {
    f := os.Open!("")
    defer f.Close()
    // etc
}

func bar() {
    count := mustErr!().Read!()
}

@силр

Как бы вы себя чувствовали, если бы go func() было go(func()) ?

Да ладно, это было бы вполне приемлемо.

@syrr Спасибо, но мы не запрашиваем альтернативные предложения в этой теме. См. также это о сохранении сосредоточенности.

Что касается вашего комментария : Go - прагматичный язык, поэтому использование встроенного здесь является прагматичным выбором. Он имеет несколько преимуществ по сравнению с использованием ключевого слова, как подробно описано в документации по дизайну . Обратите внимание, что try — это просто синтаксический сахар для общего шаблона (в отличие от go , который реализует основную функцию Go и не может быть реализован с другими механизмами Go), например append , copy и т. д. Использование встроенного — прекрасный выбор.

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

Я просто размышлял над частью моего собственного кода и над тем, как это будет выглядеть с try :

slurp, err := ioutil.ReadFile(path)
if err != nil {
    return err
}
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)

Мог стать:

return ioutil.WriteFile(path, append(copyrightText, try(ioutil.ReadFile(path))...), 0666)

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

@gbbr У вас есть выбор. Вы можете написать это как:

slurp := try(ioutil.ReadFile(path))
return ioutil.WriteFile(path, append(copyrightText, slurp...), 0666)

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

@griesemer Этот пример _присуще_ попробовать, вы не можете вкладывать код, который сегодня может дать сбой - вы вынуждены обрабатывать ошибки с потоком управления. Я хотел бы кое-что прояснить из https://github.com/golang/go/issues/32825#issuecomment -507099786 / https://github.com/golang/go/issues/32825#issuecomment -507136111, на которые вы ответил https://github.com/golang/go/issues/32825#issuecomment -507358397. Позже этот же вопрос снова обсуждался в https://github.com/golang/go/issues/32825#issuecomment -508813236 и https://github.com/golang/go/issues/32825#issuecomment -508937177 - последний из которых я заявляю:

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

_Или_ предложение должно содержать примеры, соответствующие реализации, чтобы все люди, рассматривающие его, могли ознакомиться с тем, что неизбежно появится в коде Go. Наряду со всеми краеугольными случаями, с которыми мы можем столкнуться при устранении неполадок не идеально написанного программного обеспечения, которое возникает на любом языке / в любой среде. Он должен отвечать на такие вопросы, как, например, как будут выглядеть трассировки стека с несколькими уровнями вложенности, легко ли распознаются местоположения ошибок? Как насчет значений методов, литералов анонимных функций? Какой тип трассировки стека создается ниже, если строка, содержащая вызовы fn(), не работает?

fn := func(n int) (int, error) { ... }
return try(func() (int, error) { 
    mu.Lock()
    defer mu.Unlock()
    return try(try(fn(111111)) + try(fn(101010)) + try(func() (int, error) {
       // yea...
    })(2))
}(try(fn(1)))

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

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

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

package main
import "fmt"
func dopanic(b bool) int { if b { panic("panic") }; return 1 }
type bar struct { a, b, d int; b *bar }
func main() {
    fmt.Println(&bar{
        a: 1,
        c: 1,
        d: 1,
        b: &bar{
            a: 1,
            c: 1,
            d: dopanic(true) + dopanic(false),
        },
    })
}

Обратите внимание, как первая игровая площадка дает сбой с левой стороны допаника, вторая справа, но обе выводят идентичную трассировку стека:
https://play.golang.org/p/SYs1r4hBS7O
https://play.golang.org/p/YMKkflcQuav

panic: panic

goroutine 1 [running]:
main.dopanic(...)
    /tmp/sandbox709874298/prog.go:7
main.main()
    /tmp/sandbox709874298/prog.go:27 +0x40

Я ожидал, что второй будет +0x41 или какое-то смещение после 0x40, что можно использовать для определения фактического вызова, который не удался во время паники. Даже если мы получим правильные шестнадцатеричные смещения, я не смогу определить, где произошел сбой, без дополнительной отладки. Сегодня это крайний случай, с которым люди редко сталкиваются. Если вы выпустите вложенную версию try, это станет нормой, так как даже предложение включает в себя try() + try() strconv, показывающее, что возможно и приемлемо использовать try таким образом.

1) Учитывая приведенную выше информацию, какие изменения в трассировке стека вы планируете внести (если таковые имеются), чтобы я все еще мог сказать, где мой код дал сбой?

2) Разрешено ли вложение попыток, потому что вы считаете, что это должно быть? Если да, то в чем вы видите преимущества вложения попыток и как вы предотвратите злоупотребления? Я думаю, что tryhard следует настроить для выполнения вложенных попыток там, где вы считаете это приемлемым, чтобы люди могли принять более обоснованное решение о том, как это влияет на их код, поскольку в настоящее время мы получаем только лучшие/самые строгие примеры использования. Это даст нам представление о том, какие ограничения на vet будут наложены, на данный момент вы сказали, что ветеринария будет защитой от необоснованных попыток, но как это будет реализовано?

3) Попытка вложенности, потому что это следствие реализации? Если да, то не кажется ли это очень слабым аргументом в пользу наиболее заметного изменения языка с момента выпуска Go?

Я думаю, что это изменение требует большего внимания в отношении попытки вложения. Каждый раз, когда я думаю об этом, где-то появляется какая-то новая болевая точка, я очень беспокоюсь, что все потенциальные негативы не проявятся, пока это не будет обнаружено в дикой природе. Вложенность также обеспечивает простой способ утечки ресурсов, как указано в https://github.com/golang/go/issues/32825#issuecomment -506882164, что сегодня невозможно. Я думаю, что история «ветерана» нуждается в гораздо более конкретном плане с примерами того, как она будет обеспечивать обратную связь, если она будет использоваться в качестве защиты от вредоносных примеров try(), которые я здесь привел, или реализация должна обеспечивать ошибки времени компиляции. для использования за пределами ваших идеальных лучших практик.

edit: я спросил у сусликов об архитектуре play.golang.org, и кто-то упомянул, что она компилируется через NaCl, так что, вероятно, это просто следствие / ошибка этого. Но я мог видеть, что это проблема на другой архитектуре, я думаю, что многие проблемы, которые могут возникнуть из-за введения нескольких возвратов на строку, просто не были полностью изучены, поскольку большинство применений сосредоточено вокруг разумного и чистого использования одной строки.

О нет, пожалуйста, не добавляйте эту «магию» в язык.
Они не выглядят и не ощущаются, как остальная часть языка.
Я уже вижу подобный код повсюду.

a, b := try( f() )
if a != 0 && b != "" {
...
}
...

вместо

a,b,err := f()
if err != nil {
...
}
...

или

a,b,_:= f()

Поначалу отцовство call if err.... было немного неестественным для меня, но теперь я привык к
Мне легче справляться с ошибками, поскольку они могут возникать в потоке выполнения, вместо того, чтобы писать обертки/обработчики, которые должны будут отслеживать какое-то состояние, чтобы действовать после запуска.
И если я решу игнорировать ошибки, чтобы спасти жизнь своей клавиатуре, я знаю, что однажды запаникую.

я даже изменил свои привычки в vbscript на:

on error resume next
a = f()
if er.number <> 0 then
    ...
end if
...

мне нравится это предложение

Все проблемы, которые у меня были (например, в идеале это должно быть ключевое слово, а не встроенное), рассматриваются в подробном документе.

Это не на 100% идеально, но это достаточно хорошее решение, которое а) решает актуальную проблему и б) делает это с учетом множества проблем обратной совместимости и других проблем.

Конечно, это делает некоторое «волшебство», но также и defer . Единственная разница заключается в том, что ключевое слово отличается от встроенного, и здесь имеет смысл избегать использования ключевого слова.

Я чувствую, что все важные отзывы о предложении try() уже озвучены. Но позвольте мне попытаться резюмировать:

1) try() переводит вертикальную сложность кода в горизонтальную
2) Вложенные вызовы try() так же трудно читать, как и тернарные операторы.
3) Вводит невидимый поток управления «возврат», который визуально не отличается (по сравнению с блоками с отступом, начинающимся с ключевого слова return )
4) Ухудшает практику переноса ошибок (контекст функции вместо конкретного действия)
5) Разделяет сообщество #golang и стиль кода (анти-gofmt)
6) Заставит разработчиков часто переписывать try() в if-err-nil и наоборот (tryhard вместо добавления логики очистки/дополнительных журналов/лучшего контекста ошибки)

@VojtechVitek Я думаю, что ваши замечания субъективны и могут быть оценены только после того, как люди начнут серьезно их использовать.

Однако я считаю, что есть один технический момент, который мало обсуждался. Шаблон использования defer для переноса/украшения ошибок влияет на производительность помимо простой стоимости самого defer , поскольку функции, использующие defer , не могут быть встроены.

Это означает, что использование try с упаковкой ошибок сопряжено с двумя потенциальными затратами по сравнению с возвратом завернутой ошибки сразу после проверки err != nil :

  1. отсрочка для всех путей через функцию, даже успешных
  2. потеря инлайнинга

Несмотря на предстоящие впечатляющие улучшения производительности за defer , стоимость по-прежнему не равна нулю.

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

история "ветеринара" нуждается в гораздо более конкретном плане

ветеринарная история - сказка. Он будет работать только для известных типов и будет бесполезен для пользовательских.

Всем привет,

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

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

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

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

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

(Возможно также, что постановка проблемы обработки ошибок была полностью отодвинута на второй план, когда в тот же день был опубликован черновой вариант дженериков.)

Что касается более широкой темы о том, что можно улучшить в обработке ошибок Go, мы были бы очень рады увидеть отчеты об опыте о том, какие аспекты обработки ошибок в Go являются для вас наиболее проблематичными в ваших собственных кодовых базах и рабочих средах, и какое влияние окажет хорошее решение. иметь в своем развитии. Если вы напишете такой отчет, разместите ссылку на странице Go2ErrorHandlingFeedback .

Спасибо всем, кто принял участие в этом обсуждении, здесь и в других местах. Как ранее указывал Расс Кокс, обсуждения в масштабах сообщества, подобные этому, в лучшем случае являются открытым исходным кодом . Мы очень благодарны всем за помощь в изучении этого конкретного предложения и, в более общем плане, в обсуждении лучших способов улучшить состояние обработки ошибок в Go.

Роберт Гриземер, для Комитета по рассмотрению предложений.

Спасибо, команда Go, за работу, проделанную над предложением попробовать. И спасибо комментаторам, которые боролись с этим и предлагали альтернативы. Иногда эти вещи живут своей собственной жизнью. Спасибо Go Team за то, что выслушали и ответили должным образом.

Ура!

Спасибо всем за обсуждение, чтобы у нас был наилучший результат!

Звонок для списка проблем и негативного опыта с обработкой ошибок Go. Однако,
Я и Teams очень довольны xerrors.As, xerrors.Is и xerrors.Errorf в производстве. Эти новые дополнения полностью изменяют обработку ошибок чудесным образом для нас теперь, когда мы полностью приняли изменения. На данный момент мы не сталкивались с какими-либо проблемами или потребностями, которые не были бы учтены.

@griesemer Просто хотел сказать спасибо (и, возможно, многим другим, кто работал с вами) за ваше терпение и усилия.

хорошо!

@griesemer Спасибо вам и всем остальным в команде Go за то, что неустанно выслушивали все отзывы и мирились со всеми нашими разными мнениями.

Так что, может быть, сейчас самое подходящее время, чтобы закрыть эту тему и перейти к будущим вещам?

@griesemer @rsc и @all , круто, всем спасибо. для меня это отличная дискуссия / определение / уточнение. улучшение некоторых частей, таких как проблема с ошибкой в ​​​​go, требует более открытого обсуждения (в предложениях и комментариях ...), чтобы сначала определить / прояснить основные проблемы.

ps, x/xerrors пока хороши.

(может иметь смысл заблокировать и эту тему...)

Спасибо команде и сообществу за участие в этом. Мне нравится, как много людей заботятся о Go.

Я действительно надеюсь, что сообщество сначала увидит усилия и навыки, которые были вложены в предложение попробовать, а затем дух вовлечения, которое последовало за этим, что помогло нам принять это решение. Будущее го очень яркое, если мы сможем продолжать в том же духе, особенно если мы все сможем поддерживать позитивный настрой.

func M() (данные, ошибка){
а, ошибка1 := А()
б, ошибка2 := В()
вернуть б, ноль
} => (если err1 != nil){ вернуть a, err1}.
(если err2 != nil){ вернуть b, err2}

Хорошо... Мне понравилось это предложение, но мне нравится, как сообщество и команда Go отреагировали и участвовали в конструктивном обсуждении, хотя иногда это было немного грубо.

У меня есть 2 вопроса относительно этого результата:
1/ Является ли «обработка ошибок» все еще областью исследований?
2/ Пересматриваются ли приоритеты отложенных улучшений?

Это еще раз доказывает, что сообщество Go услышано и может обсуждать противоречивые предложения по изменению языка. Как и изменения, которые вносятся в язык, изменения, которые не вносятся в него, являются улучшением. Спасибо, команда и сообщество Go, за усердную работу и цивилизованное обсуждение этого предложения!

Превосходно!

классно, очень полезно

Может быть, я слишком привязан к Go, но я думаю, что здесь была показана точка, так как
Расс описал: есть момент, когда сообщество становится не просто
курица без головы, это сила, с которой нужно считаться и
использовал для своего блага.

Благодаря координации, обеспечиваемой командой Go, мы можем
все гордятся тем, что мы пришли к выводу, с которым мы можем жить и
вернусь, без сомнения, когда условия будут более зрелыми.

Будем надеяться, что боль, ощущаемая здесь, сослужит нам хорошую службу в будущем.
(не было бы грустно, иначе?).

Лусио.

Я не согласен с решением. Однако я полностью поддерживаю подход, который предприняла команда Go. Широкое обсуждение в сообществе и рассмотрение отзывов разработчиков — вот что такое открытый исходный код.

Интересно, придут ли и дефер-оптимизации. Мне очень нравится аннотировать ошибки с его помощью и xerrors вместе, и сейчас это слишком дорого.

@pierrec Я думаю, нам нужно более четкое понимание того, какие изменения в обработке ошибок были бы полезны. Некоторые изменения значений ошибок будут в предстоящем выпуске 1.13 (https://tip.golang.org/doc/go1.13#errors), и мы приобретем опыт работы с ними. В ходе этого обсуждения мы видели много-много предложений по обработке синтаксических ошибок, и было бы полезно, если бы люди могли голосовать и комментировать те из них, которые кажутся особенно полезными. В более общем плане, как сказал @griesemer , отчеты об опыте были бы полезны.

Также было бы полезно лучше понять, в какой степени синтаксис обработки ошибок проблематичен для людей, плохо знакомых с языком, хотя это будет трудно определить.

В https://golang.org/cl/183677 ведется активная работа по улучшению производительности defer , и если не возникнет каких-либо серьезных препятствий, я ожидаю, что это попадет в выпуск 1.14.

@griesemer @ianlancetaylor @rsc Планируете ли вы решить проблему многословной обработки ошибок с помощью другого предложения, которое решит некоторые или все проблемы, поднятые здесь?

Итак, поздно на вечеринку, так как это уже было отклонено, но для будущего обсуждения темы, как насчет троичного синтаксиса условного возврата? (Я не видел ничего подобного при просмотре темы или просмотре ее представления Рассом Коксом, опубликованным в Твиттере.) Пример:

f, err := Foo()
return err != nil ? nil, err

Возвращает nil, err , если ошибка не равна нулю, и продолжает выполнение, если ошибка равна нулю. Форма заявления будет

return <boolean expression> ? <return values>

и это будет синтаксическим сахаром для:

if <boolean expression> {
    return <return values>
}

Основное преимущество заключается в том, что это более гибко, чем ключевое слово check или встроенная функция try , потому что оно может срабатывать не только при ошибках (например, return err != nil || f == nil ? nil, fmt.Errorf("failed to get Foo") , при большем количестве ошибок). чем просто ошибка, не равная нулю (например, return err != nil && err != io.EOF ? nil, err ) и т. д., но при этом довольно интуитивно понятная при чтении (особенно для тех, кто привык читать тернарные операторы на других языках).

Это также гарантирует, что обработка ошибок _по-прежнему происходит в месте вызова_, а не происходит автоматически на основе какого-либо оператора отсрочки. Одна из самых больших проблем, с которыми я столкнулся с первоначальным предложением, заключается в том, что оно пытается каким-то образом сделать фактическую _обработку_ ошибок неявными процессами, которые просто происходят автоматически, когда ошибка не равна нулю, без четкого указания на то, что поток управления вернется, если вызов функции вернет ненулевую ошибку. Вся _точка_ Go, использующая явные возвраты ошибок вместо системы, подобной исключениям, состоит в том, чтобы побудить разработчиков явно и намеренно проверять и обрабатывать свои ошибки, а не просто позволять им распространяться вверх по стеку, чтобы теоретически обрабатываться в какой-то момент выше. вверх. По крайней мере, явный, пусть и условный, оператор return четко аннотирует, что происходит.

@ngrilly Как сказал @griesemer , я думаю, нам нужно лучше понять, какие аспекты обработки ошибок программисты Go считают наиболее проблематичными.

Говоря лично, я не думаю, что стоит делать предложение, которое удаляет небольшое количество многословия. В конце концов, язык сегодня работает достаточно хорошо. Каждое изменение имеет свою цену. Если мы собираемся что-то изменить, нам нужна значительная выгода. Я думаю, что это предложение дало существенную выгоду в уменьшении детализации, но очевидно, что значительная часть программистов Go считает, что дополнительные затраты, которые оно налагает, были слишком высокими. Не знаю, есть ли здесь золотая середина. И я не знаю, стоит ли вообще решать эту проблему.

@kaedys Этот закрытый и чрезвычайно подробный вопрос определенно не подходит для обсуждения конкретных альтернативных синтаксисов для обработки ошибок.

@ianlancetaylor

Я думаю, что это предложение дало существенную выгоду в уменьшении детализации, но очевидно, что значительная часть программистов Go считает, что дополнительные затраты, которые оно налагает, были слишком высокими.

Я боюсь, что есть предвзятость самоотбора. Go известен своей подробной обработкой ошибок и отсутствием дженериков. Это, естественно, привлекает разработчиков, которые не заботятся об этих двух проблемах. Тем временем другие разработчики продолжают использовать свои текущие языки (Java, C++, C#, Python, Ruby и т. д.) и/или переключаются на более современные языки (Rust, TypeScript, Kotlin, Swift, Elixir и т. д.) из-за этого. . Я знаю многих разработчиков, которые избегают Go в основном по этой причине.

Я также думаю, что в игре присутствует предвзятость подтверждения. Гоферы использовались для защиты подробной обработки ошибок и отсутствия обработки ошибок, когда люди критикуют Go. Это затрудняет объективную оценку такого предложения, как try.

Несколько дней назад Стив Клабник опубликовал интересный комментарий на Reddit . Он был против введения ? в Rust, потому что это было «два способа написать одно и то же» и это было «слишком неявно». Но теперь, когда он написал несколько строк кода, ? — одна из его любимых функций.

@ngrilly Я согласен с вашими комментариями. Этих предубеждений очень трудно избежать. Что было бы очень полезно, так это более четкое понимание того, сколько людей избегают Go из-за подробной обработки ошибок. Я уверен, что число не равно нулю, но его трудно измерить.

Тем не менее верно также и то, что try внес новое изменение в поток управления, которое было трудно увидеть, и что, хотя try предназначался для помощи в обработке ошибок, он не помог с аннотированием ошибок. .

Спасибо за цитату Стива Клабника. Хотя я ценю и согласен с этим мнением, стоит учитывать, что как язык Rust, кажется, несколько более склонен полагаться на синтаксические детали, чем Go.

Как сторонник этого предложения, я, естественно, разочарован тем, что теперь оно было отозвано, хотя я думаю, что команда Go поступила правильно в сложившихся обстоятельствах.

Одна вещь, которая сейчас кажется совершенно ясной, заключается в том, что большинство пользователей Go не считают многословие обработки ошибок проблемой, и я думаю, что остальным из нас просто придется с этим смириться, даже если это отпугнет потенциальных новых пользователей. .

Я потерял счет тому, сколько альтернативных предложений я прочитал, и, хотя некоторые из них довольно хороши, я не видел ни одного, который, по моему мнению, стоил бы принятия, если бы try пришлось кусать пыль. Так что шанс появления какого-то среднего предложения кажется мне маловероятным.

Что касается более положительного момента, текущее обсуждение указало на способы, которыми все потенциальные ошибки в функции могут быть оформлены одинаковым образом и в одном и том же месте (используя defer или даже goto ) который я ранее не рассматривал, и я надеюсь, что команда Go, по крайней мере, рассмотрит возможность изменения go fmt , чтобы разрешить запись одного оператора if в одну строку, что, по крайней мере, сделает обработку ошибок _выглядит_ более компактным, даже если на самом деле не удаляет какой-либо шаблон.

@пьеррек

1/ Является ли «обработка ошибок» все еще областью исследований?

Уже более 50 лет! Кажется, не существует общей теории или даже практического руководства для последовательной и систематической обработки ошибок. В стране го (как и в других языках) существует даже путаница в отношении того, что такое ошибка. Например, EOF может быть исключительным условием, когда вы пытаетесь прочитать файл, но почему это ошибка? Является ли это фактической ошибкой или нет, зависит от контекста. Есть и другие подобные проблемы.

Возможно, требуется обсуждение на более высоком уровне (но не здесь).

Спасибо @griesemer @rsc и всем, кто участвовал в предложении. Многие другие говорили об этом выше, и стоит повторить, что ваши усилия по осмыслению проблемы, написанию предложения и добросовестному его обсуждению ценятся. Спасибо.

@ianlancetaylor

Спасибо за цитату Стива Клабника. Хотя я ценю и согласен с этим мнением, стоит учитывать, что как язык Rust, кажется, несколько более склонен полагаться на синтаксические детали, чем Go.

В целом я согласен с тем, что Rust больше полагается на синтаксические детали, чем Go, но я не думаю, что это применимо к данному конкретному обсуждению подробностей обработки ошибок.

Ошибки — это такие же значения в Rust, как и в Go. Вы можете обрабатывать их, используя стандартный поток управления, как в Go. В первых версиях Rust это был единственный способ обработки ошибок, как в Go. Затем они представили макрос try! , который удивительно похож на предложение встроенной функции try . В конце концов они добавили оператор ? , который представляет собой синтаксическую вариацию и обобщение макроса try! , но это не обязательно для демонстрации полезности try и того факта, что что сообщество Rust не жалеет, что добавило его.

Я хорошо осведомлен об огромных различиях между Go и Rust, но что касается многословия обработки ошибок, я думаю, что их опыт применим к Go. RFC и обсуждения, связанные с try! и ? , действительно стоит прочитать. Я был действительно удивлен тем, насколько похожи вопросы и аргументы за и против изменения языка.

@griesemer , вы объявили о решении отклонить предложение try в его нынешнем виде, но не сказали, что команда Go планирует делать дальше.

Планируете ли вы по-прежнему решать проблему многословия обработки ошибок с помощью другого предложения, которое решило бы проблемы, поднятые в этом обсуждении (отладка печати, покрытие кода, лучшее оформление ошибок и т. д.)?

В целом я согласен с тем, что Rust больше полагается на синтаксические детали, чем Go, но я не думаю, что это применимо к данному конкретному обсуждению подробностей обработки ошибок.

Поскольку другие все еще добавляют свои два цента, я думаю, что у меня все еще есть место, чтобы сделать то же самое.

Хотя я программирую с 1987 года, с Go я работаю всего около года. Около 18 месяцев назад, когда я искал новый язык для удовлетворения определенных потребностей, я смотрел и на Go, и на Rust. Я выбрал Go, потому что чувствовал, что код Go намного проще в изучении и использовании, и этот код Go был гораздо более читаемым, потому что Go, кажется, предпочитает слова для передачи смысла, а не краткие символы.

Так что я, например, был бы очень недоволен, если бы Go стал более похожим на Rust , включая использование восклицательных знаков ( ! ) и вопросительных знаков ( ? ) для обозначения значения.

Точно так же я думаю, что введение макросов изменило бы природу Go и привело бы к появлению тысяч диалектов Go, как в случае с Ruby. Так что я надеюсь, что макросы никогда не будут добавлены в Go, хотя я предполагаю, что шансов на это мало, к счастью, ИМО.

jmtcw

@ianlancetaylor

Что было бы очень полезно, так это более четкое понимание того, сколько людей избегают Go из-за подробной обработки ошибок. Я уверен, что число не равно нулю, но его трудно измерить.

Это не «мера» как таковая, но это обсуждение Hacker News содержит десятки комментариев от разработчиков, недовольных обработкой ошибок Go из-за ее многословия (и некоторые комментарии объясняют их рассуждения и приводят примеры кода): https://news.ycombinator. com/item?id=20454966.

Прежде всего, спасибо всем за отзывы, поддерживающие окончательное решение, даже если оно многих не удовлетворило. Это была действительно командная работа, и я очень рад, что нам всем удалось пройти через интенсивные обсуждения в целом вежливо и уважительно.

@ngrilly Говоря от себя, я все еще думаю, что было бы неплохо в какой-то момент решить проблему многословия обработки ошибок. Тем не менее, мы только что посвятили этому довольно много времени и энергии за последние полгода и особенно за последние 3 месяца, и мы были вполне довольны этим предложением, но мы явно недооценили возможную реакцию на него. Теперь имеет смысл сделать шаг назад, переварить и обработать отзывы, а затем принять решение о дальнейших шагах.

Кроме того, на самом деле, поскольку у нас нет неограниченных ресурсов, я думаю, что мысли о поддержке языка для обработки ошибок отойдут на некоторое время в пользу большего прогресса на других фронтах, особенно работа над дженериками, по крайней мере, для следующие несколько месяцев. if err != nil может раздражать, но это не повод для срочных действий.

Если вы желаете продолжить дискуссию, я хотел бы мягко предложить всем уйти отсюда и продолжить дискуссию в другом месте, в отдельной теме (если есть четкое предложение) или на других форумах, более подходящих для открытого обсуждения. Этот вопрос закрыт, в конце концов. Спасибо.

Я боюсь, что есть предвзятость самоотбора.

Я хотел бы ввести здесь и сейчас новый термин: «предвзятость создателя». Если кто-то готов поставить работу, им следует дать презумпцию невиновности.

Галерее арахиса очень легко кричать во всеуслышание на несвязанных форумах о том, что им не нравится предлагаемое решение проблемы. Также каждому очень легко написать незавершенную попытку из 3 абзацев для другого решения (без реальной работы, представленной на обочине). Если кто-то согласен со статус-кво, ок. Честная оценка. Представление чего-либо еще в качестве решения без полного предложения дает вам -10 000 баллов.

Я не поддерживаю и не против попыток, но я доверяю суждению Go Teams по этому вопросу, до сих пор их суждения предоставляли отличный язык, поэтому я думаю, что все, что они решат, сработает для меня, попробую или не попробую, я считаю мы должны понимать, как посторонние, что сопровождающие имеют более широкое представление об этом вопросе. синтаксис мы можем обсуждать весь день. Я хотел бы поблагодарить всех, кто работал или пытается улучшить go в данный момент, за их усилия, мы благодарны и с нетерпением ждем новых (не нарушающих предыдущие) улучшений в языковых библиотеках и среде выполнения, если таковые будут сочтены полезно вам, ребята.

Также каждому очень легко написать незавершенную попытку из 3 абзацев для другого решения (без реальной работы, представленной на обочине).

Единственное, что я (и многие другие) хотел сделать try полезным, так это необязательный аргумент, позволяющий ему возвращать обернутую версию ошибки вместо неизмененной ошибки. Я не думаю, что для этого потребовался огромный объем дизайнерской работы.

О нет.

Понимаю. Go хочу сделать что-то отличное от других языков.

Может быть, кто-то должен закрыть эту проблему? Обсуждение, вероятно, лучше подходит в другом месте.

Этот вопрос уже настолько затянулся, что запирать его кажется бессмысленным.

Все, имейте в виду, что эта тема закрыта, и комментарии, которые вы здесь сделаете, почти наверняка будут навсегда проигнорированы. Если с вами все в порядке, прокомментируйте.

В случае, если кто-то ненавидит слово try, которое позволяет им думать о языке Java, C *, я советую не использовать «try», а другие слова, такие как «help», «must» или «checkError».. (игнорируйте меня)

В случае, если кто-то ненавидит слово try, которое позволяет им думать о языке Java, C *, я советую не использовать «try», а другие слова, такие как «help», «must» или «checkError».. (игнорируйте меня)

Всегда будут пересекающиеся ключевые слова и понятия, которые имеют небольшие или большие семантические различия в языках, которые достаточно близки друг к другу (например, языки семейства C). Языковая особенность не должна вызывать путаницу внутри самого языка, различия между языками будут всегда.

плохой. это антипаттерн, неуважение к автору этого предложения

@alersenkevich Пожалуйста, будьте вежливы. См . https://golang.org/conduct.

Я думаю, что рад решению не идти дальше с этим. Для меня это было похоже на быстрый хак, чтобы решить небольшую проблему, связанную с тем, что если err != nil находится в нескольких строках. Мы же не хотим загромождать Go второстепенными ключевыми словами для решения таких второстепенных задач, не так ли? Поэтому предложение с гигиеническими макросами https://github.com/golang/go/issues/32620 кажется лучше. Он пытается быть более универсальным решением, чтобы открыть больше возможностей для большей гибкости. Там продолжается обсуждение синтаксиса и использования, поэтому не думайте, что это макросы C/C++. Суть в том, чтобы обсудить лучший способ делать макросы. С его помощью вы можете реализовать свою собственную попытку.

Я хотел бы получить отзывы о подобном предложении, которое решает проблему с текущей обработкой ошибок https://github.com/golang/go/issues/33161.

Честно говоря, это следует открыть заново, из всех предложений по обработке ошибок это самое разумное.

@OneOfOne с уважением, я не согласен с тем, что это следует открыть снова. Этот поток установил, что существуют реальные ограничения синтаксиса. Возможно, вы правы в том, что это самое «разумное» предложение, но я считаю, что статус-кво все же более разумен.

Я согласен с тем, что if err != nil слишком часто пишется в Go, но наличие единственного способа возврата из функции значительно улучшает читабельность. Хотя в целом я могу поддержать предложения, которые сокращают шаблонный код, IMHO стоимость никогда не должна быть удобочитаемостью.

Я знаю, что многие разработчики жалуются на "от руки" проверку ошибок в go, но, честно говоря, краткость часто противоречит удобочитаемости. Go имеет множество устоявшихся шаблонов здесь и в других местах, которые поощряют определенный способ ведения дел, и, по моему опыту, результатом является надежный код, который хорошо стареет. Это очень важно: код реального мира должен быть прочитан и понят много раз на протяжении всей его жизни, но написан только один раз. Когнитивные накладные расходы — это реальная цена даже для опытных разработчиков.

Вместо:

f := try(os.Open(filename))

Я ожидал:

f := try os.Open(filename)

Все, имейте в виду, что эта тема закрыта, и комментарии, которые вы здесь сделаете, почти наверняка будут навсегда проигнорированы. Если с вами все в порядке, прокомментируйте.
—@ianlancetaylor

Было бы неплохо, если бы мы могли использовать try для блока кодов вместе с текущим способом обработки ошибок.
Что-то вроде этого:

// Generic Error Handler
handler := func(err error) error {
    return fmt.Errorf("We encounter an error: %v", err)  
}
a := "not Integer"
b := "not Integer"

try(handler){
    f := os.Open(filename)
    x := strconv.Atoi(a)
    y, err := strconv.Atoi(b) // <------ If you want a specific error handler
    if err != nil {
        panic("We cannot covert b to int")   
    }
}

Код выше кажется чище, чем первоначальный комментарий. Я бы хотел, чтобы я мог это сделать.

Я сделал новое предложение #35179

val := попытка f() (ошибка){
паника (ошибка)
}

Я надеюсь, что это так:

i, err := strconv.Atoi("1")
if err {
    println("ERROR")
} else {
    println(i)
}

или

i, err := strconv.Atoi("1")
if err {
    io.EOF:
        println("EOF")
    io.ErrShortWrite:
        println("ErrShortWrite")
} else {
    println(i)
}

@myroid Я бы не возражал, если бы ваш второй пример был немного более общим в виде оператора switch-else :

```иди
я, ошибка := strconv.Atoi("1")
ошибка переключения != ноль; ошибка {
случай io.EOF:
println("EOF")
случай io.ErrShortWrite:
println("ErrShortWrite")
} еще {
println(я)
}

@piotrkowalczuk Ваш код выглядит намного лучше моего. Я думаю, что код может быть более кратким.

i, err := strconv.Atoi("1")
switch err {
case io.EOF:
    println("EOF")
case io.ErrShortWrite:
    println("ErrShortWrite")
} else {
    println(i)
}

Это не рассматривает вариант там будет глаз другого типа

Должен быть
Ошибка регистра!=ноль

Для ошибок, которые разработчик явно не зафиксировал

Пт, 8 ноября 2019 г., 12:06 Ян Фан, уведомления@github.com написал:

@piotrkowalczuk https://github.com/piotrkowalczuk Ваш код выглядит много
лучше, чем у меня. Я думаю, что код может быть более кратким.

i, err := strconv.Atoi("1")switch err {case io.EOF:
println("EOF")case io.ErrShortWrite:
println("ErrShortWrite")
} еще {
println(я)
}


Вы получаете это, потому что вас упомянули.
Ответьте на это письмо напрямую, просмотрите его на GitHub
https://github.com/golang/go/issues/32437?email_source=notifications&email_token=ABNEY4VH4KS2OX4M5BVH673QSU24DA5CNFSM4HTGCZ72YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEDPTTYY5950comment-5950-59
или отписаться
https://github.com/notifications/unsubscribe-auth/ABNEY4W4XIIHGUGIW2KXRPTQSU24DANCNFSM4HTGCZ7Q
.

switch не нуждается в else , у него есть default .

Я открыл https://github.com/golang/go/issues/39890 , в котором предлагается что-то похожее на Swift guard , которое должно решить некоторые проблемы с этим предложением:

  • поток управления
  • локальность обработки ошибок
  • удобочитаемость

Он не получил большой популярности, но может представлять интерес для тех, кто прокомментировал здесь.

Была ли эта страница полезной?
0 / 5 - 0 рейтинги