简介
在使用grpc库时候 ,很多时候我们需要对反序列化的参数进行校验,代码中有很多参数校验的代码,如果手动实现,会非常繁琐,对于grpc来说,在定义proto的时候使用直接定义参数的限制规则是一种更合理、更优雅的方式,插件protoc-gen-validate就是来帮助我们实现这一功能的,使用 proto-gen-validate 生成后的代码进行参数校验,我们可以通过在 proto 中编写参数校验规则,然后生成代码,通过中间件自动的进行校验。
安装
go install github.com/envoyproxy/protoc-gen-validate@latest
规则示例
下面为大家列举几种常用类型的参数校验示例,更多的示例可以在 proto-gen-validate 文档中查看。
数字类型
// 参数必须大于 0
int64 id = 1 [(validate.rules).int64 = {gt: 0}];
// 参数必须在 0 到 120 之间
int32 age = 2 [(validate.rules).int64 = {gt:0, lte: 120}];
// 参数是 1 或 2 或 3
uint32 code = 3 [(validate.rules).uint32 = {in: [1,2,3]}];
// 参数不能是 0 或 99.99
float score = 1 [(validate.rules).float = {not_in: [0, 99.99]}];
布尔类型
// 参数必须为 true
bool state = 5 [(validate.rules).bool.const = true];
// 参数必须为 false
bool state = 5 [(validate.rules).bool.const = false];
文本类型
// 参数必须为 /hello
string path = 6 [(validate.rules).string.const = "/hello"];
// 参数文本长度必须为 11
string phone = 7 [(validate.rules).string.len = 11];
// 参数文本长度不能小于 10 个字符
string explain = 8 [(validate.rules).string.min_len = 10];
// 参数文本长度不能小于 1 个字符并且不能大于 10 个字符
string name = 9 [(validate.rules).string = {min_len: 1, max_len: 10}];
// 参数文本使用正则匹配,匹配必须是非空的不区分大小写的十六进制字符串
string card = 10 [(validate.rules).string.pattern = "(?i)^[0-9a-f]+$"];
// 参数文本必须是 email 格式
string email = 11 [(validate.rules).string.email = true];
消息体
// 参数为必填项
Info info = 11 [(validate.rules).message.required = true];
message Info {string address = 1;
}
定义proto文件
syntax = "proto3";package examplepb;
option go_package = "./example";
import "validate/validate.proto";message Person {uint64 id = 1 [(validate.rules).uint64.gt = 999];string email = 2 [(validate.rules).string.email = true];string name = 3 [(validate.rules).string = {pattern: "^[^[0-9]A-Za-z]+( [^[0-9]A-Za-z]+)*$",max_bytes: 256,}];Location home = 4 [(validate.rules).message.required = true];
// 参数必须大于 0
int64 ids = 5 [(validate.rules).int64 = {gt: 0}];
// 参数必须在 0 到 120 之间
int32 age = 6 [(validate.rules).int32 = {gt:0, lte: 120}];
// 参数是 1 或 2 或 3
uint32 code = 7 [(validate.rules).uint32 = {in: [1,2,3]}];
// 参数不能是 0 或 99.99
float score = 8 [(validate.rules).float = {not_in: [0, 99.99]}];message Location {double lat = 1 [(validate.rules).double = { gte: -90, lte: 90 }];double lng = 2 [(validate.rules).double = { gte: -180, lte: 180 }];}
}
使用命令生成go文件
% protoc \ -I . \--plugin=$GOPATH/bin/protoc-gen-validate \-I ${GOPATH}/pkg/mod/github.com/envoyproxy/protoc-gen-validate@v0.1.0/ \--go_out=":./generated" \--validate_out="lang=go:./generated" \example.proto
相应的,我们得到了两个文件
learn/pgv/generated/example/example.pb.go
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v3.19.4
// source: example.protopackage exampleimport (_ "github.com/envoyproxy/protoc-gen-validate/validate"protoreflect "google.golang.org/protobuf/reflect/protoreflect"protoimpl "google.golang.org/protobuf/runtime/protoimpl"reflect "reflect"sync "sync"
)const (// Verify that this generated code is sufficiently up-to-date._ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)// Verify that runtime/protoimpl is sufficiently up-to-date._ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)type Person struct {state protoimpl.MessageStatesizeCache protoimpl.SizeCacheunknownFields protoimpl.UnknownFieldsId uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"`Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`Home *Person_Location `protobuf:"bytes,4,opt,name=home,proto3" json:"home,omitempty"`// 参数必须大于 0Ids int64 `protobuf:"varint,5,opt,name=ids,proto3" json:"ids,omitempty"`// 参数必须在 0 到 120 之间Age int32 `protobuf:"varint,6,opt,name=age,proto3" json:"age,omitempty"`// 参数是 1 或 2 或 3Code uint32 `protobuf:"varint,7,opt,name=code,proto3" json:"code,omitempty"`// 参数不能是 0 或 99.99Score float32 `protobuf:"fixed32,8,opt,name=score,proto3" json:"score,omitempty"`
}func (x *Person) Reset() {*x = Person{}if protoimpl.UnsafeEnabled {mi := &file_example_proto_msgTypes[0]ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))ms.StoreMessageInfo(mi)}
}func (x *Person) String() string {return protoimpl.X.MessageStringOf(x)
}func (*Person) ProtoMessage() {}func (x *Person) ProtoReflect() protoreflect.Message {mi := &file_example_proto_msgTypes[0]if protoimpl.UnsafeEnabled && x != nil {ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))if ms.LoadMessageInfo() == nil {ms.StoreMessageInfo(mi)}return ms}return mi.MessageOf(x)
}// Deprecated: Use Person.ProtoReflect.Descriptor instead.
func (*Person) Descriptor() ([]byte, []int) {return file_example_proto_rawDescGZIP(), []int{0}
}func (x *Person) GetId() uint64 {if x != nil {return x.Id}return 0
}func (x *Person) GetEmail() string {if x != nil {return x.Email}return ""
}func (x *Person) GetName() string {if x != nil {return x.Name}return ""
}func (x *Person) GetHome() *Person_Location {if x != nil {return x.Home}return nil
}func (x *Person) GetIds() int64 {if x != nil {return x.Ids}return 0
}func (x *Person) GetAge() int32 {if x != nil {return x.Age}return 0
}func (x *Person) GetCode() uint32 {if x != nil {return x.Code}return 0
}func (x *Person) GetScore() float32 {if x != nil {return x.Score}return 0
}type Person_Location struct {state protoimpl.MessageStatesizeCache protoimpl.SizeCacheunknownFields protoimpl.UnknownFieldsLat float64 `protobuf:"fixed64,1,opt,name=lat,proto3" json:"lat,omitempty"`Lng float64 `protobuf:"fixed64,2,opt,name=lng,proto3" json:"lng,omitempty"`
}func (x *Person_Location) Reset() {*x = Person_Location{}if protoimpl.UnsafeEnabled {mi := &file_example_proto_msgTypes[1]ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))ms.StoreMessageInfo(mi)}
}func (x *Person_Location) String() string {return protoimpl.X.MessageStringOf(x)
}func (*Person_Location) ProtoMessage() {}func (x *Person_Location) ProtoReflect() protoreflect.Message {mi := &file_example_proto_msgTypes[1]if protoimpl.UnsafeEnabled && x != nil {ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))if ms.LoadMessageInfo() == nil {ms.StoreMessageInfo(mi)}return ms}return mi.MessageOf(x)
}// Deprecated: Use Person_Location.ProtoReflect.Descriptor instead.
func (*Person_Location) Descriptor() ([]byte, []int) {return file_example_proto_rawDescGZIP(), []int{0, 0}
}func (x *Person_Location) GetLat() float64 {if x != nil {return x.Lat}return 0
}func (x *Person_Location) GetLng() float64 {if x != nil {return x.Lng}return 0
}var File_example_proto protoreflect.FileDescriptorvar file_example_proto_rawDesc = []byte{0x0a, 0x0d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,0x09, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x70, 0x62, 0x1a, 0x17, 0x76, 0x61, 0x6c, 0x69,0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72,0x6f, 0x74, 0x6f, 0x22, 0xb5, 0x03, 0x0a, 0x06, 0x50, 0x65, 0x72, 0x73, 0x6f, 0x6e, 0x12, 0x1a,0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x42, 0x0a, 0xba, 0xe9, 0xc0, 0x03,0x05, 0x32, 0x03, 0x20, 0xe7, 0x07, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x05, 0x65, 0x6d,0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x09, 0xba, 0xe9, 0xc0, 0x03, 0x04,0x72, 0x02, 0x60, 0x01, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x12, 0x44, 0x0a, 0x04, 0x6e,0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x30, 0xba, 0xe9, 0xc0, 0x03, 0x2b,0x72, 0x29, 0x28, 0x80, 0x02, 0x32, 0x24, 0x5e, 0x5b, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x5d, 0x41,0x2d, 0x5a, 0x61, 0x2d, 0x7a, 0x5d, 0x2b, 0x28, 0x20, 0x5b, 0x5e, 0x5b, 0x30, 0x2d, 0x39, 0x5d,0x41, 0x2d, 0x5a, 0x61, 0x2d, 0x7a, 0x5d, 0x2b, 0x29, 0x2a, 0x24, 0x52, 0x04, 0x6e, 0x61, 0x6d,0x65, 0x12, 0x3a, 0x0a, 0x04, 0x68, 0x6f, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32,0x1a, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x70, 0x62, 0x2e, 0x50, 0x65, 0x72, 0x73,0x6f, 0x6e, 0x2e, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0a, 0xba, 0xe9, 0xc0,0x03, 0x05, 0x8a, 0x01, 0x02, 0x10, 0x01, 0x52, 0x04, 0x68, 0x6f, 0x6d, 0x65, 0x12, 0x1b, 0x0a,0x03, 0x69, 0x64, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x42, 0x09, 0xba, 0xe9, 0xc0, 0x03,0x04, 0x22, 0x02, 0x20, 0x00, 0x52, 0x03, 0x69, 0x64, 0x73, 0x12, 0x1d, 0x0a, 0x03, 0x61, 0x67,0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0b, 0xba, 0xe9, 0xc0, 0x03, 0x06, 0x1a, 0x04,0x18, 0x78, 0x20, 0x00, 0x52, 0x03, 0x61, 0x67, 0x65, 0x12, 0x21, 0x0a, 0x04, 0x63, 0x6f, 0x64,0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x0d, 0xba, 0xe9, 0xc0, 0x03, 0x08, 0x2a, 0x06,0x30, 0x01, 0x30, 0x02, 0x30, 0x03, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x27, 0x0a, 0x05,0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x02, 0x42, 0x11, 0xba, 0xe9, 0xc0,0x03, 0x0c, 0x0a, 0x0a, 0x3d, 0x00, 0x00, 0x00, 0x00, 0x3d, 0xe1, 0xfa, 0xc7, 0x42, 0x52, 0x05,0x73, 0x63, 0x6f, 0x72, 0x65, 0x1a, 0x64, 0x0a, 0x08, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f,0x6e, 0x12, 0x2b, 0x0a, 0x03, 0x6c, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x01, 0x42, 0x19,0xba, 0xe9, 0xc0, 0x03, 0x14, 0x12, 0x12, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x56, 0x40,0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x56, 0xc0, 0x52, 0x03, 0x6c, 0x61, 0x74, 0x12, 0x2b,0x0a, 0x03, 0x6c, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x42, 0x19, 0xba, 0xe9, 0xc0,0x03, 0x14, 0x12, 0x12, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x66, 0x40, 0x29, 0x00, 0x00,0x00, 0x00, 0x00, 0x80, 0x66, 0xc0, 0x52, 0x03, 0x6c, 0x6e, 0x67, 0x42, 0x0b, 0x5a, 0x09, 0x2e,0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}var (file_example_proto_rawDescOnce sync.Oncefile_example_proto_rawDescData = file_example_proto_rawDesc
)func file_example_proto_rawDescGZIP() []byte {file_example_proto_rawDescOnce.Do(func() {file_example_proto_rawDescData = protoimpl.X.CompressGZIP(file_example_proto_rawDescData)})return file_example_proto_rawDescData
}var file_example_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_example_proto_goTypes = []interface{}{(*Person)(nil), // 0: examplepb.Person(*Person_Location)(nil), // 1: examplepb.Person.Location
}
var file_example_proto_depIdxs = []int32{1, // 0: examplepb.Person.home:type_name -> examplepb.Person.Location1, // [1:1] is the sub-list for method output_type1, // [1:1] is the sub-list for method input_type1, // [1:1] is the sub-list for extension type_name1, // [1:1] is the sub-list for extension extendee0, // [0:1] is the sub-list for field type_name
}func init() { file_example_proto_init() }
func file_example_proto_init() {if File_example_proto != nil {return}if !protoimpl.UnsafeEnabled {file_example_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {switch v := v.(*Person); i {case 0:return &v.statecase 1:return &v.sizeCachecase 2:return &v.unknownFieldsdefault:return nil}}file_example_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {switch v := v.(*Person_Location); i {case 0:return &v.statecase 1:return &v.sizeCachecase 2:return &v.unknownFieldsdefault:return nil}}}type x struct{}out := protoimpl.TypeBuilder{File: protoimpl.DescBuilder{GoPackagePath: reflect.TypeOf(x{}).PkgPath(),RawDescriptor: file_example_proto_rawDesc,NumEnums: 0,NumMessages: 2,NumExtensions: 0,NumServices: 0,},GoTypes: file_example_proto_goTypes,DependencyIndexes: file_example_proto_depIdxs,MessageInfos: file_example_proto_msgTypes,}.Build()File_example_proto = out.Filefile_example_proto_rawDesc = nilfile_example_proto_goTypes = nilfile_example_proto_depIdxs = nil
}
learn/pgv/generated/example/example.pb.validate.go
// Code generated by protoc-gen-validate. DO NOT EDIT.
// source: example.protopackage exampleimport ("bytes""errors""fmt""net""net/mail""net/url""regexp""strings""time""unicode/utf8""github.com/golang/protobuf/ptypes"
)// ensure the imports are used
var (_ = bytes.MinRead_ = errors.New("")_ = fmt.Print_ = utf8.UTFMax_ = (*regexp.Regexp)(nil)_ = (*strings.Reader)(nil)_ = net.IPv4len_ = time.Duration(0)_ = (*url.URL)(nil)_ = (*mail.Address)(nil)_ = ptypes.DynamicAny{}
)// Validate checks the field values on Person with the rules defined in the
// proto definition for this message. If any rules are violated, an error is returned.
func (m *Person) Validate() error {if m == nil {return nil}if m.GetId() <= 999 {return PersonValidationError{field: "Id",reason: "value must be greater than 999",}}if err := m._validateEmail(m.GetEmail()); err != nil {return PersonValidationError{field: "Email",reason: "value must be a valid email address",cause: err,}}if len(m.GetName()) > 256 {return PersonValidationError{field: "Name",reason: "value length must be at most 256 bytes",}}if !_Person_Name_Pattern.MatchString(m.GetName()) {return PersonValidationError{field: "Name",reason: "value does not match regex pattern \"^[^[0-9]A-Za-z]+( [^[0-9]A-Za-z]+)*$\"",}}if m.GetHome() == nil {return PersonValidationError{field: "Home",reason: "value is required",}}if v, ok := interface{}(m.GetHome()).(interface{ Validate() error }); ok {if err := v.Validate(); err != nil {return PersonValidationError{field: "Home",reason: "embedded message failed validation",cause: err,}}}if m.GetIds() <= 0 {return PersonValidationError{field: "Ids",reason: "value must be greater than 0",}}if val := m.GetAge(); val <= 0 || val > 120 {return PersonValidationError{field: "Age",reason: "value must be inside range (0, 120]",}}if _, ok := _Person_Code_InLookup[m.GetCode()]; !ok {return PersonValidationError{field: "Code",reason: "value must be in list [1 2 3]",}}if _, ok := _Person_Score_NotInLookup[m.GetScore()]; ok {return PersonValidationError{field: "Score",reason: "value must not be in list [0 99.99]",}}return nil
}func (m *Person) _validateHostname(host string) error {s := strings.ToLower(strings.TrimSuffix(host, "."))if len(host) > 253 {return errors.New("hostname cannot exceed 253 characters")}for _, part := range strings.Split(s, ".") {if l := len(part); l == 0 || l > 63 {return errors.New("hostname part must be non-empty and cannot exceed 63 characters")}if part[0] == '-' {return errors.New("hostname parts cannot begin with hyphens")}if part[len(part)-1] == '-' {return errors.New("hostname parts cannot end with hyphens")}for _, r := range part {if (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '-' {return fmt.Errorf("hostname parts can only contain alphanumeric characters or hyphens, got %q", string(r))}}}return nil
}func (m *Person) _validateEmail(addr string) error {a, err := mail.ParseAddress(addr)if err != nil {return err}addr = a.Addressif len(addr) > 254 {return errors.New("email addresses cannot exceed 254 characters")}parts := strings.SplitN(addr, "@", 2)if len(parts[0]) > 64 {return errors.New("email address local phrase cannot exceed 64 characters")}return m._validateHostname(parts[1])
}// PersonValidationError is the validation error returned by Person.Validate if
// the designated constraints aren't met.
type PersonValidationError struct {field stringreason stringcause errorkey bool
}// Field function returns field value.
func (e PersonValidationError) Field() string { return e.field }// Reason function returns reason value.
func (e PersonValidationError) Reason() string { return e.reason }// Cause function returns cause value.
func (e PersonValidationError) Cause() error { return e.cause }// Key function returns key value.
func (e PersonValidationError) Key() bool { return e.key }// ErrorName returns error name.
func (e PersonValidationError) ErrorName() string { return "PersonValidationError" }// Error satisfies the builtin error interface
func (e PersonValidationError) Error() string {cause := ""if e.cause != nil {cause = fmt.Sprintf(" | caused by: %v", e.cause)}key := ""if e.key {key = "key for "}return fmt.Sprintf("invalid %sPerson.%s: %s%s",key,e.field,e.reason,cause)
}var _ error = PersonValidationError{}var _ interface {Field() stringReason() stringKey() boolCause() errorErrorName() string
} = PersonValidationError{}var _Person_Name_Pattern = regexp.MustCompile("^[^[0-9]A-Za-z]+( [^[0-9]A-Za-z]+)*$")var _Person_Code_InLookup = map[uint32]struct{}{1: {},2: {},3: {},
}var _Person_Score_NotInLookup = map[float32]struct{}{0: {},99.99: {},
}// Validate checks the field values on Person_Location with the rules defined
// in the proto definition for this message. If any rules are violated, an
// error is returned.
func (m *Person_Location) Validate() error {if m == nil {return nil}if val := m.GetLat(); val < -90 || val > 90 {return Person_LocationValidationError{field: "Lat",reason: "value must be inside range [-90, 90]",}}if val := m.GetLng(); val < -180 || val > 180 {return Person_LocationValidationError{field: "Lng",reason: "value must be inside range [-180, 180]",}}return nil
}// Person_LocationValidationError is the validation error returned by
// Person_Location.Validate if the designated constraints aren't met.
type Person_LocationValidationError struct {field stringreason stringcause errorkey bool
}// Field function returns field value.
func (e Person_LocationValidationError) Field() string { return e.field }// Reason function returns reason value.
func (e Person_LocationValidationError) Reason() string { return e.reason }// Cause function returns cause value.
func (e Person_LocationValidationError) Cause() error { return e.cause }// Key function returns key value.
func (e Person_LocationValidationError) Key() bool { return e.key }// ErrorName returns error name.
func (e Person_LocationValidationError) ErrorName() string { return "Person_LocationValidationError" }// Error satisfies the builtin error interface
func (e Person_LocationValidationError) Error() string {cause := ""if e.cause != nil {cause = fmt.Sprintf(" | caused by: %v", e.cause)}key := ""if e.key {key = "key for "}return fmt.Sprintf("invalid %sPerson_Location.%s: %s%s",key,e.field,e.reason,cause)
}var _ error = Person_LocationValidationError{}var _ interface {Field() stringReason() stringKey() boolCause() errorErrorName() string
} = Person_LocationValidationError{}
使用示例
然后我们就可以通过Validate方法来进行验证
结合之前博客介绍的拦截器进行校验:
// ValidateAll 对应 protoc-gen-validate 生成的 *.pb.validate.go 中的代码
type Validator interface {ValidateAll() error
}func ServerValidationUnaryInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {if r, ok := req.(Validator); ok {if err := r.ValidateAll(); err != nil {return nil, status.Error(codes.InvalidArgument, err.Error())}}return handler(ctx, req)
}
然后在拦截器中引入我们定义的插件:
s := grpc.NewServer(grpc.ChainUnaryInterceptor(grpc_opentracing.UnaryServerInterceptor(grpc_opentracing.WithTracer(opentracing.GlobalTracer()),),ServerValidationUnaryInterceptor,),
)