Use the go-kit microservice framework to implement an application that supports both http and grpc services. Take one of the most common article services as an example and start the tutorial!
project shelf
Introduction to the go-kit three-layer model
go-kit
is a set of open source golang microservice tools collection. go-kit provides a three-layer model from top to bottom, namely the Transport layer, the Endpoint layer, and the Service layer.
- Transport layer: handles protocol-related logic such as HTTP, gRPC, and Thrift, mainly decoding requests and encoding responses;
- Endpoint layer: As the middleware of the business in the upper layer of the Service, it can use the capabilities of current limiting, fusing, monitoring, etc.;
- Service layer: used to handle business logic;
Project initialization
Thanks to FengGeSe/demo project for providing a good project demo. This tutorial is based on this repository. The tutorial code is here go-kit-demo .
FengGeSe/demo
project adds Server and Router layers to the go-kit three-layer model. The former is started as a service, and the latter is used as a route forwarding.
The data model Model is used as a "neutral" data format, which is compatible with multi-protocol requests at the same time.
eg:
When an http request comes, the json data will be converted to model, and the model will be converted to json in response.
When a grpc request comes, the protobuf data will be converted to model, and when the response is received, the model will be converted to protobuf.
Project directory structure
.
├── README.md
├── cmd // 提供client和server的入口
│ ├── client
│ └── server
├── conf // 配置相关
├── endpoint // endpoint层
│ └── article
├── errors // 错误处理
├── go.mod
├── go.sum
├── params // model层(在代码中使用params表示)
│ └── article
├── pb // pb层
│ └── article
├── router // 路由层。grpc和http注册路由的地方
│ ├── grpc
│ └── http
├── server // server层,启动服务的地方
│ ├── grpc
│ └── http
├── service // service层,处理业务逻辑的地方
│ └── article
├── static // 文档,文档图片相关
│ └── img
├── transport // transport, 数据转换的地方
│ ├── grpc
│ └── http
├── util // 工具方法
└── vendor // 三方依赖
Enter the development below!
Service development
define interface
The Service layer is used to handle business logic. A Service consists of many functional methods. Take one of the most familiar article services as an example, it will provide the function of adding, deleting, modifying and checking.
Define the interface. Define the ArticleService interface and specify the methods to be provided by the Service.
// service/article.go
package service
import (
"context"
"demo/params/article_param"
"fmt"
)
type ArticleService interface {
Create (ctx context.Context, req *article_param.CreateReq) (*article_param.CreateResp, error)
Detail (ctx context.Context, req *article_param.DetailReq) (*article_param.DetailResp, error)
}
Define the data model
To implement a method, we must first think about its data model model, and clarify its input and output parameters. In order to distinguish the model layer of orm, the data model model of in the code implementation is called params , which mainly defines a request
incoming parameter and
outgoing parameter.
// params/article_param/article.go
package article_param
type CreateReq struct {
Title string `json:"title"`
Content string `json:"content"`
CateId int64 `json:"cate_id"`
}
type CreateResp struct {
Id int64 `json:"id"`
}
type DetailReq struct {
Id int64 `json:"id"`
}
type DetailResp struct {
Id int64 `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
CateId int64 `json:"cate_id"`
UserId int64 `json:"user_id"`
}
Service specific implementation
By defining an articleService structure, implement all the methods planned by the ArticleService interface. And expose the implemented Service through the NewArticleService
method.
package service
import (
"context"
"demo/params/article_param"
"fmt"
)
// ArticleService 定义文章service接口,规定本service要提供的方法
type ArticleService interface {
Create (ctx context.Context, req *article_param.CreateReq) (*article_param.CreateResp, error)
Detail (ctx context.Context, req *article_param.DetailReq) (*article_param.DetailResp, error)
}
// NewArticleService new service
func NewArticleService() ArticleService {
var svc = &articleService{}
{
// middleware
}
return svc
}
// 定义文章service结构体,并实现文章service接口的所有方法
type articleService struct {}
func (s *articleService) Create (ctx context.Context, req *article_param.CreateReq) (*article_param.CreateResp, error) {
fmt.Printf("req:%#v\n", req)
// mock:insert 根据传入参数req插库生成Id
id := 1
return &article_param.CreateResp{
Id: int64(id),
}, nil
}
func (s *articleService) Detail (ctx context.Context, req *article_param.DetailReq) (*article_param.DetailResp, error) {
……
}
Taking the method of creating an article as an example, a method gets the input parameters defined by the model layer, then executes specific logic, and finally returns the output parameters defined by the model layer.
As a web developer, it is easy to think that the parameters returned by an http response should be in json format, which is different here. here to the struct defined by the mdoel layer, which is a neutral data structure , which is only related to the development language. The json format is coupled with the http service. Not all services use json to transmit data. For example, the grpc service generally uses protobuf as the data format. Therefore, the input and output parameters of the methods implemented by the Service layer are neither json nor protobuf messages.
As for the problem of data conversion, that is the concern of the transport layer (later).
Endpoint development
The endpoint layer is called through the NewArticleService method exposed by the Service layer. Before the Service layer, the endpoint layer can be used as the middleware of the business. It also doesn't care if the request is http or grpc.
// endpoint/article/article.go
package article
import (
"context"
"demo/errors"
"demo/params/article_param"
service "demo/service"
"github.com/go-kit/kit/endpoint"
)
// make endpoint service -> endpoint
func MakeCreateEndpoint(svc service.ArticleService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req, ok := request.(*article_param.CreateReq)
if !ok {
return nil, errors.EndpointTypeError
}
resp, err := svc.Create(ctx, req)
if err != nil {
return nil, err
}
return resp, nil
}
}
// make endpoint service -> endpoint
func MakeDetailEndpoint(svc service.ArticleService) endpoint.Endpoint {
……
}
HTTP + Json development
Transport layer
The transport layer is before the endpoint layer, and its main function is to parse the requested data and encode the response data. At this layer, it is necessary to distinguish whether the request is http or grpc. After all, the data formats of the two service requests are different and need to be processed separately.
In the transport layer to implement decodeRequest, encodeResponse and http.Handler.
- decodeRequest parses the parameters from the http request and converts them to a "neutral" model structure;
- encodeResponse converts the "neutral" model structure to json, or adds operations such as response headers;
- The handler utilizes the transport/http fusion decodeRequest and encodeResponse provided by go-kit, and calls the endpoint layer.
// transport/http/article/create.go
package article
import (
"context"
endpoint "demo/endpoint/article"
"demo/params/article_param"
"demo/service"
transport "demo/transport/http"
"encoding/json"
"fmt"
httptransport "github.com/go-kit/kit/transport/http"
"net/http"
)
// Server
// 1. decode request http.request -> model.request
func decodeCreateRequest(_ context.Context, r *http.Request) (interface{}, error) {
if err := transport.FormCheckAccess(r); err != nil {
return nil, err
}
if err := r.ParseForm(); err != nil {
fmt.Println(err)
return nil, err
}
req := &article_param.CreateReq{}
err := transport.ParseForm(r.Form, req)
if err != nil {
return nil, err
}
fmt.Printf("r.Form:%#v\n", r.Form)
fmt.Printf("req:%#v\n", req)
r.Body.Close()
return req, nil
}
// 2. encode response model.response -> http.response
func encodeCreateResponse(_ context.Context, w http.ResponseWriter, resp interface{}) error {
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(resp)
}
// make handler
func MakeCreateHandler(svc service.ArticleService) http.Handler {
handler := httptransport.NewServer(
endpoint.MakeCreateEndpoint(svc),
decodeCreateRequest,
encodeCreateResponse,
transport.ErrorServerOption(), // 自定义错误处理
)
return handler
}
Router + Server
The router layer forwards to different transport layers according to the url
// router/httprouter/article.go
package httprouter
import (
svc "demo/service"
transport "demo/transport/http/article"
"net/http"
)
func RegisterRouter(mux *http.ServeMux) {
mux.Handle("/article/create", transport.MakeCreateHandler(svc.NewArticleService()))
mux.Handle("/article/detail", transport.MakeDetailHandler(svc.NewArticleService()))
}
The server layer is used to start the http service and introduce the router.
// server/http/server.go
package http
import (
"demo/router/httprouter"
"net"
"net/http"
)
var mux = http.NewServeMux()
var httpServer = http.Server{Handler: mux}
// http run
func Run(addr string, errc chan error) {
// 注册路由
httprouter.RegisterRouter(mux)
lis, err := net.Listen("tcp", addr)
if err != nil {
errc <- err
return
}
errc <- httpServer.Serve(lis)
}
Finally, call http.Run in a unified script to start the http service.
// cmd/server/sever.go
package main
import http "demo/server/http"
func main() {
errc := make(chan error)
go http.Run("0.0.0.0:8080", errc)
// 等grpc服务完成后,在这里启动 grpc
log.WithField("error", <-errc).Info("Exit")
}
Grpc + Protobuf development
write protobuf
In the grpc service, we use protobuf as the data format, so the first step is to write protobuf.
In protobuf, define service and message. Write the protobuf message based on the data model model, and write the protobuf service based on the ArticleService interface of the Service layer.
// pb/article/article.proto
syntax = "proto3";
option go_package = ".;proto";
service ArticleService {
rpc Create(CreateReq) returns (CreateResp);
rpc Detail(DetailReq) returns (DetailResp);
}
message CreateReq {
string Title = 1;
string Content = 2;
int64 CateId = 3;
}
message CreateResp {
int64 Id = 1;
}
message DetailReq {
int64 Id = 1;
}
message DetailResp {
int64 Id = 1;
string Title = 2;
string Content = 3;
int64 CateId = 4;
int64 UserId = 5;
}
A proto file is similar to a data model, but don't confuse them. model is "neutral".
What does proto file specifically mean?
service
keyword specifies a grpc service, which provides two methods Create and Detail.message
keyword specifies the message structure, which consists of type variable name = serial number, and the final transmission protobuf is in binary format, only encoding the value, not encoding the variable name, so the serial number is needed to parse the corresponding variable.
When the client calls the Create method of the ArticleService service, it needs to pass in the parameter set of the CreateReq structure, and the server will return the parameter set of the CreateResp structure.
To generate a pb file, in the pb/article/
directory, execute the following command:
protoc --proto_path=./ --go_out=plugins=grpc:./ ./article.proto
The generated pb file of the Go version is as follows.
This is the Server code in article.pb.go for server use.
// pb/article/article.pb.go
……
// ArticleServiceServer is the server API for ArticleService service.
type ArticleServiceServer interface {
Create(context.Context, *CreateReq) (*CreateResp, error)
Detail(context.Context, *DetailReq) (*DetailResp, error)
}
// UnimplementedArticleServiceServer can be embedded to have forward compatible implementations.
type UnimplementedArticleServiceServer struct {
}
func (*UnimplementedArticleServiceServer) Create(context.Context, *CreateReq) (*CreateResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method Create not implemented")
}
func (*UnimplementedArticleServiceServer) Detail(context.Context, *DetailReq) (*DetailResp, error) {
return nil, status.Errorf(codes.Unimplemented, "method Detail not implemented")
}
func RegisterArticleServiceServer(s *grpc.Server, srv ArticleServiceServer) {
s.RegisterService(&_ArticleService_serviceDesc, srv)
}
This is the Client code in article.pb.go, the pb file will be sent to the client, and the Client code will be used by the client.
// pb/article/article.pb.go
……
// ArticleServiceClient is the client API for ArticleService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
type ArticleServiceClient interface {
Create(ctx context.Context, in *CreateReq, opts ...grpc.CallOption) (*CreateResp, error)
Detail(ctx context.Context, in *DetailReq, opts ...grpc.CallOption) (*DetailResp, error)
}
type articleServiceClient struct {
cc grpc.ClientConnInterface
}
func NewArticleServiceClient(cc grpc.ClientConnInterface) ArticleServiceClient {
return &articleServiceClient{cc}
}
func (c *articleServiceClient) Create(ctx context.Context, in *CreateReq, opts ...grpc.CallOption) (*CreateResp, error) {
out := new(CreateResp)
err := c.cc.Invoke(ctx, "/ArticleService/Create", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *articleServiceClient) Detail(ctx context.Context, in *DetailReq, opts ...grpc.CallOption) (*DetailResp, error) {
out := new(DetailResp)
err := c.cc.Invoke(ctx, "/ArticleService/Detail", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
Transport layer
Next, implement the transport layer of the grpc service.
In the transport layer to implement decodeRequest, encodeResponse and grpc.Handler.
- decodeRequest parses the protobuf requested by grpc and converts it to a "neutral" model structure;
- encodeResponse converts the "neutral" model structure to protobuf;
- The handler utilizes the fusion decodeRequest and encodeResponse of transport/grpc provided by go-kit, and calls the endpoint layer.
// transport/grpc/article/create.go
package article
import (
"context"
"demo/params/article_param"
pb "demo/pb/article"
"fmt"
)
// 1. decode request pb -> model
func decodeCreateRequest(c context.Context, grpcReq interface{}) (interface{}, error) {
req, ok := grpcReq.(*pb.CreateReq)
if !ok {
fmt.Println("grpc server decode request出错!")
return nil, fmt.Errorf("grpc server decode request出错!")
}
// 过滤数据
request := &article_param.CreateReq{
Title: req.Title,
Content: req.Content,
CateId: req.CateId,
}
return request, nil
}
// 2. encode response model -> pb
func encodeCreateResponse(c context.Context, response interface{}) (interface{}, error) {
fmt.Printf("%#v\n", response)
resp, ok := response.(*article_param.CreateResp)
if !ok {
return nil, fmt.Errorf("grpc server encode response error (%T)", response)
}
r := &pb.CreateResp{
Id: resp.Id,
}
return r, nil
}
The transport layer of the grpc service is similar to the transport layer of the http service, except that a little "glue" is needed to integrate grpc and go-kit.
// transport/grpc/article/article.go
package article
import (
"context"
endpoint "demo/endpoint/article"
pb "demo/pb/article"
"demo/service"
grpctransport "github.com/go-kit/kit/transport/grpc"
)
// ArticleGrpcServer 1.实现了 pb.ArticleServiceServer 的所有方法,实现了”继承“;
// 2.提供了定义了 create 和 detail 两个 grpctransport.Handler。
type ArticleGrpcServer struct {
createHandler grpctransport.Handler
detailHandler grpctransport.Handler
}
// 通过 grpc 调用 Create 时,Create 只做数据传递, Create 内部又调用 createHandler,转交给 go-kit 处理
func (s *ArticleGrpcServer) Create (ctx context.Context, req *pb.CreateReq) (*pb.CreateResp, error) {
_, rsp, err := s.createHandler.ServeGRPC(ctx, req)
if err != nil {
return nil, err
}
return rsp.(*pb.CreateResp), err
}
func (s *ArticleGrpcServer) Detail (ctx context.Context, req *pb.DetailReq) (*pb.DetailResp, error) {
_, rsp, err := s.detailHandler.ServeGRPC(ctx, req)
if err != nil {
return nil, err
}
return rsp.(*pb.DetailResp), err
}
// NewArticleGrpcServer 返回 proto 中定义的 article grpc server
func NewArticleGrpcServer(svc service.ArticleService, opts ...grpctransport.ServerOption) pb.ArticleServiceServer {
createHandler := grpctransport.NewServer(
endpoint.MakeCreateEndpoint(svc),
decodeCreateRequest,
encodeCreateResponse,
opts...,
)
articleGrpServer := new(ArticleGrpcServer)
articleGrpServer.createHandler = createHandler
return articleGrpServer
}
The role of
1. Implemented all the methods of the pb.ArticleServiceServer interface, and realized "inheritance". It can also be said that the instance of ArticleGrpcServer is a type of pb.ArticleServiceServer.
2. Provides two grpctransport.Handlers that define create and detail. The purpose of is to interface with the go-kit model .
When the Create method is called through grpc, Create only does data transfer, and Create calls createHandler internally, so the request is forwarded to go-kit for processing .
The role of returns the article grpc server defined in proto to expose the grpc service for the outer grpc router to call, which integrates multiple handlers.
The Handler of go-kit calls endpoint, decodeRequest, and encodeResponse.
createHandler := grpctransport.NewServer(
endpoint.MakeCreateEndpoint(svc),
encodeCreateResponse,
decodeCreateRequest,
opts...,
)
Router + Server
Register the services exposed by the transport layer at the router layer.
// router/grpcrouter/article.go
package grpcrouter
import (
pb "demo/pb/article"
"demo/service"
transport "demo/transport/grpc/article"
"google.golang.org/grpc"
)
func RegisterRouter(grpcServer *grpc.Server) {
pb.RegisterArticleServiceServer(grpcServer, transport.NewArticleGrpcServer(service.NewArticleService()))
}
The server layer introduces the router layer to start the grpc service.
// server/grpc/server.go
package grpc
import (
"demo/router/grpcrouter"
"net"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"google.golang.org/grpc"
)
var opts = []grpc.ServerOption{
grpc_middleware.WithUnaryServerChain(
RecoveryInterceptor,
),
}
var grpcServer = grpc.NewServer(opts...)
func Run(addr string, errc chan error) {
// 注册grpcServer
grpcrouter.RegisterRouter(grpcServer)
lis, err := net.Listen("tcp", addr)
if err != nil {
errc <- err
return
}
errc <- grpcServer.Serve(lis)
}
Finally, call grpc.Run in a unified script to start the grpc service and the http service implemented before.
// cmd/server/sever.go
package main
import http "demo/server/http"
import grpc "demo/server/grpc"
func main() {
errc := make(chan error)
go http.Run("0.0.0.0:8080", errc)
go grpc.Run("0.0.0.0:5000", errc)
log.WithField("error", <-errc).Info("Exit")
}
run script
go run cmd/server/sever.go
Finish!
reference
https://github.com/FengGeSe/demo
https://github.com/junereycasuga/gokit-grpc-demo
https://github.com/win5do/go-microservice-demo
Personal blog synchronization article uses go-kit to implement microservices that support http and grpc
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。