GoKit

GoKit

————

————

Go kit (http://gokit.io)是一个Go软件包的集合,可以帮助您构建健壮、可靠、可维护的微服务。Go kit提供了用于实现系统可观察性和弹性模式的组件的库,如日志、度量、跟踪、速率限制和断路,这些都是在生产环境中运行微服务的基本要求.

除了构建微服务的工具包外,它还鼓励在服务中应用程序架构的良好设计原则。Go工具包可以帮助你拥抱扎实的设计原则、领域驱动设计(DDD)和Alistair Cockburn提出的“六边形架构”,或者类似的架构方法之一,即Jeffrey Palermo提出的“洋葱架构”和Robert C. Martin提出的“清洁架构”。虽然Go kit被设计成一个微服务工具包,但它也非常适合构建优雅的整体。

Go Kit 遵循简单的规则,例如:

没有全局状态

声明式组合
显式依赖关系

接口即约定
领域驱动设计

GoKit架构

在基于Gokit 的应用架构中有三个主要组件:

传输层

端点层

服务层

传输层transport

当您构建基于分布式系统的微服务时,服务通常使用具体的传输方式(如HTTP或gRPC)或使用pub/sub系统(如NATS)进行通信。Go套件中的传输层绑定到具体运输。Go kit支持各种传输服务使用HTTP, gRPC, nat, AMQP和Thrift。因为Go kit服务只专注于实现业务逻辑,而不了解具体的传输,所以您可以为同一个服务提供多个传输。例如,一个单独的Go kit服务可以同时使用HTTP和gRPC来公开。

终端Endpoints

端点是服务器和客户机的基本构建块。在Go kit中,主要的消息传递模式是RPC。端点表示单个RPC方法。Go kit服务中的每个服务方法都转换为一个端点,以便在服务器和客户机之间进行RPC风格的通信。每个端点通过使用具体的传输(如HTTP或gRPC),使用传输层向外部世界公开服务方法。单个端点可以通过使用多个传输来公开。

是gokit最重要的一个层,是一个抽象的接收请求返回响应的函数类型。在这个定义的类型里面会去调用service层的方法,组装成response返回。而gokit中的所有中间件组件都是通过装饰者设计模式注入的

以下就是Endpoint的实现, 其他go-kit组件全部通过装饰者模式注入

type Endpoint func(ctx context.Context, request interface{
    
    }) (response interface{
    
    }, err error)
 
type Middleware func(Endpoint) Endpoint

如日志中间件, 仅需实现gokit的Logger interface即可

type Endpoint func(ctx context.Context, request interface{
    
    }) (response interface{
    
    }, err error)
 
func(log Logger, in endpoint.Endpoint) endpoint.Endpoint {
    
    
    return func(ctx context.Context, req interface{
    
    }) (interface{
    
    }, error) {
    
    
            logger.Log("input", toJSON(req))
            resp, err := in(ctx, req)
            logger.Log("output", toJSON(resp), "err", err)
            return resp, err
    }
}

@Repository:标注数据访问层,可以告诉SpringMVC这是一个数据访问层,并将其申明为一个bean,例如UserDao接口的实现类UserDaoImpl,在类上加注解@Repository(“userDao”),bean的名称为userDao
@Service:标注业务逻辑层,例如UserService接口的实现类,在类上加@Service(“userService”),bean的名称为userService
@Controller:控制层,在控制层类上加@Controller即可,确认其是一个控制层类
@Entity:标准实体类,例如实体类User

服务Services

业务逻辑在服务中实现。Go套件服务被建模为接口。服务中的业务逻辑包含核心业务逻辑,这些核心业务逻辑不应该包含端点或具体传输(如HTTP或gRPC),也不应该包含对请求和响应消息类型的编码和解码。这将鼓励您为基于Go套件的服务遵循一个干净的架构。每个服务方法通过使用适配器作为端点进行转换,并通过使用具体的传输公开。由于清晰的体系结构,单个Go kit服务可以通过使用多个传输来公开。

go-kit 使用一个抽象Endpoint 来表示每一个服务提供的方法。Endpoint通过一个service实现具体功能。

go-kit 组件围绕Endpoint来构建, 包括断路器, 限流器,日志, Metrics, 请求追踪, 服务发现和负载均衡。比如你可以使用etcd, consul, zookeeper实现你的服务注册

image-20211117105310166 image-20211119095208960

Transport就相当于Controller控制层;Endpoint就相当于Service业务逻辑层,而gokit的Service层则相当于业务实现层,

Go套件中的中间件

Go kit通过强制分离关注点来鼓励良好的设计原则。使用中间件实现服务和端点的交叉组件。Go kit中的中间件是一种强大的机制,可以包装服务和端点以添加功能(交叉组件),例如日志记录,断路器,速率限制,负载平衡或分布式

1、Circuit breaker(熔断器)

2、Rate limiter(限流器)

3、Logging(日志)

4、Metrics(Prometheus统计)

5、Request tracing(请求跟踪)

6、Service discovery and load balancing(服务发现和负载均衡)

img

组件包

img image-20211123093047032

Gokit 使用http

Step-1:创建Service

请求流程:
请求->decodeRequestFunc -> endPoint -> encodeResponseFunc -> 返回输出

按照gokit的设计理念,Service将作为核心业务逻辑实现部分。所以,我们写一个Service用于实现两个整数之间的加减乘除运算。

在GOPATH下新建一个项目,然后新建go文件service.go,定义接口Service,代码如下所示:

// Service Define a service interface
type Service interface {
    
    
 
	// Add calculate a+b
	Add(a, b int) int
 
	// Subtract calculate a-b
	Subtract(a, b int) int
 
	// Multiply calculate a*b
	Multiply(a, b int) int
 
	// Divide calculate a/b
	Divide(a, b int) (int, error)
}

接下来创建结构ArithmeticService实现Service接口。加减乘除的实现非常简单,只有除法运算需要做下异常判断

//ArithmeticService implement Service interface
type ArithmeticService struct {
    
    
}
 
// Add implement Add method
func (s ArithmeticService) Add(a, b int) int {
    
    
	return a + b
}
 
// Subtract implement Subtract method
func (s ArithmeticService) Subtract(a, b int) int {
    
    
	return a - b
}
 
// Multiply implement Multiply method
func (s ArithmeticService) Multiply(a, b int) int {
    
    
	return a * b
}
 
// Divide implement Divide method
func (s ArithmeticService) Divide(a, b int) (int, error) {
    
    
	if b == 0 {
    
    
		return 0, errors.New("the dividend can not be zero!")
	}
	return a / b, nil
}

Step-2: 创建请求-响应模型

请求模型:接收http客户端的请求后,把请求参数转为请求模型对象,用于后续业务逻辑处理。观察Service接口可以发现四个接口方法的输入参数均为两个整数,区别在于运算类型不同,所以请求模型只需包含三个字段,即:请求类型、第一个整数、第二个整数。

响应模型:用于向客户端响应结果。对于响应模型可以设置两个字段:一是结果,用于表示正常情况下的运算结果;二是错误描述,用于表示异常时的错误描述。

创建go文件endpoints.go,编写如下代码:

// ArithmeticRequest define request struct
type ArithmeticRequest struct {
    
    
	RequestType string `json:"request_type"`
	A           int    `json:"a"`
	B           int    `json:"b"`
}
 
// ArithmeticResponse define response struct
type ArithmeticResponse struct {
    
    
	Result int   `json:"result"`
	Error  error `json:"error"`
}

Step-3: 创建Endpoint

在gokit中Endpoint是可以包装到http.Handler中的特殊方法,gokit采用装饰着模式,把Service应该执行的逻辑封装到Endpoint方法中执行。Endpoint的作用是:调用Service中相应的方法处理请求对象(ArithmeticRequest),返回响应对象(ArithmeticResponse)。接下来在endpoints.go文件中增加以下代码:

// MakeArithmeticEndpoint make endpoint
func MakeArithmeticEndpoint(svc Service) endpoint.Endpoint {
    
    
	return func(ctx context.Context, request interface{
    
    }) (response interface{
    
    }, err error) {
    
    
		req := request.(ArithmeticRequest)
 
		var (
			res, a, b int
			calError  error
		)
 
		a = req.A
		b = req.B
 
		if strings.EqualFold(req.RequestType, "Add") {
    
    
			res = svc.Add(a, b)
		} else if strings.EqualFold(req.RequestType, "Substract") {
    
    
			res = svc.Subtract(a, b)
		} else if strings.EqualFold(req.RequestType, "Multiply") {
    
    
			res = svc.Multiply(a, b)
		} else if strings.EqualFold(req.RequestType, "Divide") {
    
    
			res, calError = svc.Divide(a, b)
		} else {
    
    
			return nil, ErrInvalidRequestType
		}
 
		return ArithmeticResponse{
    
    Result: res, Error: calError}, nil
	}
}

Step-4: 创建Transport

Transport层用于接收用户网络请求并将其转为Endpoint可以处理的对象,然后交由Endpoint执行,最后将处理结果转为响应对象向用户响应。为了完成这项工作,Transport需要具备两个工具方法:

解码器:把用户的请求内容转换为请求对象(ArithmeticRequest);
编码器:把处理结果转换为响应对象(ArithmeticResponse);
下面创建新的go文件,命名为transports.go

// decodeArithmeticRequest decode request params to struct
func decodeArithmeticRequest(_ context.Context, r *http.Request) (interface{
    
    }, error) {
    
    
	vars := mux.Vars(r)
	requestType, ok := vars["type"]
	if !ok {
    
    
		return nil, ErrorBadRequest
	}
 
	pa, ok := vars["a"]
	if !ok {
    
    
		return nil, ErrorBadRequest
	}
 
	pb, ok := vars["b"]
	if !ok {
    
    
		return nil, ErrorBadRequest
	}
 
	a, _ := strconv.Atoi(pa)
	b, _ := strconv.Atoi(pb)
 
	return ArithmeticRequest{
    
    
		RequestType: requestType,
		A:           a,
		B:           b,
	}, nil
}
 
// encodeArithmeticResponse encode response to return
func encodeArithmeticResponse(ctx context.Context, w http.ResponseWriter, response interface{
    
    }) error {
    
    
	w.Header().Set("Content-Type", "application/json;charset=utf-8")
	return json.NewEncoder(w).Encode(response)
}

decodeArithmeticRequest从用户请求中解析请求参数type、a、b,并将三个参数转换为请求对象ArithmeticRequest;encodeArithmeticResponse把响应内容转为json结构,向用户回写响应内容。完成以上工作后,就可以使用解码器、编码器创建HTTP处理方法了,代码如下所示:

var (
	ErrorBadRequest = errors.New("invalid request parameter")
)
 
// MakeHttpHandler make http handler use mux
func MakeHttpHandler(ctx context.Context, endpoint endpoint.Endpoint, logger log.Logger) http.Handler {
    
    
	r := mux.NewRouter()
 
	options := []kithttp.ServerOption{
    
    
		kithttp.ServerErrorLogger(logger),
		kithttp.ServerErrorEncoder(kithttp.DefaultErrorEncoder),
	}
 
	r.Methods("POST").Path("/calculate/{type}/{a}/{b}").Handler(kithttp.NewServer(
		endpoint,
		decodeArithmeticRequest,
		encodeArithmeticResponse,
		options...,
	))
	return r
}

Step-5: 编写main方法

到目前为止,我们已经为该服务完成了Service、Endpoint、Transport三个层次的构建工作,只需要通过main方法将它们按照gokit的要求组织起来,然后使用http库将服务发布即可。组织步骤如下:

创建Service对象:声明Service接口,实例化为ArithmeticService;
创建Endpoint对象;
创建http处理对象handler;
启动http服务;
创建go文件main.go添加以下代码:

func main() {
    
    
 
	ctx := context.Background()
	errChan := make(chan error)
 
	var svc Service
	svc = ArithmeticService{
    
    }
	endpoint := MakeArithmeticEndpoint(svc)
 
	var logger log.Logger
	{
    
    
		logger = log.NewLogfmtLogger(os.Stderr)
		logger = log.With(logger, "ts", log.DefaultTimestampUTC)
		logger = log.With(logger, "caller", log.DefaultCaller)
	}
 
	r := MakeHttpHandler(ctx, endpoint, logger)
 
	go func() {
    
    
		fmt.Println("Http Server start at port:9000")
		handler := r
		errChan <- http.ListenAndServe(":9000", handler)
	}()
 
	go func() {
    
    
		c := make(chan os.Signal, 1)
		signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
		errChan <- fmt.Errorf("%s", <-c)
	}()
 
	fmt.Println(<-errChan)
}

Gokit 使用gRPC

gRPC的Transport会比http稍微麻烦一些,主要是gRPC还需要实现一个Handler接口,除此之外与http的实现几乎差不多。

gRPC集成到go-kit的transport层,transport层用于接收用户网络请求并将其转为endpoint可以处理的
对象,然后交给endpoint层执行,最后将处理结果转为响应对象返回给客户端

image-20211123155007326

Step-1: 请求-响应模型

创建请求、响应模型的方式和使用http有一些不同,我们首先定义proto文件及生成pb文件

在实现grpcServer之前先得定义接口:

syntax = "proto3";
 
message GrpcResponse {
    
    
    int32 Result = 1;
    string Error = 2;
}
 
message GrpcRequest {
    
    
    string RequestType=1;
    int32 A = 2;
    int32 B = 3;
}
 
service GrpcService {
    
    
    rpc Add(GrpcRequest) returns (GrpcResponse) {
    
    }
 
    rpc    Subtract(GrpcRequest) returns (GrpcResponse) {
    
    }
 
    rpc    Multiply(GrpcRequest) returns (GrpcResponse) {
    
    }
 
    rpc    Divide(GrpcRequest) returns (GrpcResponse) {
    
    }
}

进入proto文件目录下执行

protoc service.proto --go_out==plugins=grpc:. *.proto

生成的文件如下

// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
//     protoc-gen-go v1.25.0-devel
//     protoc        v3.14.0
// source: arithmetic.proto
 
package mygrpc
 
import (
   context "context"
   grpc "google.golang.org/grpc"
   codes "google.golang.org/grpc/codes"
   status "google.golang.org/grpc/status"
   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 GrpcResponse struct {
    
    
   state         protoimpl.MessageState
   sizeCache     protoimpl.SizeCache
   unknownFields protoimpl.UnknownFields
 
   Result int32  `protobuf:"varint,1,opt,name=Result,proto3" json:"Result,omitempty"`
   Error  string `protobuf:"bytes,2,opt,name=Error,proto3" json:"Error,omitempty"`
}
......
type GrpcRequest struct {
    
    
   state         protoimpl.MessageState
   sizeCache     protoimpl.SizeCache
   unknownFields protoimpl.UnknownFields
 
   RequestType string `protobuf:"bytes,1,opt,name=RequestType,proto3" json:"RequestType,omitempty"`
   A           int32  `protobuf:"varint,2,opt,name=A,proto3" json:"A,omitempty"`
   B           int32  `protobuf:"varint,3,opt,name=B,proto3" json:"B,omitempty"`
}
 
 
func (c *grpcServiceClient) Add(ctx context.Context, in *GrpcRequest, opts ...grpc.CallOption) (*GrpcResponse, error) {
    
    
   out := new(GrpcResponse)
   err := c.cc.Invoke(ctx, "/GrpcService/Add", in, out, opts...)
   if err != nil {
    
    
      return nil, err
   }
   return out, nil
}
 
func (c *grpcServiceClient) Subtract(ctx context.Context, in *GrpcRequest, opts ...grpc.CallOption) (*GrpcResponse, error) {
    
    
   out := new(GrpcResponse)
   err := c.cc.Invoke(ctx, "/GrpcService/Subtract", in, out, opts...)
   if err != nil {
    
    
      return nil, err
   }
   return out, nil
}
 
func (c *grpcServiceClient) Multiply(ctx context.Context, in *GrpcRequest, opts ...grpc.CallOption) (*GrpcResponse, error) {
    
    
   out := new(GrpcResponse)
   err := c.cc.Invoke(ctx, "/GrpcService/Multiply", in, out, opts...)
   if err != nil {
    
    
      return nil, err
   }
   return out, nil
}
 
func (c *grpcServiceClient) Divide(ctx context.Context, in *GrpcRequest, opts ...grpc.CallOption) (*GrpcResponse, error) {
    
    
   out := new(GrpcResponse)
   err := c.cc.Invoke(ctx, "/GrpcService/Divide", in, out, opts...)
   if err != nil {
    
    
      return nil, err
   }
   return out, nil
}
 
// GrpcServiceServer is the server API for GrpcService service.
type GrpcServiceServer interface {
    
    
   Add(context.Context, *GrpcRequest) (*GrpcResponse, error)
   Subtract(context.Context, *GrpcRequest) (*GrpcResponse, error)
   Multiply(context.Context, *GrpcRequest) (*GrpcResponse, error)
   Divide(context.Context, *GrpcRequest) (*GrpcResponse, error)
}
......

Step-2:创建Endpoint

在gokit中Endpoint是可以包装到grpc.Handler中的特殊方法,gokit采用装饰着模式,把Service应该执行的逻辑封装到Endpoint方法中执行。Endpoint的作用是:调用Service中相应的方法处理请求对象(GrpcRequest ),返回响应对象(GrpcResponse )。接下来在myendpoint.go文件中增加以下代码:

func MakeArithmeticEndpoint(svc myhttp.Service) endpoint.Endpoint {
    
    
   return func(ctx context.Context, request interface{
    
    }) (response interface{
    
    }, err error) {
    
    
      req := request.(*GrpcRequest)
 
      var (
         a, b, res int
         calError  error
      )
 
      a = int(req.A)
      b = int(req.B)
 
      if strings.EqualFold(req.RequestType, "Add") {
    
    
         res = svc.Add(a, b)
      } else if strings.EqualFold(req.RequestType, "Substract") {
    
    
         res = svc.Subtract(a, b)
      } else if strings.EqualFold(req.RequestType, "Multiply") {
    
    
         res = svc.Multiply(a, b)
      } else if strings.EqualFold(req.RequestType, "Divide") {
    
    
         res, calError = svc.Divide(a, b)
      } else {
    
    
         return nil, errors.New("the dividend can not be zero!")
      }
      if calError != nil {
    
    
         return &GrpcResponse{
    
    Result: int32(res), Error: calError.Error()}, nil
      }
      return &GrpcResponse{
    
    Result: int32(res), Error: ""}, nil
   }
}

Step-3:创建Transport

Transport层用于接收用户网络请求并将其转为Endpoint可以处理的对象,然后交由Endpoint执行,最后将处理结果转为响应对象向用户响应。

package mygrpc
 
import (
   "context"
   "github.com/go-kit/kit/endpoint"
   "github.com/go-kit/kit/transport/grpc"
)
 
func decodeRequest(_ context.Context, req interface{
    
    }) (interface{
    
    }, error) {
    
    
   return req, nil
}
 
func encodeResponse(_ context.Context, req interface{
    
    }) (interface{
    
    }, error) {
    
    
   return req, nil
}
 
type GrpcServer struct {
    
    
   grpcHandler grpc.Handler
}
 
func (g *GrpcServer) Add(ctx context.Context, in *GrpcRequest) (*GrpcResponse, error) {
    
    
   _, rsp, err := g.grpcHandler.ServeGRPC(ctx, in)
   if err != nil {
    
    
      return nil, err
   }
   return rsp.(*GrpcResponse), err
 
}
func (g *GrpcServer) Subtract(ctx context.Context, in *GrpcRequest) (*GrpcResponse, error) {
    
    
   _, rsp, err := g.grpcHandler.ServeGRPC(ctx, in)
   if err != nil {
    
    
      return nil, err
   }
   return rsp.(*GrpcResponse), err
 
}
func (g *GrpcServer) Multiply(ctx context.Context, in *GrpcRequest) (*GrpcResponse, error) {
    
    
   _, rsp, err := g.grpcHandler.ServeGRPC(ctx, in)
   if err != nil {
    
    
      return nil, err
   }
   return rsp.(*GrpcResponse), err
 
}
func (g *GrpcServer) Divide(ctx context.Context, in *GrpcRequest) (*GrpcResponse, error) {
    
    
   _, rsp, err := g.grpcHandler.ServeGRPC(ctx, in)
   if err != nil {
    
    
      return nil, err
   }
   return rsp.(*GrpcResponse), err
 
}
 
func MakeGRPCHandler(ctx context.Context, endpoint endpoint.Endpoint) *GrpcServer {
    
    
   return &GrpcServer{
    
    
      grpcHandler: grpc.NewServer(
         endpoint,
         decodeRequest,
         encodeResponse,
      ),
   }
}

Step-4:编写Main方法

main方法也要做一些修改,代码如下:

var svc myhttp.Service
svc = myhttp.ArithmeticService{
    
    }
endpoint = mygrpc.MakeArithmeticEndpoint(svc)
grpcServer := mygrpc.MakeGRPCHandler(ctx, endpoint)
 
go func() {
    
    
   //启动grpc服务
   ls, _ := net.Listen("tcp", ":9002")
   gs := grpc.NewServer()
   mygrpc.RegisterGrpcServiceServer(gs, grpcServer)
   gs.Serve(ls)
 
}()

GRPC Client

写一个client来测试一下我们的代码

package main
 
import (
   "context"
   "fmt"
   "gokitdemo/pkg/mygrpc"
   "google.golang.org/grpc"
)
 
func main() {
    
    
   serviceAddress := "127.0.0.1:9002"
   conn, err := grpc.Dial(serviceAddress, grpc.WithInsecure())
   if err != nil {
    
    
      panic("connect error")
   }
   defer conn.Close()
 
   grpcClient:=mygrpc.NewGrpcServiceClient(conn)
   response,err:=grpcClient.Add(context.Background(),&mygrpc.GrpcRequest{
    
    A:5,B:4,RequestType:"Add"})
   fmt.Println(response.Result)
}

问题

endpoint 返回值必须为resp ,nil,才能将resp传递到transport的encode

image-20220111154017901

猜你喜欢

转载自blog.csdn.net/flowerqt/article/details/128953848
OIT