1、架构
transport(传输层):主要负责与HTTP、gRPC、thrift等相关逻辑,或使用NATS等发布订阅系统相
互通信,除此之外,还支持AMQP和thrift等多种网络通信模式;
endpoint(接口层):是服务器和客户端的基本构建块。在go-kit中服务中的每个对外提供的接口方
法都会定一个端点(endpoint),以便服务器和客户端之间进行网络通信。每个断电使用传输层通过
http或者grpc等通信模式对外提供服务。使用时定义request与response格式,并可以使用装饰器
包装函数,从而来实现各种中间件嵌套;
service(业务层):实现具体的业务逻辑,不会包含具体的网络传输或者消息类型的编解码,只存放
业务类接口
context
上下文 context.Context Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。上下文与 Goroutine 有比较密切的关系,是 Go 语言中独特的设计,在其他编程语言中我们很少见到类似的概念。
context.Context 是 Go 语言在 1.7 版本中引入标准库的接口1,该接口定义了四个需要实现的方法,其中包括:
- Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期;
- Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel;
- Err — 返回 context.Context 结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空的值;如果 context.Context 被取消,会返回 Canceled 错误;如果 context.Context 超时,会返回 DeadlineExceeded 错误;
- Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
context 包中提供的 context.Background 、 context.TODO 、 context.WithDeadline 和context.WithValue 函数会返回实现该接口的私有结构体。
默认上下文
context 包中最常用的方法还是 context.Background 、 context.TODO ,这两个方法都会返回预先初始化好的私有变量 background 和 todo,它们会在同一个 Go 程序中被复用:
func Background() Context {
return background
}
func TODO() Context {
return todo
}
从源代码来看, context.Background 和 context.TODO 也只是互为别名,没有太大的差别,只是在使用和语义上稍有不同:
- context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生出来;
- context.TODO 应该仅在不确定应该使用哪种上下文时使用;
在多数情况下,如果当前函数没有上下文作为入参,我们都会使用 context.Background 作为起始的上下文向下传递。
接口
隐式接口
很多面向对象语言都有接口这一概念,例如 Java 和 C#。Java 的接口不仅可以定义方法签名,还可以定义变量,这些定义的变量可以直接在实现接口的类中使用,这里简单介绍一下 Java 中的接口:
public interface MyInterface {
public String hello = "Hello";
public void sayHello();
}
上述代码定义了一个必须实现的方法 sayHello 和一个会注入到实现类的变量 hello。在下面的代码中,MyInterfaceImpl 实现了 MyInterface 接口:
public class MyInterfaceImpl implements MyInterface {
public void sayHello() {
System.out.println(MyInterface.hello);
}
}
Java 中的类必须通过上述方式显式地声明实现的接口,但是在 Go 语言中实现接口就不需要使用类似的方式。首先,我们简单了解一下在 Go 语言中如何定义接口。定义接口需要使用 interface 关键字,在接口中我们只能定义方法签名,不能包含成员变量,一个常见的 Go 语言接口是这样的:
type error interface {
Error() string
}
如果一个类型需要实现 error 接口,那么它只需要实现 Error() string 方法,下面的 RPCError 结构体就是error 接口的一个实现:
type RPCError struct {
Code int64
Message string
}
func (e *RPCError) Error() string {
return fmt.Sprintf("%s, code=%d", e.Message, e.Code)
}
上述代码根本就没有 error 接口的影子,这是为什么呢?Go 语言中接口的实现都是隐式的,我们只需要实现 Error() string 方法就实现了 error 接口。Go 语言实现接口的方式与 Java 完全不同:在 Java 中:实现接口需要显式地声明接口并实现所有方法;在 Go 中:实现接口的所有方法就隐式地实现了接口;我们使用上述 RPCError 结构体时并不关心它实现了哪些接口,Go 语言只会在传递参数、返回参数以及变量赋值时才会对某个类型是否实现接口进行检查,这里举几个例子来演示发生接口类型检查的时机:
func main() {
var rpcErr error = NewRPCError(400, "unknown err") // typecheck1
err := AsErr(rpcErr) // typecheck2
println(err)
}
func NewRPCError(code int64, msg string) error {
return &RPCError{ // typecheck3
Code: code,
Message: msg,
}
}
func AsErr(err error) error {
return err
}
将 *RPCError 类型的变量赋值给 error 类型的变量 rpcErr;将 *RPCError 类型的变量 rpcErr 传递给签名中参数类型为 error 的 AsErr 函数;将 *RPCError 类型的变量从函数签名的返回值类型为 error 的 NewRPCError 函数中返回;
路由组件mux
mux代表“ HTTP请求多路复用器”。像standard一样 http.ServeMux , mux.Router 将传入的请求与已注册的路由列表进行匹配,并为与URL或其他条件匹配的路由调用处理程序。主要特点是:
- 它实现了 http.Handler 接口,因此与标准兼容 http.ServeMux 。
- 可以基于URL主机,路径,路径前缀,方案,标头和查询值,HTTP方法或使用自定义匹配器来匹配请求。
- URL主机,路径和查询值可以具有带有可选正则表达式的变量。
- 可以构建或“反转”已注册的URL,这有助于维护对资源的引用。
- 路由可用作子路由:仅在父路由匹配时才测试嵌套路由。这对于定义具有共同条件(例如主机,路径前缀或其他重复属性)的路由组很有用。另外,这可以优化请求匹配
使用
func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
// 解析路由变量
vars := mux.Vars(r)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Category: %v\n", vars["category"])
}
func main() {
r := mux.NewRouter()
// 绝对路径
r.HandleFunc("/", HomeHandler)
r.HandleFunc("/products", ProductsHandler)
r.HandleFunc("/articles", ArticlesHandler)
// 带参数
r.HandleFunc("/products/{key}", ProductHandler)
r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)
r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler)
http.Handle("/", r)
}
mux: gorilla/mux 实现了一个请求路由和分发的 Go 框架
Transport
定义http handler
使用mux路由转发组件 mux.NewRouter()
func MakeHttpHandler(ctx context.Context, endpoints endpoint.ChannelEndpoints, logger log.Logger) http.Handler {
r := mux.NewRouter()
options := []kithttp.ServerOption{
kithttp.ServerErrorHandler(transport.NewLogErrorHandler(logger)),
kithttp.ServerErrorEncoder(encodeError),
}
r.Methods("GET").Path("/health").Handler(kithttp.NewServer(
endpoints.HealthCheckEndpoint,
decodeHealthCheckRequest,
encodeJsonResponse,
options...,
))
return r
}
encode & decode
func decodeHealthCheckRequest(ctx context.Context, r *http.Request) (interface{}, error) {
return model.HealthRequest{}, nil
}
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
switch err {
default:
w.WriteHeader(http.StatusInternalServerError)
}
json.NewEncoder(w).Encode(map[string]interface{}{
"error": err.Error(),
})
}