💭 如何从 Go 后端向前端发送清晰漂亮的错误消息?

2025-06-09

💭 如何从 Go 后端向前端发送清晰漂亮的错误消息?

介绍

嘿嘿,DEV 们太棒了!😉

想花点时间阅读一篇教你如何更好地与团队中的前端开发人员沟通的文章吗?感兴趣,但不知道具体内容?别担心,我现在就来解释!

👀 我在工作中经常注意到一个有趣的趋势:后端开发人员向前端发送错误状态和解释,但这些错误在应用程序中并不总是清晰地显示并处理。结果,前端开发人员不得不花费宝贵的时间来理解正在发生的事情,并在代码中实现一些边界情况。

💡 但是,如果我告诉你,来自后端的错误不仅开发人员可以读取,甚至用户也可以理解,你会怎么想? 是的,这正是本文要讨论的内容!

我们将研究一下go-playground/validator v10包,它几乎是 Go 中解决此类问题的首选。

📝 目录

待验证的模型

想象一下,我们需要实现从POST请求到新项目创建的 REST API 端点的传入数据的后端验证。你能想象吗?好的,现在让我们用 Go 代码描述这个结构。

除了标准属性db:"..."和之外json:"...",我们还需要向需要验证的每个结构字段添加一个validate:"..."带有包中所需验证标签的新属性。go-playground/validator

这听起来很复杂,但实际上一切都简单得多。看:

// ./app/models/project_model.go

// Project struct to describe project.
type Project struct {
    Title string `db:"title" json:"title" validate:"required,lte=25"` 
    // --> verify that the field exists and is less than or equal to 25 characters

    Description string `db:"description" json:"description" validate:"required"`
    // --> verify that the field exists

    WebsiteURL string `db:"website_url" json:"website_url" validate:"uri"`
    // --> verify that the field is correct URL string

    Tags []string `db:"tags" json:"tags" validate:"len=3"`
    // --> verify that the field contains exactly three elements
}
Enter fullscreen mode Exit fullscreen mode

☝️注意:以上并非您为结构配置字段验证的所有参数!您可以在此处找到完整列表。

有趣的是,如果我们指定一个验证器来检查,例如,如果 URL 已验证,那么我们就不再需要指定required验证标签了。这是因为空字符串不是有效的 URL。

换句话说,几乎任何验证标签都将为字段包含一个强制的非空值(空字符串nil等)。

↑ 目录

包中错误的 Vanilla 表示

输入 JSON 主体(此处和下文中,我们将使用这些输入参数作为 JSON 请求主体):

{
  "title": "",
  "description": "",
  "website_url": "not-valid-uri",
  "tags": [
    "one", "two"
  ]
}
Enter fullscreen mode Exit fullscreen mode

我将以纯文本形式显示结果错误响应,以便您可以理解为什么这种前端呈现选项并不

Key: 'Project.Title' Error:Field validation for 'Title' failed on the 'required' tag

Key: 'Project.Description' Error:Field validation for 'Description' failed on the 'required' tag

Key: 'Project.WebsiteURL' Error:Field validation for 'WebsiteURL' failed on the 'uri' tag

Key: 'Project.Tags' Error:Field validation for 'Tags' failed on the 'len' tag
Enter fullscreen mode Exit fullscreen mode

有几个要点是我们希望立即改进的。

首先,前端对我们应用中的结构和模型一无所知。如果后端以这种形式返回错误(至少没有指定验证失败的字段),前端将无法针对特定字段进行可视化输出。

其次,最好指定前端使用的字段的确切名称 — —WebsiteURL而不是website_url像 JSON 那样的“但是”。

第三,错误描述本身不会告诉用户(甚至前端开发人员)任何有用的信息,除了某个地方出了问题。

好吧,让我们尝试改进它!

☝️注意:我会向你展示我在我的项目中是如何实现的。顺便说一句,我很乐意收到你关于如何在项目中自定义前端错误输出的反馈和示例。

↑ 目录

重新创建验证器

太好了,我们去掉了结构体中带有名称的字段。我们只需覆盖它们的输出,这样验证器就会查看json:"..."结构体中的参数,而不是它的实际名称。

为了完成此操作,我们使用了RegisterTagNameFunc包中内置的方法,并添加了一点小技巧。我会将其放在另一个辅助包中(./pkg/utilities/validator.go),以便提高应用程序代码的可读性:

// ./pkg/utilities/validator.go

// NewValidator func for create a new validator for struct fields.
func NewValidator() *validator.Validate {
    // Create a new validator.
    validate := validator.New()

    // Rename struct fields to JSON.
    validate.RegisterTagNameFunc(func(fl reflect.StructField) string {
        name := strings.SplitN(fl.Tag.Get("json"), ",", 2)
        if name[1] == "-" {
            return ""
        }
        return name[0]
    })

    return validate
}

// ...
Enter fullscreen mode Exit fullscreen mode

如果您不想重命名任何字段,请,-在其 JSON 名称末尾添加(逗号 + 破折号),如下所示:

WebsiteURL string `db:"website_url" json:"website_url,-" validate:"uri"`
Enter fullscreen mode Exit fullscreen mode

是的,你没看错,这种方法为自定义错误输出本身提供了极大的可能性。你可以不依赖json:"..."字段中的属性,而是依赖你自己的属性,field_name:"..."或者任何其他你想要的属性。

☝️注意:要了解其工作原理,请关注问题。

↑ 目录

检查验证错误的函数

让我们继续。现在是时候让验证错误输出更美观了,这样你团队的前端开发人员就会感谢你了。

在以 JSON 格式实现 REST API 供内部使用(例如,对于单页应用程序,又称 SPA)时,我总是使用这种做法:

  1. 我们返回的 JSON 格式与前端严格一致,但相对于交互对象;
  2. 后端响应的状态码始终为HTTP 200 OK,除非涉及服务器错误(状态码5XX);
  3. 服务器响应始终包含指示实际状态代码的status字段(type );int
  4. 如果发生错误(状态码不是2ХХ),服务器响应始终包含一个字段msg(类型string),其中简要指示错误原因;

此外,在下面的示例中,我从使用Fiber Web 框架编写的项目中获取了代码。因此,其中包含了其库中的一些元素。不要害怕,这里的关键是理解验证本身的原理。

☝️注意:如果你想了解更多关于 Fiber 的知识,我有一系列文章可以帮助你。如果你以后也能学习一下,我会很高兴的。

好的,我检查验证错误的函数如下所示:

// ./pkg/utilities/validator.go

// ...

// CheckForValidationError func for checking validation errors in struct fields.
func CheckForValidationError(ctx *fiber.Ctx, errFunc error, statusCode int, object string) error {
    if errFunc != nil {
        return ctx.JSON(&fiber.Map{
            "status": statusCode,
            "msg":    fmt.Sprintf("validation errors for the %s fields", object),
            "fields": ValidatorErrors(errFunc),
        })
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

这个函数的原理很简单:

  • 接受 Fiber 上下文,以便能够与所出现的上下文协同工作;
  • 接受上面定义的具有验证错误的对象;
  • 接受状态代码,如果发生错误则应该返回;
  • 接受我们当前正在检查的对象(或模型)的名称,以便我们可以输出更易读的错误消息;
  • 返回生成的 JSON 以及所有必要的错误和解释或nil

我现在可以轻松地CheckForValidationError在控制器中使用该功能:

// ./app/controllers/project_controller.go

import (
    // ...
    "github.com/my-user/my-repo/pkg/utilities" 
    // --> add local package `utilities`
)

// CreateNewProject func for create a new project.
func CreateNewProject(c *fiber.Ctx) error {

    // ...

    // Create a new validator, using helper function.
    validate := utilities.NewValidator()

    // Validate all incomming fields for rules in Project struct.
    if err := validate.Struct(project); err != nil {
        // Returning error in JSON format with status code 400 (Bad Request).
        return utilities.CheckForValidationError(
            c, err, fiber.StatusBadRequest, "project",
        )
    }

    // ...
}
Enter fullscreen mode Exit fullscreen mode

↑ 目录

自定义验证标签

有时,内置的验证标签(或者更确切地说,特定字段的验证规则)可能不够用。为了解决这个问题,go-playground/validator包的作者提供了一种特殊的方法。

让我们通过一个简单的例子来考虑它的用法👇

因此,我们有一个使用google/uuiduuid.UUID包创建的类型的字段,我们希望使用该包的内置验证器进行检查。我们需要做的就是函数(如上所述)添加一个新方法,并编写简单的逻辑代码:uuid.Parse()RegisterValidationNewValidator

// ./pkg/utilities/validator.go

// NewValidator func for create a new validator for struct fields.
func NewValidator() *validator.Validate {
    // Create a new validator.
    validate := validator.New()

    // ...

    // Custom validation for uuid.UUID fields.
    _ = validate.RegisterValidation("uuid", func(fl validator.FieldLevel) bool {
        field := fl.Field().String() // convert to string
        if _, err := uuid.Parse(field); err != nil {
            return true // field has error
        }
        return false // field has no error
    })

    // ...

    return validate
}
Enter fullscreen mode Exit fullscreen mode

就是这样!如果字段通过了验证,它将返回false逻辑值,如果有任何错误,它将返回true

☝️注意:该方法RegisterValidation应该这样阅读和理解:“请检查带有验证标签的字段的值是否有错误uuid ”。

现在我们可以像这样验证这种类型的字段:

// ./app/models/something_model.go

// MyStruct struct to describe something.
type MyStruct struct {
    ID uuid.UUID `db:"id" json:"id" validate:"uuid"` 
    // --> verify that the field is a valid UUID
}
Enter fullscreen mode Exit fullscreen mode

↑ 目录

覆盖错误消息

覆盖错误消息

现在到了最精彩的部分。覆盖验证错误消息本身。

☝️注意:请遵循代码中的注释以便更好地理解。

这个辅助函数会将所有验证错误映射到每个字段,然后简单地将该映射传递给CheckForValidationError函数(我们在上一节中描述过):

// ./pkg/utilities/validator.go

// ...

// ValidatorErrors func for show validation errors for each invalid fields.
func ValidatorErrors(err error) map[string]string {
    // Define variable for error fields.
    errFields := map[string]string{}

    // Make error message for each invalid field.
    for _, err := range err.(validator.ValidationErrors) {
        // Get name of the field's struct.
        structName := strings.Split(err.Namespace(), ".")[0]
        // --> first (0) element is the founded name

        // Append error message to the map, where key is a field name,
        // and value is an error description.
        errFields[err.Field()] = fmt.Sprintf(
            "failed '%s' tag check (value '%s' is not valid for %s struct)",
            err.Tag(), err.Value(), structName,
        )
    }

    return errFields
}
Enter fullscreen mode Exit fullscreen mode

您可能已经注意到,为了覆盖字段错误消息,我们对包的作者提供的特殊变量( err.Namespace()err.Field()和)进行操作err.Tag()err.Value()go-playground/validator

☝️注意:您可以在此处找到所有可用内容的完整列表

现在,当我们发出无效请求时,我们将收到此消息:

{
  "status": 400,
  "msg": "validation errors for the project fields",
  "fields": {
    "category": "failed 'required' tag check (value '' is not valid for Project struct)",
    "description": "failed 'required' tag check (value '' is not valid for Project struct)",
    "tags": "failed 'len' tag check (value '[one two]' is not valid for Project struct)"
    "title": "failed 'required' tag check (value '' is not valid for Project struct)"
    "website_url": "failed 'uri' tag check (value 'not-valid-uri' is not valid for Project struct)"
  }
}
Enter fullscreen mode Exit fullscreen mode

☝️注意:验证后,所有无效字段均按字母顺序排列,而不是按 Go 结构定义的顺序排列。

太棒了!🎉 我们得到了想要的东西,没有人受伤。相反,每个人都很高兴,无论是在前端还是后端。

↑ 目录

照片和视频来自

聚苯乙烯

如果你想在本博客上看到更多类似的文章,请在下方留言并订阅我。谢谢!😻

❗️ 您可以在Boosty上支持我,可以是永久支持,也可以是一次性支持。所有收益都将用于支持我的开源项目,并激励我为社区创作新的产品和文章。

在 Boosty 上支持我

当然,你也可以帮助我改善开发者的生活!只需以贡献者的身份连接到我的一个项目即可。非常简单!

我的主要项目需要您的帮助(和星星)👇

  • 🔥 gowebly:下一代 CLI 工具,可轻松使用 Go 在后端创建出色的 Web 应用程序,使用 htmx、hyperscript 或 Alpine.js 以及前端最流行的 CSS 框架。
  • create-go-app:通过运行一个 CLI 命令,创建一个具有 Go 后端、前端和部署自动化的新的生产就绪项目。

我的其他小项目:yatrgosljson2csvcsv2api

错误信息:https://dev.to/koddr/how-to-make-clear-pretty-error-messages-from-the-go-backend-to-your-frontend-21b2
PREV
如何从 GitHub 永久删除泄露的 .env 文件...
NEXT
📖 通过示例了解 Go Fiber:Fiber Web 框架如何发挥作用?