Как изящно обрабатывать ошибки в веб-сервисе

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

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

func DeleteAPI(c *gin.Context) {
    var db = c.MustGet("db").(*sql.DB)
    query := "DELETE FROM table WHERE some condition"
    tx, err := db.Begin()
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    defer tx.Rollback()
    result, err := tx.Exec(query)
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    num, err := result.RowsAffected()
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    err = tx.Commit()
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    c.JSON(200, gin.H{"deleted": num})
}

Как видите, даже этот простой обработчик четыре раза повторяет один и тот же шаблон «if err! = Nil». В API на основе «select» у меня их вдвое больше, поскольку есть потенциальные ошибки при связывании входных данных и ошибки при маршалинге ответа в JSON. Есть ли хороший способ сделать это более СУХИМ?


person Mad Wombat    schedule 31.05.2018    source источник
comment
Это сделано намеренно. Go многословен. Вы можете извлечь логику, как это сделал Адриан, но нет волшебной конструкции, позволяющей избежать if err != nil   -  person Aurelia    schedule 31.05.2018
comment
Кажется, есть много предложений об использовании интерфейса ошибок golang и управлении обработкой ошибок веб-сервисов в промежуточном программном обеспечении, но я не уверен, как применить их к джин.   -  person Mad Wombat    schedule 31.05.2018
comment
На самом деле нет хорошего способа обрабатывать ошибки в промежуточном программном обеспечении. Обработчик не может вернуть ошибку, поэтому единственный способ сделать это - паника / восстановление, что является ужасным способом обработки подобных ошибок.   -  person Adrian    schedule 31.05.2018
comment
Мне интересно, почему паника / восстановление не одобряется в сообществе го. В чем причина не использовать его?   -  person Mad Wombat    schedule 31.05.2018
comment
@MadWombat: прочтите это. Но TL; DR; исключения как ошибки (или паника в Go) были обходным решением для устаревших языковых ограничений, которые не применимы к Go (или многим другим современным языкам).   -  person Flimzy    schedule 31.05.2018
comment
Спасибо за ссылку. Это определенно пища для размышлений.   -  person Mad Wombat    schedule 31.05.2018
comment
@MadWombat Вы всегда можете сделать свой db-код более абстрактным таким образом, чтобы у вас было 3-4 функции, и они сами могут иметь от 4 до 8 проверок ошибок, но вы можете повторно использовать эти функции в любое время, когда вам нужно прикоснуться к db, и так значительно уменьшить количество строк if-err-not-nil.   -  person mkopriva    schedule 31.05.2018


Ответы (2)


Вы можете сделать его немного более СУХИМ с помощью помощника:

func handleError(c *gin.Context, err error) bool {
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return true
    }
    return false
}

Используется в качестве:

err = tx.Commit()
if handleError(c,err) {
    return
}

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

person Adrian    schedule 31.05.2018
comment
Это немного лучше, хотя я все же предпочел бы что-то более СУХОЕ. Отмечу ваш ответ, если ничего лучше не придет. - person Mad Wombat; 31.05.2018
comment
К сожалению, идиоматический Go включает в себя шаблон проверки ошибок для каждого вызова, который может возвращать ошибку. - person Adrian; 31.05.2018

Мой обычный подход - использовать функцию обертывания. У этого есть преимущество (по сравнению с ответом Адриана - который также является хорошим, кстати) в том, что обработка ошибок остается в более идиоматической форме (return result, err, в отличие от засорения вашего кода вызовами типа handleError(err)), при этом все еще консолидируя это в одно место.

func DeleteAPI(c *gin.Context) {
    num, err := deleteAPI(c)
    if err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    c.JSON(200, gin.H{"deleted": num})
}

func deleteAPI(c *gin.Context) (int, error) {
    var db = c.MustGet("db").(*sql.DB)
    query := "DELETE FROM table WHERE some condition"
    tx, err := db.Begin()
    if err != nil {
        return 0, err
    }
    defer tx.Rollback()
    result, err := tx.Exec(query)
    if err != nil {
        return 0, err
    }
    num, err := result.RowsAffected()
    if err != nil {
        return 0, err
    }
    err = tx.Commit()
    if err != nil {
        return 0, err
    }
    return num, nil
}

Для меня (и вообще для кодеров Go) приоритетом является читаемость кода над DRY. И из трех вариантов (ваш оригинал, Адриан и мой), на мой взгляд, моя версия более читабельна по той простой причине, что ошибки обрабатываются совершенно идиоматическим способом, и они переходят в верхний обработчик. Этот же подход работает одинаково хорошо, если ваш контроллер в конечном итоге вызывает другие функции, возвращающие ошибки. Перенося всю обработку ошибок в самую верхнюю функцию, вы избавляетесь от беспорядка, связанного с обработкой ошибок (кроме простой конструкции if err! = Nil {return err} `) во всем остальном коде.

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

func DeleteAPI(c *gin.Context) {
    result, err := deleteAPI(c)
    if handleError(c, err) {
        return
    }
    c.JSON(200, gin.H{"deleted": num})
}
person Flimzy    schedule 31.05.2018
comment
Это хороший образец для данного конкретного случая, я стремился создать что-то более общее для обработчика, который должен иметь дело с множеством потенциальных ошибок. Я бы сказал, учитывая, что deleteAPI теперь фактически является обработчиком базы данных, он должен просто принимать sql.DB в качестве параметра, а не иметь зависимость от gin. Таким образом, его можно было бы переместить в пакет DBAL, который вообще не должен иметь дело с джином, а только с базой данных. - person Adrian; 31.05.2018
comment
@Adrian: Я думаю, что ваш подход обработчика очень мощный - я часто использую его, и часто в сочетании с этим. Я обновил свой ответ, чтобы включить комбинацию в качестве опции. - person Flimzy; 31.05.2018
comment
Да, я обычно использую оба - обработчик HTTP, который использует помощник по ошибкам, который обращается к DBAL, который объединяет несколько возможных ошибок БД в одно возвращаемое значение. Я также иногда использую другой помощник по ошибкам в DBAL, который преобразует ошибки, специфичные для БД, во внутренние ошибки, так что уровень HTTP не должен знать о типах ошибок, специфичных для БД. - person Adrian; 31.05.2018
comment
@Adrian: Я также обычно встраиваю коды состояния HTTP в свои ошибки, которые затем извлекает мой обработчик верхнего уровня при отправке ответа клиенту. Слишком много всего, чтобы охватить одним ответом :) - person Flimzy; 31.05.2018
comment
К сожалению, я вижу в приведенном выше коде то, что если ваше возвращаемое значение не является интерфейсом, вы не можете вернуть nil. Таким образом, вы вынуждены возвращать действительное значение вместе с ошибкой. В этом случае, если бы я абстрагировал свои методы доступа к базе данных в пакет, и кто-то другой использовал бы их и забыл проверить ошибки, у них была бы странная, трудно отслеживаемая ошибка, когда операция удаления могла удалить некоторые записи, но был возвращен 0. - person Mad Wombat; 31.05.2018
comment
@MadWombat: интерфейсы - не единственные типы, допускающие обнуление. Но это не должно вызывать беспокойства - верните тип, подходящий для вашего варианта использования, и если это int (как в этом примере), просто верните 0 (или любое другое выбрасываемое значение) при возврате ошибки. Это обычная практика. - person Flimzy; 31.05.2018
comment
И не оптимизируйте для людей, которые забывают проверять ошибки. Такие люди заслуживают проблемы, которые у них возникают :) (А если серьезно ... УЖАСНАЯ идея написать свой код для людей, которые явно пишут плохой код. А отказ от проверки ошибок - одна из худших вещей, которые может сделать любой программист Go.) - person Flimzy; 31.05.2018
comment
Есть разница. На каком-то другом языке я бы бросил исключение и ожидал, что вызывающие абоненты поймают / проигнорируют его. Или просто позвольте любым исключениям, созданным в моем коде, распространяться. Таким образом, если кто-то не выполняет проверку на ошибки, он получит хороший, жирный бэкбэк. На ходу, если я возвращаю nil и выдаю ошибку, я четко указываю, что что-то не так, и следующая строка, которая пытается использовать результат «nil», выйдет из строя. Но если я верну действительное значение и ошибку, я полагаюсь на других людей, которые действительно позаботятся об этом, и это кажется плохим. - person Mad Wombat; 31.05.2018
comment
В случае удаления записей я всегда могу вернуть -1, так как вы никогда не сможете удалить отрицательное количество записей, но в целом это кажется хрупким. - person Mad Wombat; 31.05.2018
comment
@MadWombat есть много кода Go, в том числе в стандартной библиотеке, который вместе с ошибкой также возвращает значение, отличное от нуля (на ум приходят вездесущие Read/Write методы). Нет ничего плохого или хрупкого в возврате значения, не равного нулю, вместе с ошибкой. Что не только кажется, но на самом деле плохо, так это то, что другие программисты Go не проверяют ошибки. - person mkopriva; 31.05.2018
comment
Что происходит в вашем коде, если вы пытаетесь удалить 10 сущностей, а 9 удаляются успешно, но в 1 есть ошибка? Идиоматический ответ - вернуть 9 с ошибкой. Даже если вы удаляете только 1, а это не удается, возврат 0 и ошибка имеет смысл. Если какой-то вызывающий не заботится об ошибках, а только о количестве успешных удалений, он может игнорировать ошибки, и 0 имеет смысл. Не борись с языком. - person Flimzy; 01.06.2018