Go Kit
Go Kit是个相当优秀的微服务框架,没有太多使用上的限制,提供了丰富的组件以实现各种强大的功能。
本文简单介绍框架的结构和使用,没有负载均衡,流量限制等实现。
go-kit 框架主要结构分为三层:
- transport:负责数据的传输方式定义,包括传输协议的数据和业务数据结构的转换(编码,解码)。
- endpoint:负责连接 transport 和 service 层,起到连接两层的作用。
- service:业务逻辑定义,通常和 repository 层交互,对数据进行操作。
示例
示例只有一个接口,为传入用户 ID 查询用户信息返回,项目结构如下
.
├── cmd
│ ├── grpc
│ │ └── main.go
│ └── http
│ └── main.go
├── endpoint
│ └── user.go
├── generate_pb.sh
├── go.mod
├── go.sum
├── model
│ ├── http_request.go
│ ├── http_response.go
│ ├── user.go
│ └── user_table.go
├── pb
│ ├── user.pb.go
│ └── user_grpc.pb.go
├── repositroy
│ ├── db.go
│ └── user.go
├── service
│ └── user.go
├── transport
│ ├── grpc.go
│ ├── grpc_decode.go
│ ├── grpc_encode.go
│ ├── http.go
│ ├── http_decode.go
│ └── http_encode.go
└── user.proto
请求处理过程
用户
—decode
—endpoint
—service
—...数据操作
—service 返回
—endpoint 返回
—encode
—用户
service
服务简单定义一下
package service
import (
"gorm.io/gorm"
"kit-demo/model"
)
type UserSvcInterface interface {
GetUserByID(user *model.UserTable, tx *gorm.DB) error
}
type UserSvc struct{}
func NewUserSvc() *UserSvc {
return &UserSvc{}
}
func (u UserSvc) GetUserByID(user *model.UserTable, tx *gorm.DB) error {
if user.ID == 100 {
user.Name = "admin"
}
return nil
}
endpoint
这里主要是就是转换传入参数的数据,传入调用的服务中然后返回接口执行的结果。
传入参数是在 transport 层中的编码解码部分处理,在这里只需要确保传入参数是业务逻辑需要的数据类型即可
package endpoint
import (
"context"
"errors"
"github.com/go-kit/kit/endpoint"
"kit-demo/model"
"kit-demo/service"
)
func MakeGetUserByIDEndpoint(svc service.UserSvcInterface) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
user, ok := request.(*model.UserTable)
if !ok {
return nil, errors.New("request data type error")
}
if user.ID == 0 {
return nil, errors.New("id is required")
}
err = svc.GetUserByID(user, nil)
if err != nil {
return nil, err
}
return user, nil
}
}
transport
本层分为两部分,grpc 和 http 两种,当然 go-kit 还支持其他传输方式。可以选择仅实现其中一种。
encode、decode、endpoint 执行顺序
go-kit 中的源码可以看到顺序为:decode、endpoint、encode
// go-kit http 部分源码
// s.dec 是 decode 函数
// s.enc 是 encode 函数
// s.e 是 endpoint 函数
func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if len(s.finalizer) > 0 {
iw := &interceptingWriter{w, http.StatusOK, 0}
defer func() {
ctx = context.WithValue(ctx, ContextKeyResponseHeaders, iw.Header())
ctx = context.WithValue(ctx, ContextKeyResponseSize, iw.written)
for _, f := range s.finalizer {
f(ctx, iw.code, r)
}
}()
w = iw
}
for _, f := range s.before {
ctx = f(ctx, r)
}
// 执行 decode
request, err := s.dec(ctx, r)
if err != nil {
s.errorHandler.Handle(ctx, err)
s.errorEncoder(ctx, err, w)
return
}
// 执行 endpoint
response, err := s.e(ctx, request)
if err != nil {
s.errorHandler.Handle(ctx, err)
s.errorEncoder(ctx, err, w)
return
}
for _, f := range s.after {
ctx = f(ctx, w)
}
// 执行 encode
if err := s.enc(ctx, w, response); err != nil {
s.errorHandler.Handle(ctx, err)
s.errorEncoder(ctx, err, w)
return
}
}
grpc
使用 grpc 方式,我们需要先定义 proto 协议文件,本示例协议文件内容如下
syntax = "proto3";
option go_package = "kit-demo/pb";
service UserService {
rpc GetUserByID(IDRequest) returns (UserInfo);
}
message IDRequest {
uint64 id = 1;
}
message UserInfo {
uint64 id = 1;
string account = 2;
string name = 3;
string phone = 4;
string email = 5;
int64 create_ts = 6;
}
简单描述了服务需要的参数类型和返回值类型
使用工具生成 go 的 pb 文件
#!/usr/bin/env bash
protoc --go_out=./pb --go_opt=paths=source_relative \
--go-grpc_out=./pb --go-grpc_opt=paths=source_relative \
user.proto
在 transport 层中实现 pb 文件中的接口。
这里在创建服务时需要传入 endpoint 和 decode, encode 两个编码解码方法
// transport/grpc.go
package transport
import (
"context"
transportGrpc "github.com/go-kit/kit/transport/grpc"
"kit-demo/endpoint"
"kit-demo/pb"
"kit-demo/service"
)
// UserGrpcServer 实现 pb 文件接口
type UserGrpcServer struct {
getUserByIDHandler transportGrpc.Handler
pb.UnimplementedUserServiceServer
}
func NewUserGrpcServer(svc service.UserSvcInterface, opts ...transportGrpc.ServerOption) pb.UserServiceServer {
getUserByIDHandler := transportGrpc.NewServer(
endpoint.MakeGetUserByIDEndpoint(svc),
decodeGetUserByIDRequest,
encodeGetUserByIDResponse,
opts...,
)
userGrpcServer := &UserGrpcServer{
getUserByIDHandler: getUserByIDHandler,
}
return userGrpcServer
}
func (u UserGrpcServer) GetUserByID(ctx context.Context, request *pb.IDRequest) (*pb.UserInfo, error) {
// 调用 handler 方法处理请求
_, resp, err := u.getUserByIDHandler.ServeGRPC(ctx, request)
if err != nil {
return nil, err
}
return resp.(*pb.UserInfo), nil
}
编码解码函数如下:
- 解码方法主要是转换 pb 协议文件中的数据为 业务接口需要的 数据类型。
- 编码方法主要是转换 业务数据类型 为 pb 协议文件中的数据类型。
func decodeGetUserByIDRequest(ctx context.Context, grpcRequest interface{}) (interface{}, error) {
req, ok := grpcRequest.(*pb.IDRequest)
if !ok {
return nil, errors.New("grpc server decode request error")
}
request := &model.UserTable{
ID: req.Id,
}
return request, nil
}
func encodeGetUserByIDResponse(ctx context.Context, response interface{}) (interface{}, error) {
resp, ok := response.(*model.SvcResponseUserInfo)
if !ok {
return nil, errors.New("grpc server encode response error")
}
r := &pb.UserInfo{
Id: resp.ID,
Account: resp.Account,
Name: resp.Name,
Phone: resp.Phone,
Email: resp.Email,
CreateTs: resp.CreateTs,
}
return r, nil
}
对外提供服务
package main
import (
"fmt"
"google.golang.org/grpc"
"kit-demo/pb"
"kit-demo/service"
"kit-demo/transport"
"net"
)
func main() {
var opts []grpc.ServerOption
grpcServer := grpc.NewServer(opts...)
pb.RegisterUserServiceServer(grpcServer, transport.NewUserGrpcServer(service.NewUserSvc()))
fmt.Println("grpc server start")
listener, err := net.Listen("tcp", ":8099")
if err != nil {
fmt.Println(err.Error())
return
}
err = grpcServer.Serve(listener)
if err != nil {
fmt.Println(err.Error())
return
}
}
http
http 部分本文使用 gin 框架来处理请求和路由,逻辑基本同 grpc 一致
接口部分实现
// tansport/http.go
package transport
import (
"github.com/gin-gonic/gin"
transportHTTP "github.com/go-kit/kit/transport/http"
"kit-demo/endpoint"
"kit-demo/service"
"net/http"
)
type UserHTTPServerInterface interface {
GetUserByID(ctx *gin.Context)
}
type UserHTTPServer struct {
getUserByIDHandler http.Handler
}
func NewUserHTTPServer(svc service.UserSvcInterface, opts ...transportHTTP.ServerOption) UserHTTPServerInterface {
getUserByIDHandler := transportHTTP.NewServer(
endpoint.MakeGetUserByIDEndpoint(svc),
decodeGetUserByIDHttpReq,
encodeGetUserByIDHttpResp,
opts...,
)
return &UserHTTPServer{
getUserByIDHandler: getUserByIDHandler,
}
}
// gin 接口,保存在 keys 中的数据,可以转换到 header 中向 go-kit 中传递
func (u UserHTTPServer) GetUserByID(ctx *gin.Context) {
ctx.Request.Header.Set("name", "xxxx")
u.getUserByIDHandler.ServeHTTP(ctx.Writer, ctx.Request)
}
编码解码函数如下,同 grpc 作用一致,转换数据为服务端需要的类型,然后交给 endpoint 处理
func decodeGetUserByIDHttpReq(ctx context.Context, request *http.Request) (interface{}, error) {
bodyBytes, err := ioutil.ReadAll(request.Body)
if err != nil {
return nil, err
}
req := &model.RequestGetUserByID{}
err = json.Unmarshal(bodyBytes, &req)
if err != nil {
return nil, err
}
user := &model.UserTable{ID: req.ID}
return user, nil
}
func encodeGetUserByIDHttpResp(ctx context.Context, writer http.ResponseWriter, resp interface{}) error {
writer.Header().Set("Content-Type", "application/json")
return json.NewEncoder(writer).Encode(resp)
}
对外提供服务
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"kit-demo/service"
"kit-demo/transport"
)
var (
engine *gin.Engine
)
func init() {
gin.SetMode(gin.ReleaseMode)
engine = gin.Default()
api := transport.NewUserHTTPServer(service.NewUserSvc())
engine.POST("/user/get", api.GetUserByID)
}
func main() {
err := engine.Run(":8100")
if err != nil {
fmt.Println(err)
return
}
}