19 Go Web 框架(二):框架技术详解

一、 net/http包够用吗?

Go的net相关标准包提供web开发的大多数实现支持,如果你的项目的路径端点在十个以内,如一个简单的企业网站,这当然是够用的。但如果你的项目是构建一个大型电商网站,有成千上万个页面的路径端点,如下:

GET   /goods/:id
POST  /order/:id
DELTE /goods/:id
GET   /goods/:id/name
...
GET   /goods/:id/recommend

在这种情况下,如果只是用net/http包构建,那你不仅要手写url参数解析器,且单单写handler函数就足以让你奔溃。所以对于大多数中大型项目而言,使用框架技术开发是明智之举。

根据经验,只要你的路由带有参数,并且这个项目的API数目超过了10,就尽量不要使用net/http中默认的路由。在Go开源界应用最广泛的router是httpRouter,很多开源的router框架都是基于httpRouter进行一定程度的改造的成果。

二、Web框架技术

现代Web技术大多基于HTTP协议构建,所谓Web框架技术,其本质是在框架底层对HTTP进行有设计的再度封装,最基本的为对Request和对Response处理的封装,除此也包括路由调度,请求参数校验,模板引擎等。再附加一些加速开发和模块管理的套件,如:中间件、MVC分层设计、数据库IO处理等开发套件。使用框架的目的在于屏蔽一些通用的底层编码,封装对用户友好的调用方法,让你把精力集中在业务模块的开发。当然这种封装有利有弊。利在于你不用重复造轮子,有专业的框架团队帮你维护比较规范和安全的底层逻辑,因为框架规范,团队开发中也比较易于沟通;弊在于框架的通用设计有可能不适合你的产品特性,且如果你不深入研究框架底层实现,有可能在项目开发过程中出现一些不可控的bug,这意味着如果你使用一种框架,团队中必须有深入理解该框架的成员,才能让项目开发的进展在可控的范围内。

1. 框架类型:

开源社区有许许多多不同类型的web框架,帮助用户实现快速上手开发,根据不同的项目需求,你可能需要构建API,有可能构建web页面服务,也有可能构建分布式的微服务,这就区分出许多框架类型,以下为框架类型的划分及热度较高的开源项目:

  • Router型框架
    • Ace: 快速开发的Web框架。
    • api2go: Go的JSON API实现。
    • Gin: 一个微框架,类似Martinier的API,重点是小巧、易用、性能好很多,也因为 httprouter 的性能提高了40倍。
    • Goat: Go中的简约REST API服务器
    • goMiddlewareChain: 一个像express.js的中间件调用链框架。
    • Hikaru: 支持独立部署和谷歌AppEngine
    • Hitch: 将 httprouter, httpcontext, 和中间件 捆绑在一起。
    • httpway: 具有httprouter上下文的简单中间件扩展和具有正常关闭支持的服务框架
    • kami: 使用 x/net/context的小型Web框架。
    • Medeina: 灵感来自Ruby的 Roda 和Cuba。
    • Neko: 一个轻量级Web应用程序框架。
    • River: 一个简单轻巧的REST服务框架。
    • Roxanna:httprouter的合并,更好的日志记录和热重载。
    • siesta:具有上下文的可组合HTTP处理程序。
    • xmux: xmux is a httprouter fork on top of xhandler (net/context aware) aware)

基于httpRouter进行简单的封装,然后提供定制的中间件和一些简单的小工具集成比如gin,主打轻量,易学,高性能。

  • 迁移型框架
    • Beego:一个快速开发 Go 应用的 HTTP 框架,他可以用来快速开发 API、Web 及后端服务等各种应用,是一个 RESTful 的框架,主要设计灵感来源于 tornado、sinatra 和 flask 这三个框架,但是结合了 Go 本身的一些特性(interface、struct 嵌入等)而设计的一个框架。beego 是基于八大独立的模块构建的,是一个高度解耦的框架。设计之初就考虑功能模块化,用户即使不使用 beego 的 HTTP 逻辑,也依旧可以使用这些独立模块。
    • Iris:Iris的理念是为HTTP提供强大的工具,使其成为单页面应用程序,网站,混合或公共HTTP API的理想解决方案。

借鉴其它语言的编程风格,使用MVC开发模式的框架,例如beego、Iris-go等,这种框架方便从其它语言迁移过来的程序员快速上手,快速开发,很多PHP、JAVA程序员对这种框架更容易上手。

  • 代码生成型框架
    • Goa:Goa采用不同的方法来构建服务,使用简单的Go DSL 来描述服务API 的设计。Goa使用描述生成专门的服务帮助程序代码,客户端代码和文档。Goa可以通过插件扩展,例如 goakit插件生成利用Go套件库的代码。

使用GO DSL描述来生成服务或API代码,支持数据库schema设计,插件设计,大部分代码可直接生成。goa就是这种类型的框架,它着重于构建微服务和API的系统。如果你玩过Yii2或Laravel,你会感觉这种框架开发的套路很熟悉。

  • 微服务型框架
    • Goa:以上。
    • go-micro:Go Micro提供了分布式系统开发的核心要求,包括RPC、事件驱动的异步通信、消息编解码、基于服务发现的负载均衡以及可插拔的插件机制。
    • go-kit:Go kit是一个编程工具包,用于在Go中构建微服务(或优雅的整体)。解决了分布式系统和应用程序架构中的常见问题,因此你可以专注于开发业务。

微服务架构在近年来越来越盛行,在容器化服务的时代,微服务化的应用也代表着未来,go在微服务的开发方面也有得天独厚的优势,这类框架有go-micro、go-kit等,其中go-micro更像是构建微服务的框架,而go-kit更像是微服务开发的工具集。

2. 框架设计解析

Go在2009年才诞生,迄今也就十年历史,在框架技术方面很多都是借鉴于其他语言的框架设计经验,所以在很多使用框架开发的项目中beego很流行,主要是因为人们更熟悉MVC这种开发套路,但如果你使用beego开发过你会发现有些许别扭,相较于MVC的框架,许多Gopher更喜欢使用轻量级的Router型框架构建REST API,或使用微服务框架搭建运行于容器的应用。或许它更符合Go Web开发的设计理念。至于如何选择框架,这得考虑产品的特性和团队的成员技术栈了,没有唯一答案,适合的才是最好的。

下面我们来解析一下大多数框架设计的共同特性吧:

2.1 HTTP封装

Web系统的本质就是对网络通信协议的应用设计,大体上基于HTTP协议进行应用的封装,如许多RESTful风格的API框架,就是完全贴合HTTP协议而设计的。HTTP的一次交互可抽象为Request(请求)和Response(响应),在上一篇中我们了解了Request和Response的具体结构和信息,Request包含八种请求Method来表明客户对资源的操作方式,并赋予请求头、请求参数等信息(在此抽象概述先忽略其它请求报文的处理),而Response则包含服务端在接收到用户请求并进行业务处理后的响应信息,如响应头、状态码、响应体等(在此抽象概述先忽略其他响应报文内容)。这是Web服务端和客户端交互的简单抽象。go的标准库net/http包已实现这些基本的处理,大多web框架根据各自设计的特性进行Request和Response的深度封装,使其对用户开发更友好,或增强其他处理的特性等。

一般框架会对Request和Response进行接口设计或直接设计为结构体实现,在服务中的Handler处理器进行传递,这是net/http包原生的处理方法,但多数框架会把Request和Response以及其他的附加结构封装到一个统一的Context接口或结构体,在服务中的Handler处理器进行传递,如gin:

type Context struct {
    writermem responseWriter
    Request   *http.Request
    Writer    ResponseWriter

    Params   Params
    handlers HandlersChain
    index    int8

    engine *Engine

    // Keys is a key/value pair exclusively for the context of each request.
    Keys map[string]interface{}

    // Errors is a list of errors attached to all the handlers/middlewares who used this context.
    Errors errorMsgs

    // Accepted defines a list of manually accepted formats for content negotiation.
    Accepted []string
}

像你以上看到的,许多应用框架会把一次HTTP的交互中的所有要素统一抽象到一个上下文Context中,由路由调度指派的Handler处理器接收处理,于是你会看到gin框架的做法,也为多数web框架的做法:

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    //将访问路径/ping的请求的处理指派到一个handler匿名函数,传递的是*gin.Context。
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}
2.2 路由设计

在Web交互中,客户端都是通过URL(统一资源定位符)对服务端资源进行HTTP 请求的,一个URL包含资源的请求路径,该路径应由系统路由(router)调度到请求处理器。在常见的Web框架中,router是必备的组件。Go语言圈子里router也时常被称为http的multiplexer。

net/http包内置的ServeMux来完成简单的路由功能,如果开发Web系统对路径中没有带参数要求的话,用net/http标准库中的mux就可以了,但现实并没那么简单,一个稍微复杂的项目都有路径划分及携带参数的要求,特别是RESTful风格的API设计,这就需要对请求路径的URL进行路由调度设计。

在RESTful中除了GET和POST之外,还使用了HTTP协议定义的几种其它的标准化语义。具体包括:

const (
    MethodGet     = "GET"
    MethodHead    = "HEAD"
    MethodPost    = "POST"
    MethodPut     = "PUT"
    MethodPatch   = "PATCH" // RFC 5789
    MethodDelete  = "DELETE"
    MethodConnect = "CONNECT"
    MethodOptions = "OPTIONS"
    MethodTrace   = "TRACE"
)

来看看RESTful中常见的请求路径:

GET /repos/:owner/:repo/comments/:id/reactions
POST /projects/:project_id/columns
PUT /user/starred/:owner/:repo
DELETE /user/starred/:owner/:repo

这是常见的几个RESTful API设计。RESTful风格的API重度依赖请求路径。会将很多参数放在请求URI中。除此之外还会使用很多并不那么常见的HTTP状态码。

如果我们的系统也想要这样的URI设计,使用http包的ServeMux显然不够。

较流行的开源go Web框架大多使用httprouter,或是基于httprouter的变种对路由进行支持。前面提到的github的参数式路由在httprouter中都是可以支持的。

因为httprouter中使用的是显式匹配,所以在设计路由的时候需要规避一些会导致路由冲突的情况,例如:

//conflict:
GET /user/info/:name
GET /user/:id

//no conflict:
GET /user/info/:name
POST /user/:id

简单来讲的话,如果两个路由拥有一致的http方法(指 GET/POST/PUT/DELETE)和请求路径前缀,且在某个位置出现了A路由是wildcard(指:id这种形式)参数,B路由则是普通字符串,那么就会发生路由冲突。路由冲突会在初始化阶段直接panic。

除了正常情况下的路由支持,httprouter也支持对一些特殊情况下的回调函数进行定制,例如404的时候:

r := httprouter.New()
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("oh no, not found"))
})
或者内部panic的时候:

r.PanicHandler = func(w http.ResponseWriter, r *http.Request, c interface{}) {
    log.Printf("Recovering from panic, Reason: %#v", c.(error))
    w.WriteHeader(http.StatusInternalServerError)
    w.Write([]byte(c.(error).Error()))
}

目前开源界较流行的Web框架gin使用的就是httprouter的变种。

2.3 中间件

我们来看看现实开发的场景,假如你正在编写一处产品提出的业务需求,和往常一样,你使用一个路径绑定一个Handler处理器实现业务编码,而正在这时,你的开发主管为了统计每个业务中的耗时任务,要求开发成员在业务代码中插入耗时计算的逻辑,你照做了;再过一段时间,你的开发主管为了收集用户的行为数据,要求开发成员在用户的业务逻辑上安插日志记录,以供后期离线计算,你也照做了;后来随着业务不断扩大,系统业务衍生出了“大会员”子系统,需要对符合要求的用户进行鉴权并过滤,要求开发人员在每个业务处理做权限控制,这时你崩溃了,系统已经随着业务发展变得越来越复杂,而且掺杂了各种业务和非业务的逻辑代码,你还继续一个一个改吗?很显然你已经陷入代码泥潭!

所谓中间件,是在实际处理用户业务逻辑的生命周期中,在对应的节点安插逻辑处理,可以在核心业务逻辑前或后,是剥离非业务逻辑的重要系统组件。

中间件设计像是一个调用链,context在链中各节点传递,各节点进行各自的功能处理:

中间件.png

看上图大家就明白,在核心业务的前后,将非业务逻辑剥离出来,实现预先处理和后置处理的效果,这就是中间件的本质。

实现中间件思路非常简单,Go的高阶函数编程可以非常容易的实现中间件设计,还是以Gin的中间件设计为例:

//编写一个Logger中间件,该中间件统一返回一个HandlerFunc,每个HandlerFunc都传递Context。
func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        t := time.Now()

        // 设置 example 变量
        c.Set("example", "12345")

        //请求前...

        //执行下一层调用节点,根据Use()方法的调用顺序执行,执行完所有中间件后再进入核心业务Handler。
        c.Next()

        //请求后...
        latency := time.Since(t)
        log.Print(latency)

        // 获取发送的 status
        status := c.Writer.Status()
        log.Println(status)
    }
}

func main() {
    s := gin.New()
    //使用中间件调用Use()方法即可
    s.Use(Logger())

    s.GET("/test", func(c *gin.Context) {
        example := c.MustGet("example").(string)

        // 打印:"12345"
        log.Println(example)
    })

    // 监听并在 0.0.0.0:8080 上启动服务
    s.Run(":8080")
}
2.4 请求参数校验
请求参数校验波动拳.jpg

你或许看过上图,这是社区中嘲笑PHP校验请求参数的例子,实际上,这种做法与语言无关,很多没有设计的参数校验都是这种粗糙的写法。这种if-else风暴组织的代码无疑是丑陋的。从代码的易读性和逻辑性角度看,都应该对条件分支进行重构,其实许多if嵌套都可重构为顺序条件结构,来提高代码的优雅程度,在此不便展开。回到正题,其实在Handler写一大堆参数校验的代码是挺难看的,既然参数校验是每个请求必须的流程,有没有什么方法可以把参数校验分离出Handler处理器呢?答案是Validator验证器设计。

从设计的角度讲,我们一定会为每个请求都声明一个结构体。

这里我们引入一个新的validator库:https://github.com/go-playground/validator

以下为使用示例:

import "gopkg.in/go-playground/validator.v9"

type RegisterReq struct {
    // 字符串的 gt=0 表示长度必须 > 0,gt = greater than
    Username       string   `validate:"gt=0"`
    // 同上
    PasswordNew    string   `validate:"gt=0"`
    // eqfield 跨字段相等校验
    PasswordRepeat string   `validate:"eqfield=PasswordNew"`
    // 合法 email 格式校验
    Email          string   `validate:"email"`
}

validate := validator.New()

func validate(req RegisterReq) error {
    err := validate.Struct(req)
    if err != nil {
        doSomething()
        return err
    }
    ...
}

这样就不需要在每个请求进入业务逻辑之前都写重复的validate()函数了。本例中只列出了这个校验器非常简单的几个功能。

我们试着跑一下这个程序,输入参数设置为:

//...

var req = RegisterReq {
    Username       : "Fun",
    PasswordNew    : "abc12138",
    PasswordRepeat : "ABC12138",
    Email          : "[email protected]",
}

err := validate(req)
fmt.Println(err)

// Key: 'RegisterReq.PasswordRepeat' Error:Field validation for
// 'PasswordRepeat' failed on the 'eqfield' tag

错误信息可以针对每种tag定制,各位可查阅各验证器文档,开源社区有许多验证器的独立项目,也可以参考各框架的验证器实现,如有特殊需求也可以自定义验证器。

2.5 数据库交互

在《Go进阶系列》前三篇中,我们已经了解Go操作数据库的大体实现方法,对关系型数据库的操作,主要基于标准库的database/sql包,开源社区也有许多对sql包的再度封装项目,但这些库的使用许多都是对sql原生语句的操作,使用这些包的项目中,你会发现许多非常相似的sql语句,当项目比较大时会充斥许多冗余代码。于是为了提高项目生产效率,许多框架或独立开源项目都提供了ORM或SQL Builder的支持。在进阶系列开篇我们简单使用了gorm。

数据库交互在Web系统中可为是标配组件,因为数据持久化的场景随处可见,在其他语言的框架中,许多框架都内置了数据库交互组件,如PHP的Laravel、Yii等,数据访问层都是框架的标配,但许多静态语言的框架中,数据访问层是独立的框架,如JAVA的Hibernate,框架的设计依赖于语言的特点,由于Go包管理的特点,Go的框架中大部分数据库访问层是独立的,类似在Go Web框架中Router型框架占多数,它们只专注于处理HTTP相关的核心部分,其他的由更专业的数据访问层框架来做,如gorm、xorm等ORM框架,然而有些迁移型框架也包揽数据访问层,如beego、goa等,但这种框架的数据访问层也是独立的模块,是可独立使用的。

Go的数据访问层框架相对比较自由,数据访问层的库大体分三种:

  • 基于database/sql进行再度封装:如sqlx;
  • ORM框架:如gorm、xorm、beego/orm;
  • SQL Builder:如 gendry、 goqu
ORM 利弊

使用ORM框架的项目,基本已经屏蔽了SQL语句的编写,所有的数据库交互都使用对象映射的方式,用操作对象替代SQL语句,这无疑对OOP程序员是福利。

ORM的目的就是屏蔽掉DB层,实际上很多语言的ORM只要把你的类或结构体定义好,再用特定的语法将结构体之间的一对一或者一对多关系表达出来。那么任务就完成了。然后你就可以对这些映射好了数据库表的对象进行各种操作,例如save,create,retrieve,delete。至于ORM在背地里做了什么阴险的勾当,你是不一定清楚的。使用ORM的时候,我们往往比较容易有一种忘记了数据库的直观感受。ORM设计初衷就是为了让数据的操作和存储的具体实现所剥离。但是在上了规模的公司的人们渐渐达成了一个共识,由于隐藏重要的细节,ORM可能是失败的设计。其所隐藏的重要细节对于上了规模的系统开发来说至关重要。

SQL Builder 利弊

相比ORM来说,SQL Builder在SQL和项目可维护性之间取得了比较好的平衡。首先sql builer不像ORM那样屏蔽了过多的细节,其次从开发的角度来讲,SQL Builder简单进行封装后也可以非常高效地完成开发:

where := map[string]interface{} {
    "order_id > ?" : 0,
    "customer_id != ?" : 0,
}
limit := []int{0,100}
orderBy := []string{"id asc", "create_time desc"}

orders := orderModel.GetList(where, limit, orderBy)

读写SQL Builder的相关代码都不费劲,也非常接近SQL语句所表达的意思。所以通过代码就可以对这个查询是否命中数据库索引,是否走了覆盖索引,是否能够用上联合索引进行分析了。

说白了SQL Builder是sql在代码里的一种特殊方言,如果你们没有DBA但研发有自己分析和优化sql的能力,或者你们公司的DBA对于学习这样一些sql的方言没有异议。那么使用SQL Builder是一个比较好的选择,不会导致什么问题。

另外在一些本来也不需要DBA介入的场景内,使用SQL Builder也是可以的,例如你要做一套运维系统,且将MySQL当作了系统中的一个组件,系统的QPS不高,查询不复杂等等。

一旦你做的是高并发的OLTP在线系统,且想在人员充足分工明确的前提下最大程度控制系统的风险,使用SQL Builder就不合适了。

小结

使用什么框架作为数据访问层并无同一说法,完全看项目特性,如项目中对SQL的审核非常严格,建议使用基于database/sql封装的库,这样sql语句就会相对透明;如果你的团队不太注重SQL细节且非常注重开发效率,可使用ORM;相对以上两种而言,SQL Builder是比较折中的做法,如果你能控制系统风险,并可牺牲一些高并发性能,SQL Builder是不错的选择。

2.6 分层架构

说到分层,我们就会想到大名鼎鼎的MVC分层设计模式,在前后端分离架构大热前,MVC分层设计的框架可谓大行其道。在那时MVC单体架构可谓“万能”,只要做web项目第一个想到的就是MVC。

在MVC中,为了方便对业务模块进行划分和团队的分工合作,将程序划分为:

  • 控制器(Controller)- 负责转发请求,对请求进行处理。
  • 视图(View) - 界面设计人员进行图形界面设计。
  • 模型(Model) - 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。
MVC.jpeg

但随着移动互联网的兴起,C/S结构的软件又再度兴起,许多web应用的客户端都独立运行,逐渐兴起大前端热潮,MVC中的V从此分离出去,衍生出MC+V的程序结构:

前后端分离交互图.png

其实在Go兴起的年代,移动互联网应用已是主流,编写Go后端程序很多是不倡导使用MVC模式的,这与go的设计思想不符。但Go是新兴的语言,许多开发者对软件开发的理解还没与Go的设计思想适配,于是乎出现了许多迁移型框架,提供熟悉的设计方法方便其他语言的程序员迁移过来,这当中beego就是代表,其mvc设计是该框架的核心组件,最近兴起的Iris也对MVC结构的程序提供框架级别的支持。对于这种现象其实也有利有弊,利在于让更多的程序员更容易使用GO上手实际项目,弊在于这可能对Go设计Web程序的思想有点误导,Go倡导大道至简,许多更简单高效的Router型框架更切合Go Web的设计思想。

以下提供一个Iris实现MVC的编写示例:

package main

import (
    "github.com/kataras/iris"
    "github.com/kataras/iris/mvc"

    "github.com/kataras/iris/middleware/logger"
    "github.com/kataras/iris/middleware/recover"
)

func newApp() *iris.Application {
    app := iris.New()

    app.Use(recover.New())
    app.Use(logger.New())

    // Serve a controller based on the root Router, "/".
    mvc.New(app).Handle(new(ExampleController))
    return app
}

func main() {
    app := newApp()
    app.Run(iris.Addr(":8080"))
}

// 定义一个控制器
type ExampleController struct{}

// Get serves
// Method:   GET
// Resource: http://localhost:8080
func (c *ExampleController) Get() mvc.Result {
    return mvc.Response{
        ContentType: "text/html",
        Text:        "

Welcome

", } } // GetPing serves // Method: GET // Resource: http://localhost:8080/ping func (c *ExampleController) GetPing() string { return "pong" } // GetHello serves // Method: GET // Resource: http://localhost:8080/hello func (c *ExampleController) GetHello() interface{} { return map[string]string{"message": "Hello Iris!"} } func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) { anyMiddlewareHere := func(ctx iris.Context) { ctx.Application().Logger().Warnf("Inside /custom_path") ctx.Next() } b.Handle("GET", "/custom_path", "CustomHandlerWithoutFollowingTheNamingGuide", anyMiddlewareHere) } func (c *ExampleController) CustomHandlerWithoutFollowingTheNamingGuide() string { return "hello from the custom handler without following the naming guide" }

相比之下,直接使用Router + Handler的编写方式会简单得多:

package main

import (
    "github.com/kataras/iris"

    "github.com/kataras/iris/middleware/logger"
    "github.com/kataras/iris/middleware/recover"
)

func main() {
    app := iris.New()
    app.Logger().SetLevel("debug")

    app.Use(recover.New())
    app.Use(logger.New())

    // Method:   GET
    // Resource: http://localhost:8080
    app.Handle("GET", "/", func(ctx iris.Context) {
        ctx.HTML("

Welcome

") }) // same as app.Handle("GET", "/ping", [...]) // Method: GET // Resource: http://localhost:8080/ping app.Get("/ping", func(ctx iris.Context) { ctx.WriteString("pong") }) // Method: GET // Resource: http://localhost:8080/hello app.Get("/hello", func(ctx iris.Context) { ctx.JSON(iris.Map{"message": "Hello Iris!"}) }) // http://localhost:8080 // http://localhost:8080/ping // http://localhost:8080/hello app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) }

个人建议:不要为了MVC而MVC!在分层设计方面,使用经典的三层架构就可以了,表现层的前端独立分离,后端实现业务逻辑层和数据访问层使软件结构足够精简,更何况我们渐渐进入了容器服务时代,许多大型应用会进化成分布式微服务独立运行,没必要再坚守单体架构的MVC模式了。

2.7 插件机制

所谓插件,是系统中与业务无关的可插拔工具集。插件机制是面向接口编程应用的典范,一个类型的插件可有多个具体的插件实现,系统只对插件接口设计特定的签名方法即可。如缓存插件,RABC插件,日志插件。插件是一种框架的机制设计,当系统允许某些组件是可替换时,把组件编写出插件形式,即编写特定的接口规范,具体的插件实现可由第三方实现,也可由用户自定义。

如Beego框架为了实现某些组件可替换、可由用户自定义,规范了以下插件:

  • gorelic
  • 支付宝 SDK
  • pongo2
  • keenio
  • casbin - RBAC ACL plugins

这种设计有助于用户定制自己的系统。

更极致的使用插件机制的框架像go-micro,它是一个微服务框架。Go Micro为每个分布式系统抽象出接口。因此,Go Micro的接口都是可插拔的,允许其在运行时不可知的情况下仍可支持。所以只要实现接口,可以在内部使用任何的技术。

Go Micro对整个框架的核心组件都做了插件机制的设计,你可以随意选择消息总线插件、RPC通信插件、编解码插件、服务注册插件、负载均衡插件、消息传输插件等。你可以自由选择插件库中的第三方库替换插件,也可以自己实现插件,只要符合框架定义的接口规范。

插件 描述
Broker PubSub messaging; NATS, NSQ, RabbitMQ, Kafka
Client RPC Clients; gRPC, HTTP
Codec Message Encoding; BSON, Mercury
Micro Micro Toolkit Plugins
Registry Service Discovery; Etcd, Gossip, NATS
Selector Load balancing; Label, Cache, Static
Server RPC Servers; gRPC, HTTP
Transport Bidirectional Streaming; NATS, RabbitMQ
Wrapper Middleware; Circuit Breakers, Rate Limiting, Tracing, Monitoring

我们来看一下其实现,以Broker插件为例:

//定义了Broker类型插件的接口
// Broker is an interface used for asynchronous messaging.
type Broker interface {
    Init(...Option) error
    Options() Options
    Address() string
    Connect() error
    Disconnect() error
    Publish(topic string, m *Message, opts ...PublishOption) error
    Subscribe(topic string, h Handler, opts ...SubscribeOption) (Subscriber, error)
    String() string
}

...

我们再看一看一个具体的使用Redis作为Broker的定义:

// Package redis provides a Redis broker
package redis

import (
    "context"
    "errors"
    "strings"
    "time"

    "github.com/gomodule/redigo/redis"
    "github.com/micro/go-micro/broker"
    "github.com/micro/go-micro/codec"
    "github.com/micro/go-micro/codec/json"
    "github.com/micro/go-micro/config/cmd"
)

//此处省略代码...


// broker implementation for Redis.
type redisBroker struct {
    addr  string
    pool  *redis.Pool
    opts  broker.Options
    bopts *brokerOptions
}

// String returns the name of the broker implementation.
func (b *redisBroker) String() string {
    return "redis"
}

// Options returns the options defined for the broker.
func (b *redisBroker) Options() broker.Options {
    return b.opts
}

// Address returns the address the broker will use to create new connections.
// This will be set only after Connect is called.
func (b *redisBroker) Address() string {
    return b.addr
}

// Init sets or overrides broker options.
func (b *redisBroker) Init(opts ...broker.Option) error {
        //此处省略代码...
}

// Connect establishes a connection to Redis which provides the
// pub/sub implementation.
func (b *redisBroker) Connect() error {
    //此处省略代码...
}

// Disconnect closes the connection pool.
func (b *redisBroker) Disconnect() error {
    //此处省略代码...
}

// Publish publishes a message.
func (b *redisBroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error {
        //此处省略代码...
}

// Subscribe returns a subscriber for the topic and handler.
func (b *redisBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
    //此处省略代码...

}

// NewBroker returns a new broker implemented using the Redis pub/sub
// protocol. The connection address may be a fully qualified IANA address such
// as: redis://user:secret@localhost:6379/0?foo=bar&qux=baz
func NewBroker(opts ...broker.Option) broker.Broker {
    // Default options.
    bopts := &brokerOptions{
        maxIdle:        DefaultMaxIdle,
        maxActive:      DefaultMaxActive,
        idleTimeout:    DefaultIdleTimeout,
        connectTimeout: DefaultConnectTimeout,
        readTimeout:    DefaultReadTimeout,
        writeTimeout:   DefaultWriteTimeout,
    }

    // Initialize with empty broker options.
    options := broker.Options{
        Codec:   json.Marshaler{},
        Context: context.WithValue(context.Background(), optionsKey, bopts),
    }

    for _, o := range opts {
        o(&options)
    }

    return &redisBroker{
        opts:  options,
        bopts: bopts,
    }
}

可见Redis Broker插件实现了Broker插件类型的接口签名方法。Micro框架就是根据插件机制搭建的,你可以根据插件自由的定制自己的微服务系统。

三、总结

Web技术发展日新月异,框架也层出不穷,面对如此眼花缭乱的技术,我们难免会心生畏惧,那么多框架,那么多技术该如何选择?刚学完这个框架,又有更新更好的框架出来。对于这个问题,我认为我们不应该迷信框架,Web技术的本质是什么,它的底层核心技术是什么?我们只要理解其本质,其他从其衍生的技术都万变不离其宗。Web框架的设计初衷是屏蔽开发底层的逻辑使我们更专注于业务,但由于其屏蔽底层逻辑,我们更需要深入理解它,读懂它。

至此《Go进阶系列》告一段落,之后计划输出《Go实战系列》和《Go高级系列》,如果对你有帮助,不要忘了点个赞或关注本人哈,谢谢!

你可能感兴趣的:(19 Go Web 框架(二):框架技术详解)