基本思想解析
1、核心的分层概念
我们使用这个框架,首要的就是要了解service、endpoint、transport这三个概念并编写对应代码,然后可以拼成一个服务供访问。
2、我们先来聊聊transport层怎么来的?
我们要搭建的微服务体系,不确定用户会用使用什么协议,比如http、grpc、thrift等等,这些得交给一个层去实现,这个就是transport层。
它可以对多种协议进行处理的实现,但是他与endpoint层之间就用规范的接口标准去定义,这样层与层之间解耦合并有清晰的接口关系。
transport底层负责把协议中数据提取出来,交给endpoint层处理,然后拿到endpoint层返回的数据,再通过协议封装返回。 总结下来,transport层就是要确认用什么协议,然后对每个路由应用一个endpoint层的接口实现。
不同协议返回的数据不一样,http的和rpc的数据有自己的格式,不过给到endpoint和从endpoint拿的数据都是一样的, 这样transport层还得实现对每个路由有关的一对DecodeRequest和EncodeResponse函数。
总结下来transport层就是两件事,一是对接具体协议,二是编解码数据对接enpoint层。
3、再来聊聊endpoint层怎么来的?
上面说到接口,这个接口如果是我们怎么设计呢,我们需要处理过来的路由,一个路由一个处理器。
处理器就是一个函数,参数是从协议里解析的,不确定格式我们就用万能的interface{},返回值也不好说,那就也用万能的interface{},这样一来, 我们可以定义出这样一个接口就叫Point吧,Point接口就一个方法如上所说的,就是func(context.Context, interface{}) (interface{}, error)。 transport层处理完协议把不同路由的请求就调用不同的Point接口实现即可。
考虑去实现Point接口,对一个路由我们创立一个对应结构体实现Point接口,里面的方法就是具体业务实现。这样代码写的很繁琐,既然接口就一个方法, 那不如直接定义一个函数类型得了,这样就出来了EndPoint函数类型,对一个路由我们实现一个函数类型对象即可。如下就是Endpoint函数类型:
type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)
4、再来聊聊service层怎么来的?
其实上面说到的两层即endpoint和transport就已经可以把代码规划层次然后各司其职了, 把所有业务逻辑都写到一个个enpoint这个函数类型的实现中。
但在实际开发中,我们得用很多中间件,得用redis、用mysql、用其他依赖服务等等,对应的项目中就是一个个struct并构建之间复杂的关系。
类似的我们可能会构建出一套MVC分层的代码来实现业务。对一个具体的请求,我们使用Controller层与之对接。基于这个背景,go-kit让我们实现一个业务层, 这里就叫service层。在service层实现业务处理的接口,endpoint也不关心你的具体实现,就是定义好接口方法,它处理的时候按照标准和service层交互就好了。
5、最后看看怎么把这三层整合起来呢?
首先endpoint层依赖service层,我们定义一个接口比如就叫IService,我们实例化endpoint的时候就提供一个具体的IService接口实现。
上面我们说到Endpoint实现不是一个标准struct,如果是我们可以给他加上IService依赖,实例化的时候提供实现即可。现在我们的每个EndPoint已经 是一个函数类型实现,想要往里填入对IService接口的依赖,可以考虑使用闭包。闭包可以让函数类型的实现依赖外部的变量,而且在使用时在加载。 因此,我们本来要实现一个个Endpoint函数类型的实现,现在要改为实现一个个闭包,它让一个函数类型对象和外部变量绑定了。
func MakeEndPointArticleAdd(svc my_service.IService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
req := request.(my_service.ArticleAddRequest)
res := svc.ArticleAdd(ctx, req)
return res, nil
}
}
在说下transport层和endpoint层,endpoint本身就是一种函数类型了,我们实现一个transport层的处理器时,就可以提供一个Endpoint函数类型实现。 比如对于http协议的处理,transport要提供一个Server结构体,它包含了Endpoint、DecodeRequest、EncodeResponse, 我们实例化它时提供好我们的endpoint层的具体实现接口。
handler := httpTransport.NewServer(
endpointArticleAdd,
my_tansport.DecodeArticleAddRequest,
my_tansport.EncodeArticleAddResponse,
)
一个demo来说明
在项目use_gokit/v1/app中,展示了如何基于go-kit架子搭建出一个http服务。
1. 总体设计
在下面的main.go中就能看到整体的结构。
- 先创建定义的业务处理接口实现,service
- 在创建业务服务的函数签名,endpoint
- 使用kit创建handler
- http路由和服务
package main
import (
"fmt"
httpTransport "github.com/go-kit/kit/transport/http"
"net/http"
"swwgo/use_gokit/v1/app/my_endpoint"
"swwgo/use_gokit/v1/app/my_service"
"swwgo/use_gokit/v1/app/my_tansport"
)
func main() {
// 1.先创建定义的业务处理接口实现, service
svc := my_service.NewService()
// 2.再创建业务服务的函数签名,endpoint
endpointArticleAdd := my_endpoint.MakeEndPointArticleAdd(svc)
// 3.使用kit创建handler
handler := httpTransport.NewServer(
endpointArticleAdd,
my_tansport.DecodeArticleAddRequest,
my_tansport.EncodeArticleAddResponse,
)
// 4. http路由和服务
m := http.NewServeMux()
m.Handle("/article", handler)
fmt.Println("server run 0.0.0.0:8888")
err := http.ListenAndServe("0.0.0.0:8888", m)
if err != nil {
println(err.Error())
}
select {
}
}
2. service
各层之间要用接口定义进行组合,所以这里先定义出一个service层的接口,如下面的“IService”
我们定义出一个业务处理,如下面的ArticleAdd,它的入参ArticleAddRequest,出参ArticleAddResponse。
然后实现这个接口的结构体,如下面的baseService。
my_service/service.go
package my_service
import (
"context"
)
// IService 用于定义业务方法的接口
type IService interface {
ArticleAdd(ctx context.Context, param ArticleAddRequest) ArticleAddResponse
}
// baseService 用于实现上面定义的接口
type baseService struct {
// 根据业务需求填充结构体...
}
func (b baseService) ArticleAdd(ctx context.Context, param ArticleAddRequest) ArticleAddResponse {
return ArticleAddResponse{
Data: param.Name + param.Content,
}
}
func NewService() IService {
return &baseService{}
}
my_service/request.go
package my_service
type ArticleAddRequest struct {
Name string `json:"name"`
Content string `json:"content"`
}
my_service/response.go
package my_service
type ArticleAddResponse struct {
Data string `json:"data"`
}
3. endpoint
为了对接一个接口服务,构建一个endpoint,它依赖于service接口。
my_endpoint/endpoint.go
package my_endpoint
import (
"context"
"github.com/go-kit/kit/endpoint"
"swwgo/use_gokit/v1/app/my_service"
)
// MakeEndPointArticleAdd 创建关于业务的构造函数
// 传入service层定义的相关业务接口
// 返回 endpoint.Endpoint, 实际就是一个函数签名
func MakeEndPointArticleAdd(svc my_service.IService) endpoint.Endpoint {
// 这里使用闭包,可以在这里做一些业务的处理
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
// request是对应请求过来时传入的参数,实际上是Transport中一个decode函数处理得到的
// 需要进行下断言
req := request.(my_service.ArticleAddRequest)
// 这里就是调用service层定义的业务逻辑
// 把拿到的数据作为参数
res := svc.ArticleAdd(ctx, req)
// 返回值可以是任意的,不过根据规范要返回我们刚才定义好的返回对象
return res, nil
}
}
4. transport
Transport 负责HTTP、gRPC、thrift等相关协议的请求逻辑。
对每一个请求都要实现一对参数解码和返回值编码的函数签名。
my_transport/transport.go
package my_tansport
import (
"context"
"encoding/json"
"errors"
"net/http"
"swwgo/use_gokit/v1/app/my_service"
)
// Transport 负责HTTP、gRPC、thrift等相关协议的请求逻辑
// 对每一个请求都要实现一对参数解码和返回值编码的函数签名。
// DecodeRequest & EncodeResponse 函数签名是固定的。
// func DecodeRequest(c context.Context, request *http.Request) (interface{}, error)
// func EncodeResponse(c context.Context, w http.ResponseWriter, response interface{}) error
// DecodeRequest解码,请求参数封装为Endpoint中定义的Request格式
func DecodeArticleAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
var param my_service.ArticleAddRequest
param.Name = r.FormValue("name")
param.Content = r.FormValue("content")
if param.Name == "" {
return nil, errors.New("name不能为空")
}
return param, nil
}
// EncodeResponse编码,把业务的响应封装成想要的结构
func EncodeArticleAddResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
// 这里将Response返回成有效的json格式给http
// 设置请求头
w.Header().Set("Content-Type", "application/json; charset=utf-8")
// 使用内置json包转换
return json.NewEncoder(w).Encode(response)
}