好未来开源框架 go-zero:如何用它进行 rest 开发?
go-zero 是一个集成了各种工程实践的 web 和 rpc 框架,其中 rest 是 web 框架模块,基于 Go 语言原生的 http 包进行构建,是一个轻量的,高性能的,功能完整的,简单易用的 web 框架。
服务创建
go-zero 中创建 http 服务非常简单,官方推荐使用goctl[1]工具来生成。为了方便演示,这里通过手动创建服务,代码如下
package main
import (
"log"
"net/http"
"github.com/tal-tech/go-zero/core/logx"
"github.com/tal-tech/go-zero/core/service"
"github.com/tal-tech/go-zero/rest"
"github.com/tal-tech/go-zero/rest/httpx"
)
func main() {
srv, err := rest.NewServer(rest.RestConf{
Port: 9090, // 侦听端口
ServiceConf: service.ServiceConf{
Log: logx.LogConf{Path: "./logs"}, // 日志路径
},
})
if err != nil {
log.Fatal(err)
}
defer srv.Stop()
// 注册路由
srv.AddRoutes([]rest.Route{
{
Method: http.MethodGet,
Path: "/user/info",
Handler: userInfo,
},
})
srv.Start() // 启动服务
}
type User struct {
Name string `json:"name"`
Addr string `json:"addr"`
Level int `json:"level"`
}
func userInfo(w http.ResponseWriter, r *http.Request) {
var req struct {
UserId int64 `form:"user_id"` // 定义参数
}
if err := httpx.Parse(r, &req); err != nil { // 解析参数
httpx.Error(w, err)
return
}
users := map[int64]*User{
1: &User{"go-zero", "shanghai", 1},
2: &User{"go-queue", "beijing", 2},
}
httpx.WriteJson(w, http.StatusOK, users[req.UserId]) // 返回结果
}
通过 rest.NewServer 创建服务,示例配置了端口号和日志路径,服务启动后侦听在 9090 端口,并在当前目录下创建 logs 目录同时创建各等级日志文件
然后通过 srv.AddRoutes 注册路由,每个路由需要定义该路由的方法、Path 和 Handler,其中 Handler 类型为 http.HandlerFunc
最后通过 srv.Start 启动服务,启动服务后通过访问http://localhost:9090/user/info?user_id=1可以看到返回结果
{
name: "go-zero",
addr: "shanghai",
level: 1
}
到此一个简单的 http 服务就创建完成了,可见使用 rest 创建 http 服务非常简单,主要分为三个步骤:创建 Server、注册路由、启动服务
JWT 鉴权
鉴权几乎是每个应用必备的能力,鉴权的方式很多,而 jwt 是其中比较简单和可靠的一种方式,在 rest 框架中内置了 jwt 鉴权功能,jwt 的原理流程如下图
rest 框架中通过 rest.WithJwt(secret)启用 jwt 鉴权,其中 secret 为服务器秘钥是不能泄露的,因为需要使用 secret 来算签名验证 payload 是否被篡改,如果 secret 泄露客户端就可以自行签发 token,黑客就能肆意篡改 token 了。我们基于上面的例子进行改造来验证在 rest 中如何使用 jwt 鉴权
获取 jwt
第一步客户端需要先获取 jwt,在登录接口中实现 jwt 生成逻辑
srv.AddRoute(rest.Route{
Method: http.MethodPost,
Path: "/user/login",
Handler: userLogin,
})
为了演示方便,userLogin 的逻辑非常简单,主要是获取信息然后生成 jwt,获取到的信息存入 jwt payload 中,然后返回 jwt
func userLogin(w http.ResponseWriter, r *http.Request) {
var req struct {
UserName string `json:"user_name"`
UserId int `json:"user_id"`
}
if err := httpx.Parse(r, &req); err != nil {
httpx.Error(w, err)
return
}
token, _ := genToken(accessSecret, map[string]interface{}{
"user_id": req.UserId,
"user_name": req.UserName,
}, accessExpire)
httpx.WriteJson(w, http.StatusOK, struct {
UserId int `json:"user_id"`
UserName string `json:"user_name"`
Token string `json:"token"`
}{
UserId: req.UserId,
UserName: req.UserName,
Token: token,
})
}
生成 jwt 的方法如下
func genToken(secret string, payload map[string]interface{}, expire int64) (string, error) {
now := time.Now().Unix()
claims := make(jwt.MapClaims)
claims["exp"] = now + expire
claims["iat"] = now
for k, v := range payload {
claims[k] = v
}
token := jwt.New(jwt.SigningMethodHS256)
token.Claims = claims
return token.SignedString([]byte(secret))
}
启动服务后通过 cURL 访问
curl -X "POST" "http://localhost:9090/user/login" \
-H 'Content-Type: application/json; charset=utf-8' \
-d $'{
"user_name": "gozero",
"user_id": 666
}'
会得到如下返回结果
{
"user_id": 666,
"user_name": "gozero",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDYxMDgwNDcsImlhdCI6MTYwNTUwMzI0NywidXNlcl9pZCI6NjY2LCJ1c2VyX25hbWUiOiJnb3plcm8ifQ.hhMd5gc3F9xZwCUoiuFqAWH48xptqnNGph0AKVkTmqM"
}
添加 Header
通过 rest.WithJwt(accessSecret)启用 jwt 鉴权
srv.AddRoute(rest.Route{
Method: http.MethodGet,
Path: "/user/data",
Handler: userData,
}, rest.WithJwt(accessSecret))
访问/user/data 接口返回 401 Unauthorized 鉴权不通过,添加 Authorization Header,即能正常访问
curl "http://localhost:9090/user/data?user_id=1" \
-H 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDYxMDgwNDcsImlhdCI6MTYwNTUwMzI0NywidXNlcl9pZCI6NjY2LCJ1c2VyX25hbWUiOiJnb3plcm8ifQ.hhMd5gc3F9xZwCUoiuFqAWH48xptqnNGph0AKVkTmqM'
获取信息
一般会将用户的信息比如用户 id 或者用户名存入 jwt 的 payload 中,然后从 jwt 的 payload 中解析出我们预存的信息,即可知道本次请求时哪个用户发起的
func userData(w http.ResponseWriter, r *http.Request) {
var jwt struct {
UserId int `ctx:"user_id"`
UserName string `ctx:"user_name"`
}
err := contextx.For(r.Context(), &jwt)
if err != nil {
httpx.Error(w, err)
}
httpx.WriteJson(w, http.StatusOK, struct {
UserId int `json:"user_id"`
UserName string `json:"user_name"`
}{
UserId: jwt.UserId,
UserName: jwt.UserName,
})
}
实现原理
jwt 鉴权的实现在 authhandler.go 中,实现原理也比较简单,先根据 secret 解析 jwt token,验证 token 是否有效,无效或者验证出错则返回 401 Unauthorized
func unauthorized(w http.ResponseWriter, r *http.Request, err error, callback UnauthorizedCallback) {
writer := newGuardedResponseWriter(w)
if err != nil {
detailAuthLog(r, err.Error())
} else {
detailAuthLog(r, noDetailReason)
}
if callback != nil {
callback(writer, r, err)
}
writer.WriteHeader(http.StatusUnauthorized)
}
验证通过后把 payload 中的信息存入 http request 的 context 中
ctx := r.Context()
for k, v := range claims {
switch k {
case jwtAudience, jwtExpire, jwtId, jwtIssueAt, jwtIssuer, jwtNotBefore, jwtSubject:
// ignore the standard claims
default:
ctx = context.WithValue(ctx, k, v)
}
}
next.ServeHTTP(w, r.WithContext(ctx))
中间件
web 框架中的中间件是实现业务和非业务功能解耦的一种方式,在 web 框架中我们可以通过中间件来实现诸如鉴权、限流、熔断等等功能,中间件的原理流程如下图
rest 框架中内置了非常丰富的中间件,在 rest/handler 路径下,通过alice[2]工具把所有中间件链接起来,当发起请求时会依次通过每一个中间件,当满足所有条件后最终请求才会到达真正的业务 Handler 执行业务逻辑,上面介绍的 jwt 鉴权就是通过 authHandler 来实现的。由于内置中间件比较多篇幅有限不能一一介绍,感兴趣的伙伴可以自行学习,这里我们介绍一下 prometheus 指标收集的中间件 PromethousHandler,代码如下
func PromethousHandler(path string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
startTime := timex.Now() // 起始时间
cw := &security.WithCodeResponseWriter{Writer: w}
defer func() {
// 耗时
metricServerReqDur.Observe(int64(timex.Since(startTime)/time.Millisecond), path)
// code码
metricServerReqCodeTotal.Inc(path, strconv.Itoa(cw.Code))
}()
next.ServeHTTP(cw, r)
})
}
}
在该中间件中,在请求开始时记录了起始时间,在请求结束后在 defer 中通过 prometheus 的 Histogram 和 Counter 数据类型分别记录了当前请求 path 的耗时和返回的 code 码,此时我们通过访问http://127.0.0.1:9101/metrics即可查看相关的指标信息
路由原理
rest 框架中通过 AddRoutes 方法来注册路由,每一个 Route 有 Method、Path 和 Handler 三个属性,Handler 类型为 http.HandlerFunc,添加的路由会被换成 featuredRoutes 定义如下
featuredRoutes struct {
priority bool // 是否优先级
jwt jwtSetting // jwt配置
signature signatureSetting // 验签配置
routes []Route // 通过AddRoutes添加的路由
}
featuredRoutes 通过 engine 的 AddRoutes 添加到 engine 的 routes 属性中
func (s *engine) AddRoutes(r featuredRoutes) {
s.routes = append(s.routes, r)
}
调用 Start 方法启动服务后会调用 engine 的 Start 方法,然后会调用 StartWithRouter 方法,该方法内通过 bindRoutes 绑定路由
func (s *engine) bindRoutes(router httpx.Router) error {
metrics := s.createMetrics()
for _, fr := range s.routes {
if err := s.bindFeaturedRoutes(router, fr, metrics); err != nil { // 绑定路由
return err
}
}
return nil
}
最终会调用 patRouter 的 Handle 方法进行绑定,patRouter 实现了 Router 接口
type Router interface {
http.Handler
Handle(method string, path string, handler http.Handler) error
SetNotFoundHandler(handler http.Handler)
SetNotAllowedHandler(handler http.Handler)
}
patRouter 中每一种请求方法都对应一个树形结构,每个树节点有两个属性 item 为 path 对应的 handler,而 children 为带路径参数和不带路径参数对应的树节点, 定义如下:
node struct {
item interface{}
children [2]map[string]*node
}
Tree struct {
root *node
}
通过 Tree 的 Add 方法把不同 path 与对应的 handler 注册到该树上我们通过一个图来展示下该树的存储结构,比如我们定义路由如下
{
Method: http.MethodGet,
Path: "/user",
Handler: userHander,
},
{
Method: http.MethodGet,
Path: "/user/infos",
Handler: infosHandler,
},
{
Method: http.MethodGet,
Path: "/user/info/:id",
Handler: infoHandler,
},
路由存储的树形结构如下图
当请求来的时候会调用 patRouter 的 ServeHTTP 方法,在该方法中通过 tree.Search 方法找到对应的 handler 进行执行,否则会执行 notFound 或者 notAllow 的逻辑
func (pr *patRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
reqPath := path.Clean(r.URL.Path)
if tree, ok := pr.trees[r.Method]; ok {
if result, ok := tree.Search(reqPath); ok { // 在树中搜索对应的handler
if len(result.Params) > 0 {
r = context.WithPathVars(r, result.Params)
}
result.Item.(http.Handler).ServeHTTP(w, r)
return
}
}
allow, ok := pr.methodNotAllowed(r.Method, reqPath)
if !ok {
pr.handleNotFound(w, r)
return
}
if pr.notAllowed != nil {
pr.notAllowed.ServeHTTP(w, r)
} else {
w.Header().Set(allowHeader, allow)
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
总结
本文从整体上介绍了 rest,通过该篇文章能够基本了解 rest 的设计和主要功能,其中中间件部分是重点,里面集成了各种服务治理相关的功能,并且是自动集成的不需要我们做任何配置,其他功能比如参数自动效验等功能由于篇幅有限在这里就不做介绍了,感兴趣的朋友可以自行查看官方文档进行学习。go-zero 中不光有 http 协议还提供了 rpc 协议和各种提高性能和开发效率的工具,是一款值得我们深入学习和研究的框架。
项目地址
https://github.com/tal-tech/go-zero[3]
如果觉得文章不错,欢迎 github[4] 点个 star ?
参考资料
goctl: https://github.com/tal-tech/go-zero/tree/master/tools/goctl
[2]alice: https://github.com/justinas/alice
[3]https://github.com/tal-tech/go-zero: https://github.com/tal-tech/go-zero
[4]github: https://github.com/tal-tech/go-zero
推荐阅读