一、 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在链中各节点传递,各节点进行各自的功能处理:
看上图大家就明白,在核心业务的前后,将非业务逻辑剥离出来,实现预先处理和后置处理的效果,这就是中间件的本质。
实现中间件思路非常简单,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 请求参数校验
你或许看过上图,这是社区中嘲笑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) - 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。
但随着移动互联网的兴起,C/S结构的软件又再度兴起,许多web应用的客户端都独立运行,逐渐兴起大前端热潮,MVC中的V从此分离出去,衍生出MC+V的程序结构:
其实在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高级系列》,如果对你有帮助,不要忘了点个赞或关注本人哈,谢谢!