[Golang修仙之路] kitex使用validate

111 阅读5分钟

简介

validate 是一个进行参数基础校验的工具,也是通过protobuf生成代码,kitex也集成了这个工具(看起来比较像是对开源项目做了客制化),但是由于官方文档写的实在是一言难尽,因此在这里我写一个我的实战的demo,防止以后我自己忘了。

kitex 官方文档关于 validator 的描述:cloudwego.cn/zh/docs/kit…

kitex 官方的github仓库(我最终是仔细看了他的example,才调试成功的):github.com/cloudwego/p…

安装:

go install github.com/cloudwego/protoc-gen-validator@latest

踩坑点1: 代码生成在哪里?

好了现在你有一个空项目,你给自己的项目通过go mod 起名为 github.com/yzc/example

go mod init github.com/yzc/example

现在的文件结构是这样:

➜  example tree
.
├── Makefile
├── app
│   └── review_server
│       └── go.mod
└── idl
    ├── api.proto
    └── review.proto

4 directories, 4 files
  • Makefile 不是必须的,只不过是可以帮你不用每次都输入复杂的命令(换言之,你如果可以每次都输入复杂的命令来生成代码,可以完全不用Makefile)

然后创建了一个idl文件夹,文件夹下面有一个你自己定义的.proto文件:

syntax = "proto3";

package review;

option go_package = "github.com/yzc/example/kitex_gen/review";

import "api.proto";

message CreateReviewRequest {
   int64 UserId = 1 [(api.vt).gt="0"];
   int64 OrderId = 2 [(api.vt).gt="0"];
   int32 ItemScore = 3 [(api.vt)={in:["1", "2", "3", "4", "5"]}];
   int32 ServiceScore = 4 [(api.vt)={in:["1", "2", "3", "4", "5"]}];
   int32 ExpressScore = 5 [(api.vt)={in:["1", "2", "3", "4", "5"]}];
   string content = 6;
   string picInfo = 7;
   string videoInfo = 8;
   bool anonymous = 9;
}

message CreateReviewResponse {
   int64 reviewId = 1;
}

service ReviewService {
   rpc CreateReview(CreateReviewRequest) returns(CreateReviewResponse) {}
}
  • 首先这个go_package十分重要,我们的命令如果仅仅是一个基础命令(这个命令真就是官方文档中的最简单版本),如下:
.PHONY: gen-validator
gen-validator:
	@cd app/review_server && protoc \
		-I ../../idl \
		--go_out=. \
		--validator_out=. \
		../../idl/review.proto

那么我们来看一下生成的代码会是在哪个位置的:

image.png

可以看到,他就直接在命令执行的目录下,生成了go_package指定的声明的目录层级结构。这显然不是我想要的。

我要干什么呢?

我要告诉kitex,我的 go mod 是 github.com/yzc/example 也就是生成目录的根目录,应该是go.mod的上级目录,在这个案例中就是review_server。但是在根目录下,并不需要再为 github.com/yzc/example 创建目录,因为这仅仅是go mod 的名字。

于是按照官方文档,我在命令中加上2行:

.PHONY: gen-validator
gen-validator:
	@cd app/review_server && protoc \
		-I ../../idl \
		--go_out=. \
		--go_opt=module=github.com/yzc/example \
		--validator_out=. \
		--validator_opt=module=github.com/yzc/example \
		../../idl/review.proto
  • 我理解这两行的意思是,告诉这个代码生成框架,go module 的名字是:github.com/yzc/example 你只需要到 go.mod 的同级目录下,找到 go_package 去掉以 github.com/yzc/example 为前缀的部分的目录(kitex_gen/review),并在该目录下生成代码即可。

注意这个go_package:option go_package = "github.com/yzc/example/kitex_gen/review";

可以看到代码生成的位置,也是符合预期的:

image.png

踩坑点2: api文件有误,导致Validate 方法只有return nil

错误展示:

image.png

解决办法,我告诉你哈,直接复制下面这个作为api.proto, 你唯一需要修改的地方是:option go_package 这是官方github仓库example的api.proto,不用担心,不用自己编辑,我们的目的是用上validator

syntax = "proto2";

package api;

import "google/protobuf/descriptor.proto";

option go_package = "github.com/yzc/example/kitex_gen/api";

//本文件是简化版的byteapi protobuf idl注解

message FieldRules {
  optional string const = 1;
  optional string lt = 2;
  optional string le = 3;
  optional string gt = 4;
  optional string ge = 5;
  repeated string in = 6;
  repeated string not_in = 7;
  optional string len = 8;
  optional string min_size = 9;
  optional string max_size = 10;
  optional string pattern  = 11;
  optional string prefix   = 12;
  optional string suffix   = 13;
  optional string contains = 14;
  optional string not_contains = 15;
  optional string defined_only = 16;
  optional string no_sparse = 17;
  optional FieldRules key = 18;
  optional FieldRules value = 19;
  optional FieldRules elem = 20;
  optional string skip = 21;
  optional string required = 22;
  optional string not_nil = 23;
  optional string assert = 24;
}

extend google.protobuf.FieldOptions {
  optional string raw_body = 50101;
  optional string query = 50102;
  optional string header = 50103;
  optional string cookie = 50104;
  optional string body = 50105;
  optional string path = 50106;
  optional string vd = 50107;
  optional string form = 50108;
  optional string go_tag = 51001;
  optional FieldRules vt = 50110;
}

extend google.protobuf.MethodOptions {
  optional string get = 50201;
  optional string post = 50202;
  optional string put = 50203;
  optional string delete = 50204;
  optional string patch = 50205;
  optional string options = 50206;
  optional string head = 50207;
  optional string any = 50208;
  optional string re_get = 50211;
  optional string re_post = 50212;
  optional string re_put = 50213;
  optional string re_delete = 50214;
  optional string re_patch = 50215;
  optional string re_options = 50216;
  optional string re_head = 50217;
  optional string re_any = 50218;
  optional string gen_path = 50301; //客户端代码生成时用户指定的path,优先级高于api_version
  optional string api_version = 50302; //客户端代码生成时,指定path中的:version变量值
  optional string tag = 50303;  // rpc标签,可以是多个,逗号分隔
  optional string name = 50304; // rpc的名字
  optional string api_level = 50305;  //接口等级
  optional string serializer = 50306; //序列化方式, json
  optional string param = 50307;      //客户端请求是否带上公共参数
  optional string baseurl = 50308;    // ttnet选路时使用的baseurl
  optional string handler_path = 50309; // handler_path specifies the path to generate the method
}

extend google.protobuf.EnumValueOptions {
  optional int32 http_code = 50401;
}

extend google.protobuf.MessageOptions {
  optional FieldRules msg_vt = 50111;
}

正确生成的validate文件:

func (m *CreateReviewRequest) Validate() error {
	if m.GetUserId() <= int64(0) {
		return fmt.Errorf("field UserId gt rule failed, current value: %v", m.GetUserId())
	}
	if m.GetOrderId() <= int64(0) {
		return fmt.Errorf("field OrderId gt rule failed, current value: %v", m.GetOrderId())
	}
	_src := []int32{int32(1), int32(2), int32(3), int32(4), int32(5)}

	var _exist bool
	for _, src := range _src {
		if m.GetItemScore() == int32(src) {
			_exist = true
			break
		}
	}
	if !_exist {
		return fmt.Errorf("field ItemScore in rule failed, current value: %v", m.GetItemScore())
	}
	_src1 := []int32{int32(1), int32(2), int32(3), int32(4), int32(5)}

	var _exist1 bool
	for _, src := range _src1 {
		if m.GetServiceScore() == int32(src) {
			_exist1 = true
			break
		}
	}
	if !_exist1 {
		return fmt.Errorf("field ServiceScore in rule failed, current value: %v", m.GetServiceScore())
	}
	_src2 := []int32{int32(1), int32(2), int32(3), int32(4), int32(5)}

	var _exist2 bool
	for _, src := range _src2 {
		if m.GetExpressScore() == int32(src) {
			_exist2 = true
			break
		}
	}
	if !_exist2 {
		return fmt.Errorf("field ExpressScore in rule failed, current value: %v", m.GetExpressScore())
	}
	return nil
}

解决bug的收获

勇于开展简单的实验,可以很快从实践中弄清楚是怎么回事。别懒,别想着直接在自己的项目上成功,有时候搞一个最小demo,把最小demo跑通了,你也懂了。