Go微服务框架go-kratos实战学习02:proto 代码生成和项目代码编写步骤
<p>在上一篇 kratos quickstart 文章(https://www.cnblogs.com/jiujuan/p/16322725.html)中,我们直接用 <code>kratos new</code> 命令生成了一个项目。</p><p>这一篇来看看 kratos API 的定义和使用。</p>
<h2 id="一kratos-中-api-简介">一、kratos 中 API 简介</h2>
<h3 id="11-简介">1.1 简介</h3>
<p>API 全称是 Application Programming Interface,应用程序接口。</p>
<p>在 kratos 中,API 指的是 REST API 和 RPC API ,REST API 是用户访问应用程序时的入口,</p>
<p>RPC API 作为应用程序内部相互访问的接口定义。</p>
<p>那怎么定义 API?使用的是 protocol-buffers 这种与编程语言无关的接口自定义语言(IDL),它可以根据定义的 pb 来生成你</p>
<p>所需的编程语言程序。</p>
<p>gRPC 是 Go 语言编写的一个开源的 RPC 框架,它使用的 IDL 就是 protocol-buffers。</p>
<p>protocol-buffers 语法学习可以参考文档:</p>
<ul>
<li>
<p>proto3 语法, https://developers.google.com/protocol-buffers/docs/proto3</p>
<ul>
<li>中译版 (时间有点早2017-03)</li>
</ul>
</li>
<li>
<p>proto2 语法,https://developers.google.com/protocol-buffers/docs/proto</p>
</li>
</ul>
<h2 id="二kratos-中-api-定义和使用">二、kratos 中 API 定义和使用</h2>
<p>下面一步一步实现 api 文件(proto 文件)生成,然后根据 proto 文件生成对应的 pb.http, pb.grpc 代码。</p>
<p>然后生成 service 代码,使用 service 代码。然后编写 biz 代码等等步骤。</p>
<p>来理清 kratos 里代码编写的步骤。毕竟 internal 文件夹里各种 go 文件业务逻辑顺序还是要有些繁琐。</p>
<h3 id="21-快速生成-proto-文件">2.1 快速生成 proto 文件</h3>
<p>在上一篇文章的项目基础上生成一个新的 API(proto 文件)。</p>
<p>先安装 kratos cli :</p>
<pre><code class="language-shell">go install github.com/go-kratos/kratos/cmd/kratos/v2@latest
</code></pre>
<p>kratos cli 工具使用文档:https://go-kratos.dev/docs/getting-started/usage</p>
<p>进入项目 quickstart 目录,运行命令:</p>
<pre><code class="language-shell">kratos proto add api/helloworld/v1/student.proto
</code></pre>
<p>在 api/helloworld/v1 目录先就会出现一个 student.proto 的文件,</p>
<p><img src="https://img2022.cnblogs.com/blog/650581/202206/650581-20220601010448009-1204396885.png" alt="image-20220531122225550" loading="lazy"></p>
<p>里面的代码:</p>
<pre><code class="language-protobuf">syntax = "proto3";
package api.helloworld.v1;
option go_package = "quickstart/api/helloworld/v1;v1";
option java_multiple_files = true;
option java_package = "api.helloworld.v1";
service Student {
rpc CreateStudent (CreateStudentRequest) returns (CreateStudentReply);
rpc UpdateStudent (UpdateStudentRequest) returns (UpdateStudentReply);
rpc DeleteStudent (DeleteStudentRequest) returns (DeleteStudentReply);
rpc GetStudent (GetStudentRequest) returns (GetStudentReply);
rpc ListStudent (ListStudentRequest) returns (ListStudentReply);
}
message CreateStudentRequest {}
message CreateStudentReply {}
message UpdateStudentRequest {}
message UpdateStudentReply {}
message DeleteStudentRequest {}
message DeleteStudentReply {}
message GetStudentRequest {}
message GetStudentReply {}
message ListStudentRequest {}
message ListStudentReply {}
</code></pre>
<p>生成了一个 student.proto 的模板,定义了一些基本操作,Create、Update、Delete、Get、List。</p>
<h3 id="22-给-proto-添加内容">2.2 给 proto 添加内容</h3>
<p>学习 greeter.proto 里的用法,给 student.proto 添加一个简单的 HTTP 转换。</p>
<p><strong>添加一个 hello 的 http 转换接口</strong></p>
<p>第一步:引入 <code>import "google/api/annotations.proto";</code></p>
<p>第二步:在 <code>service Student</code> 里添加代码:</p>
<p>在服务里定义一个 Hello 的操作,然后在里面用 <code>option (google.api.http)</code> 语法,如下:</p>
<pre><code class="language-protobuf">rpc Hello (HelloReq) returns (HelloResp) {
option (google.api.http) = {
get: "/hello/{name}"
};
}
</code></pre>
<p>定义 HelloReq 和 HelloResp:</p>
<p>请求的字段和返回的字段</p>
<pre><code class="language-protobuf">message HelloReq {
string name = 1;
}
message HelloResp {
string message = 1;
}
</code></pre>
<p>上面就是把 HTTP REST 转换为 gRPC :</p>
<table>
<thead>
<tr>
<th>HTTP</th>
<th>gRPC</th>
</tr>
</thead>
<tbody>
<tr>
<td>GET /hello/tom</td>
<td>Hello(name: "tom")</td>
</tr>
</tbody>
</table>
<p>还可以给这个接口添加额外的接口,用 <code>additional_bindings</code>:</p>
<pre><code class="language-protobuf">rpc Hello (HelloReq) returns (HelloResp) {
option (google.api.http) = {
// 定义 GET 接口,把 name 参数映射到 HelloReq
get: "/hello/{name}",
// 添加额外的接口
additional_bindings {
// 定义了一个 POST 接口,并且把 body 映射到了 HelloReq
post: "/hello/{id}/sayhello/{sayname}",
body: "*",
}
};
}
// 这里的 HelloReq 和 HelloResp
message HelloReq {
string name = 1;
string id = 2;
string sayname = 3;
}
message HelloResp {
string message = 1;
string text = 2;
}
</code></pre>
<p>HTTP 转换问 gRPC:</p>
<table>
<thead>
<tr>
<th>HTTP</th>
<th>gRPC</th>
</tr>
</thead>
<tbody>
<tr>
<td>GET /hello/tom</td>
<td>Hello(name: "tom")</td>
</tr>
<tr>
<td>POST /hello/123/sayhello/tom</td>
<td>Hello(id: "123", sayname:"tom" text:"world!")</td>
</tr>
</tbody>
</table>
<h3 id="23-生成-proto-对应代码">2.3 生成 proto 对应代码</h3>
<p>通过 make 命令生成:</p>
<pre><code class="language-shell">make api
</code></pre>
<p>或者通过 kratos cli 生成:</p>
<pre><code class="language-shell">kratos proto client api/helloworld/v1/student.proto
</code></pre>
<p>这里通过 <code>kratos proto client api/helloworld/v1/student.proto</code> 来生成 proto 对应的代码:</p>
<blockquote>
<p>api/helloworld/v1/student.pb.go<br>
api/helloworld/v1/student_grpc.pb.go</p>
<p>// 注意 http 代码只会在 proto 文件中声明了 http 时才会生成</p>
<p>api/helloworld/v1/student_http.pb.go</p>
</blockquote>
<p><img src="https://img2022.cnblogs.com/blog/650581/202206/650581-20220601010448009-1138427468.png" alt="image-20220531183850578" loading="lazy"></p>
<h3 id="24-生成-service-代码">2.4 生成 Service 代码</h3>
<p>通过 proto 文件,直接生成对应的 Service 代码。使用 <code>-t</code> 指定生成目录:</p>
<pre><code class="language-shell">kratos proto server api/helloworld/v1/student.proto -t internal/service
</code></pre>
<p><img src="https://img2022.cnblogs.com/blog/650581/202206/650581-20220601010448020-1761347013.png" alt="image-20220531192634236" loading="lazy"></p>
<p>internal/service/student.go:</p>
<pre><code class="language-go">package service
import (
"context"
pb "quickstart/api/helloworld/v1"
)
type StudentService struct {
pb.UnimplementedStudentServer
}
func NewStudentService() *StudentService {
return &StudentService{}
}
func (s *StudentService) Createstudent(ctx context.Context, req *pb.CreateStudentRequest) (*pb.CreateStudentReply, error) {
return &pb.CreateStudentReply{}, nil
}
func (s *StudentService) Updatestudent(ctx context.Context, req *pb.UpdateStudentRequest) (*pb.UpdateStudentReply, error) {
return &pb.UpdateStudentReply{}, nil
}
func (s *StudentService) Deletestudent(ctx context.Context, req *pb.DeleteStudentRequest) (*pb.DeleteStudentReply, error) {
return &pb.DeleteStudentReply{}, nil
}
func (s *StudentService) Getstudent(ctx context.Context, req *pb.GetStudentRequest) (*pb.GetStudentReply, error) {
return &pb.GetStudentReply{}, nil
}
func (s *StudentService) Liststudent(ctx context.Context, req *pb.ListStudentRequest) (*pb.ListStudentReply, error) {
return &pb.ListStudentReply{}, nil
}
func (s *StudentService) Hello(ctx context.Context, req *pb.HelloReq) (*pb.HelloResp, error) {
return &pb.HelloResp{}, nil
}
</code></pre>
<p>看上面的代码,里面的内容是空的,需要你自己编写相应的代码逻辑。</p>
<p>通过上一篇文章我们知道,service 实现了 api 定义的服务,其实就是 student.proto 里定义的服务。它要把数据传输对象(比如 http request data) 传入到 internal/biz 里进行处理,它一般不会涉及业务逻辑代码。业务逻辑的组装会在 biz 里实现。</p>
<p>有了 service/student.go ,怎么使用?</p>
<h3 id="25-向-wire-中注入-service-代码">2.5 向 wire 中注入 Service 代码</h3>
<p>在 kratos 中,组织代码是用 wire 依赖注入的方式。</p>
<p>在 internal/service/service.go 文件里加上 NewStudentService:</p>
<pre><code class="language-go">var ProviderSet = wire.NewSet(NewStudentService)
</code></pre>
<p>假如我们要通过 http 来访问,那又要怎么做?对,还需要在服务端加 student 服务代码。</p>
<h3 id="26-向server添加代码">2.6 向server添加代码</h3>
<p>向 internal/server/http.go,internal/server/grpc.go 添加服务代码:</p>
<p>在 http.go 中:</p>
<pre><code class="language-go">// 在函数参数中添加 student *service.StudentService
func NewHTTPServer(c *conf.Server, student *service.StudentService, logger log.Logger) *http.Server {
... ...
srv := http.NewServer(opts...)
// v1.RegisterGreeterHTTPServer(srv, greeter)
v1.RegisterStudentHTTPServer(srv, student) // 在 httpserver 上注册 student
return srv
}
</code></pre>
<p>在 grpc.go 中:</p>
<pre><code class="language-go">// 在函数参数中添加 student *service.StudentService
func NewGRPCServer(c *conf.Server,student *service.StudentService, logger log.Logger) *grpc.Server {
... ...
// v1.RegisterGreeterServer(srv, greeter)
v1.RegisterStudentServer(srv, student) // 在 grpcserver 上注册 student
return srv
}
</code></pre>
<p>那需不需要在向 wire 注册后才能使用呢?不需要,在 internal/server/server.go 中已经有了:</p>
<pre><code class="language-go">var ProviderSet = wire.NewSet(NewHTTPServer, NewGRPCServer)
</code></pre>
<p>接下来,接受了参数,是不是要对参数进行相应处理。</p>
<blockquote>
<p>顺序是:service -> biz -> data</p>
</blockquote>
<h3 id="27-业务逻辑-biz">2.7 业务逻辑 biz</h3>
<p>先简单分析下 internal/biz/greeter.go 里的代码。</p>
<pre><code class="language-go">// 定义了一个 Greeter struct,主要内容就是定义 Greeter 的字段
type Greeter struct {
Hello string
}
// 对 Greeter 定义操作接口 GreeterRepo
type GreeterRepo interface {
Save(context.Context, *Greeter) (*Greeter, error)
Update(context.Context, *Greeter) (*Greeter, error)
FindByID(context.Context, int64) (*Greeter, error)
ListByHello(context.Context, string) ([]*Greeter, error)
ListAll(context.Context) ([]*Greeter, error)
}
// 操作加上日志
type GreeterUsecase struct {
repo GreeterRepo
log*log.Helper
}
// 初始化 GreeterUsercase
func NewGreeterUsecase(repo GreeterRepo, logger log.Logger) *GreeterUsecase
// 对 Greeter 的真正操作,用到的方法都是上面 GreeterRepo 定义的
func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {
uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)
return uc.repo.Save(ctx, g)
}
</code></pre>
<p>基本步骤:1.定义 struct,里面包含字段 2.定义操作 struct 的 interface 3.给操作加上日志 4.定义真正执行操作函数</p>
<blockquote>
<p>这里只定义了操作的接口 GreeterRepo interface,里面定义了常规的操作。</p>
<p>而操作接口里定义的操作需要到 data 里实现。</p>
</blockquote>
<p>照葫芦画瓢,在 internal/biz/ 文件夹下新建文件 student.go:</p>
<p>1.定义 struct Student:</p>
<pre><code class="language-go">type Student struct {
ID string
Name string
Sayname string
}
</code></pre>
<p>2.定义对 struct student 的操作接口:</p>
<pre><code class="language-go">type StudentRepo interface {
Save(context.Context, *Student) (*Student, error)
Get(context.Context, *Student) (*Student, error)
}
</code></pre>
<p>3.对 student 的操作加上日志:</p>
<pre><code class="language-go">type StudentUsercase struct {
repo StudentRepo
log*log.Helper
}
</code></pre>
<p>4.初始化 StudentUsercase</p>
<pre><code class="language-go">func NewStudentUsercase(repo StudentRepo, logger log.Logger) *StudentUsercase {
return &StudentUsercase{repo: repo, log: log.NewHelper(logger)}
}
</code></pre>
<p>5.编写 CreateStudent 方法,也就是一些业务逻辑编写</p>
<pre><code class="language-go">func (uc *StudentUsercase) CreateStudent(ctx context.Context, stu *Student) (*Student, error) {
uc.log.WithContext(ctx).Infof("CreateStudent: %v", stu.ID)
return uc.repo.Save(ctx, stu)
}
</code></pre>
<p>biz 里就是完成业务逻辑组装,数据的处理。</p>
<p>6.向 wire 注入 student</p>
<p>internal/biz/biz.go:</p>
<pre><code class="language-go">var ProviderSet = wire.NewSet(NewGreeterUsecase, NewStudentUsercase)
</code></pre>
<p>上面对 struct student 定义了操作的接口,那具体实现在哪里实现?就是在 internal/data 里实现。</p>
<h3 id="28-持久化操作">2.8 持久化操作</h3>
<p>可以仿照 2.7 小结,先看看 internal/data/greeter.go 怎么编写代码的。</p>
<p>greeter.go 里的具体代码就留给读者自己研究了。</p>
<p>下面开始编写 internal/data/student.go 代码。</p>
<p>1.定义持久化的 struct</p>
<pre><code class="language-go">type studentRepo struct {
data *Data // 这里 *Data 是连接数据库客户端
log*log.Helper
}
</code></pre>
<p>2.初始化 studentRepo struct</p>
<pre><code class="language-go">func NewStudentRepo(data *Data, logger log.Logger) biz.StudentRepo {
return &studentRepo{
data: data,
log:log.NewHelper(logger),
}
}
</code></pre>
<p>3.实现接口定义的操作</p>
<p>在 biz/student.go 里的 StudentRepo 接口,定义了 2 个操作 Save、Get,在这里实现,</p>
<pre><code class="language-go">func (repo *studentRepo) Save(ctx context.Context, stu *biz.Student) (*biz.Student, error) {
return stu, nil
}
func (repo *studentRepo) Get(ctx context.Context, stu *biz.Student) (*biz.Student, error) {
return stu, nil
}
</code></pre>
<p>上面是一个实现的模板代码。</p>
<h3 id="29-配置文件">2.9 配置文件</h3>
<p>配置文件是放在 internal/conf 文件夹中,这里放置了配置文件结构的定义文件,使用 <code>.proto</code> 进行配置定义,</p>
<p>然后通过在根目录执行 <code>make config</code> 命令,就可以将对应的 <code>.pb.go</code> 文件生成到同一目录下使用。</p>
<p>在初始状态下,这个 <code>conf.proto</code> 所定义的结构,就是 <code>configs/config.yaml</code> 的接口,请保持两者一致。</p>
<blockquote>
<p>每次修改配置文件后,记得使用 <code>make config</code> 命令重新生成 go 文件。</p>
</blockquote>
<h3 id="210-重新生成-wire_gengo-文件">2.10 重新生成 wire_gen.go 文件</h3>
<p>进入到 cmd/quickstart 目录,然后直接用 <code>wire</code> 命令重新生成 wire_gen.go 文件。</p>
<pre><code class="language-shell">// cmd/quickstart
wire
</code></pre>
<blockquote>
<p>wire 的用法可以看这篇文章:Go 依赖注入工具 wire 使用</p>
</blockquote>
<p>这篇文章已经写的有点长了,接下来的一篇文章结合 gorm 进行一些简单的增加修改列表等简单的操作。<br>
虽然 kratos 以前用的是 Ent 操作数据库,但是我感觉还是 gorm 使用的人多。</p>
<p>下一篇:Go微服务框架go-kratos学习03:使用 gorm 实现增删改查操作</p>
<hr>
<blockquote>
<p>也可以到我的公众号讨论本文 : 九卷技术录-go-kratos实战学习02</p>
</blockquote>
<h2 id="三参考">三、参考</h2>
<ul>
<li>https://go-kratos.dev/docs/getting-started/usage kratos cli 工具使用</li>
<li>https://go-kratos.dev/docs/component/api kratos api 定义</li>
<li>https://cloud.google.com/endpoints/docs/grpc/transcoding http/json 转码为 gRPC</li>
<li>https://go-kratos.dev/docs/guide/api-protobuf/ Protobuf 规范</li>
<li>https://go-kratos.dev/docs/component/config 配置</li>
<li>https://developers.google.com/protocol-buffers/docs/proto3 proto3 文档</li>
<li>https://colobu.com/2017/03/16/Protobuf3-language-guide/ Protobuf3 语法指南(中译)</li>
</ul>
</div>
<div id="MySignature" role="contentinfo">
== just do it ==<br><br>
来源:https://www.cnblogs.com/jiujuan/p/16331967.html
頁:
[1]