protovalidate-go 调研&应用

背景:

  • protovalidate-go 库支持在proto 文件中自定义返回的message
  • protoc-gen-validate(PGV ) 已进入维护阶段,也需要考虑技术问题

使用:

  • 下载:https://github.com/bufbuild/protovalidate/blob/main/proto/protovalidate/buf/validate/validate.proto 文件

  • 将该文件放置在proto文件目录中

  • Proto 文件样例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    syntax = "proto3";

    package my.package;

    import "google/protobuf/timestamp.proto";
    import "buf/validate/validate.proto";
    option go_package = "api/v1;v1";
    message Transaction {
    uint64 id = 1 [(buf.validate.field).uint64.gt = 999];
    google.protobuf.Timestamp purchase_date = 2;
    google.protobuf.Timestamp delivery_date = 3;

    string price = 4 [(buf.validate.field).cel = {
    id: "transaction.price",
    message: "price must be positive and include a valid currency symbol ($ or £)",
    expression: "(this.startsWith('$') || this.startsWith('£')) && double(this.substring(1)) > 0"
    }];

    option (buf.validate.message).cel = {
    id: "transaction.delivery_date",
    message: "delivery date must be after purchase date",
    expression: "this.delivery_date > this.purchase_date"
    };
    }
    PROTO

    可以在字段上添加验证规则,如上述示例中的 uint64.gtcel 规则
    也可以在message上添加验证规则,如上述示例中的 cel 规则
    对应的验证规则可以在expression中定义。
    message中的可以定义报错信息

  • 生成代码:

    1
    protoc --proto_path=. --proto_path=third_party --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative   my/package/*.proto
    SHELL
  • 使用示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package main

    import (
    "fmt"
    v1 "protovalidate-go-demo/api/v1"

    "github.com/bufbuild/protovalidate-go"
    )

    func main() {
    msg := &v1.Transaction{
    Id: 123,
    Price: "123.45",
    }
    if err := protovalidate.Validate(msg); err != nil {
    fmt.Println("validation failed:", err)
    } else {
    fmt.Println("validation succeeded")
    }
    }
    GO
  • 运行结果:

    1
    2
    3
    4
    validation failed: validation error:
    - delivery date must be after purchase date [transaction.delivery_date]
    - id: value must be greater than 999 [uint64.gt]
    - price: price must be positive and include a valid currency symbol ($ or £) [transaction.price]
    SHELL

CEL(通用表达式语言) 语法说明

具体可以这里:https://github.com/google/cel-spec/blob/master/doc/langdef.md
TEXT

在这个库中,需要使用 CEL 进行规则编写(很多时候可以问ChatGpt)。
这里展示一些例子:

1
2
3
has(account.user_id) || has(account.gaia_id)    // true if either one is set
size(account.emails) > 0 // true if emails is non-empty
matches(account.phone_number, "[0-9-]+") // true if number matches regexp
GCODE

Kratos 中使用

  • 在Kratos中,可以在中间件中使用该库进行参数校验

中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package validate

import (
"context"
"fmt"

"github.com/go-kratos/kratos/v2/errors"
"github.com/go-kratos/kratos/v2/middleware"

"github.com/bufbuild/protovalidate-go"
"github.com/bufbuild/protovalidate-go/legacy"
"google.golang.org/protobuf/proto"
)

// ProtoValidate is a middleware that validates the request message with [protovalidate](https://github.com/bufbuild/protovalidate)
func ProtoValidate() middleware.Middleware {
validator, err := protovalidate.New(
// Some projects may still be using PGV, turn on legacy support to handle this.
legacy.WithLegacySupport(legacy.ModeMerge),
)
if err != nil {
panic(fmt.Errorf("protovalidate.New() error: %w", err))
}

return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (reply interface{}, err error) {
if msg, ok := req.(proto.Message); ok {
if err := validator.Validate(msg); err != nil {
return nil, errors.BadRequest("VALIDATOR", err.Error()).WithCause(err)
}
}
return handler(ctx, req)
}
}
}
GO

迁移:

官方迁移向导:https://github.com/bufbuild/protovalidate/tree/main/tools/protovalidate-migrate
TEXT
  • 如中间件代码:在初始化validator时增加 legacy.WithLegacySupport(legacy.ModeMerge),就可以兼容validate/validate.proto;
  • 如果不使用迁移,则PGV规则将无法生效

对比PGV优势

  • 可自定义msg内容
  • 可以同时返回多个不符合校验规则的消息,而PGV只能返回一个消息,如果有多个字段出现问题,那则需要多次调试
  • 不会生成额外文件
  • CEL 通用表示语言有很强大的扩展性,而且带有很多函数,实现逻辑判断

参考:


protovalidate-go 调研&应用
https://blog.codefish.net/2025/01/13/package-demo-GoPackage-protovalidate-go/
作者
CodeFish
发布于
2025年1月13日
许可协议