1

代码由此去

代码结构

.- router包
├── middleware
│   ├── param.go     // 参数解析支持
│   ├── readme.md    // 文档
│   ├── reqlog.go    // 记录请求日志
│   ├── response.go  // 响应的相关函数
│   └── safe.go      // safe recover功能
└── router.go        // 入口和request处理逻辑
整个router与gweb其他模块并不耦合,只会依赖于logger。其中router.go是整个路由的入口的,而middleware提供一些工具函数和简单的封装。

router处理逻辑

router.go 主要做了以下工作:

  • 定义路由,及Controller注册
  • 自定义http.Handler, 也就是ApiHandler,实现ServeHTTP方法。

自定义路由Route

type Route struct {
    Path    string         // req URI
    Method  string         // GET,POST...
    Fn      interface{}    // URI_METHOD hanlde Func
    ReqPool *sync.Pool     // req form pool
    ResPool *sync.Pool     // response pool
}

在使用的时候使用一个map[string][]*Route结构来存储URI和Method对应的路由处理函数。脑补一下,实际的存储是这样的:

{
    "/hello": [
        &Route{
            Path: "/hello",
            Method: "GET",
            Fn: someGetFunc,
            ReqPool: someGetReqPool,
            ResPool: someGetRespPool
        },
        &Route{
            Path: "/hello",
            Method: "POST",
            Fn: somePostFunc,
            ReqPool: somePostReqPool,
            ResPool: somePostRespPool
        },
        // ... more
    ],
    // ... more
}
用这样的结构主要是为了支持Restful API,其他的暂时没有考虑

ApiHanlder

router.go 定义了一个ApiHandler如下:

type ApiHandler struct {
    NotFound         http.Handler
    MethodNotAllowed http.Handler
}

只是简单的包含了两个hander,用于支持404405请求。

!!!! 重点来了,我们为什么要定一个那样的路由?又怎么具体的解析参数,响应,处理请求呢?Talk is Cheap, show me the Code

func (a *ApiHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {

    defer middleware.SafeHandler(w, req)

    path := req.URL.Path
    route, ok := foundRoute(path, req.Method)

    //// handle 404
    if !ok {
        if a.NotFound != nil {
            a.NotFound.ServeHTTP(w, req)
        } else {
            http.NotFound(w, req)
        }
        return
    }

    // not nil and to, ref to foundRoute
    if route != nil {
        goto Found
    }

    //// handle 405
    if !allowed(path, req.Method) {
        if a.MethodNotAllowed != nil {
            a.MethodNotAllowed.ServeHTTP(w, req)
        } else {
            http.Error(w,
                http.StatusText(http.StatusMethodNotAllowed),
                http.StatusMethodNotAllowed,
            )
        }
        return
    }

Found:
    //// normal handle
    reqRes := route.ReqPool.Get()
    defer route.ReqPool.Put(reqRes)

    // parse params
    if errs := middleware.ParseParams(w, req, reqRes); len(errs) != 0 {
        je := new(middleware.JsonErr)
        Response(je, NewCodeInfo(CodeParamInvalid, ""))
        je.Errs = errs
        middleware.ResponseErrorJson(w, je)
        return
    }
    in := make([]reflect.Value, 1)
    in[0] = reflect.ValueOf(reqRes)
    Fn := reflect.ValueOf(route.Fn)

    //// Call web server handle function
    out := Fn.Call(in)

    //// response to client
    resp := out[0].Interface()
    defer route.ResPool.Put(resp)

    middleware.ResponseJson(w, resp)
    return
}

流程正如你所想的那样。处理405,405等,然后使用路由Route,进行参数解析,校验,调用,返回响应等操作。设计参照了httprouter。关于参数解析和响应,马上就到。

参数解析和校验(param.go)

参数的解析,一开始考虑的只有GET,POST,PUT,DELETE 没有考虑JSON和文件的解析。因为一开始忙于搭框架是一方面,其次因为我用的schema不支持(我也没仔细看,自己实现起来也很简单)。

这里就推荐两个我常用的golang第三方库,这也是我用于参数解析和校验的工具:

  1. schema, converts structs to and from form values.
  2. beego/validation,valid the struct
// ParseParams, parse params into reqRes from req.Form, and support
// form-data, json-body
// TODO: support parse file
func ParseParams(w http.ResponseWriter, req *http.Request, reqRes interface{}) (errs ParamErrors) {
    switch req.Method {
    case http.MethodGet:
        req.ParseForm()
    case http.MethodPost, http.MethodPut:
        req.ParseMultipartForm(20 << 32)
    default:
        req.ParseForm()
    }
    // log request
    logReq(req)

    // if should parse Json body
    // parse json into reqRes
    if shouldParseJson(reqRes) {
        data, err := getJsonData(req)
        if err != nil {
            errs = append(errs, NewParamError("parse.json", err.Error(), ""))
            return
        }
        if err = json.Unmarshal(data, reqRes); err != nil {
            errs = append(errs, NewParamError("json.unmarshal", err.Error(), ""))
            return
        }
        bs, _ := json.Marshal(reqRes)
        ReqL.Info("pasing json body: " + string(bs))
        goto Valid
    }

    // if has FILES field,
    // so parese req to get attachment files
    if shouldParseFile(reqRes) {
        AppL.Info("should parse files")
        if req.MultipartForm == nil || req.MultipartForm.File == nil {
            errs = append(errs, NewParamError("FILES", "empty file param", ""))
            return
        }
        rv := reflect.ValueOf(reqRes).Elem().FieldByName("FILES")
        // typ := reflect.ValueOf(reqRes).Elem().FieldByName("FILES").Type()
        filesMap := reflect.MakeMap(rv.Type())

        // parse file loop
        for key, _ := range req.MultipartForm.File {
            file, file_header, err := req.FormFile(key)
            if err != nil {
                errs = append(errs, NewParamError(Fstring("parse request.FormFile: %s", key),
                    err.Error(), ""))
            }
            defer file.Close()

            filesMap.SetMapIndex(
                reflect.ValueOf(key),
                reflect.ValueOf(ParamFile{
                    File:       file,
                    FileHeader: *file_header,
                }),
            )
        } // loop end

        // set value to reqRes.Field `FILES`
        rv.Set(filesMap)

        if len(errs) != 0 {
            return
        }
    }

    // decode
    if err := decoder.Decode(reqRes, req.Form); err != nil {
        errs = append(errs, NewParamError("decoder", err.Error(), ""))
        return
    }

Valid:
    // valid
    v := poolValid.Get().(*valid.Validation)
    if ok, err := v.Valid(reqRes); err != nil {
        errs = append(errs, NewParamError("validation", err.Error(), ""))
    } else if !ok {
        for _, err := range v.Errors {
            errs = append(errs, NewParamErrorFromValidError(err))
        }
    }
    return
}

或许有人会关心shouldParseJson是怎么弄的?如下:

// shouldParseJson check `i` has field `JSON`
func shouldParseJson(i interface{}) bool {
    v := reflect.ValueOf(i).Elem()
    if _, ok := v.Type().FieldByName("JSON"); !ok {
        return false
    }
    return true
}

这里强制设定了reqRes必须含有JSON字段,才会解析jsonbody;必须含有FILES才会解析请求中的文件。因此在写业务逻辑的时候,要写成这个样子了,这些示例都在demo

/*
 * JSON-Body Demo
 */
type HelloJsonBodyForm struct {
    JSON bool   `schema:"-" json:"-"` // 注意schema标签要设置“-”
    Name string `schema:"name" valid:"Required" json:"name"`
    Age  int    `schema:"age" valid:"Required;Min(0)" json:"age"`
}

var PoolHelloJsonBodyForm = &sync.Pool{New: func() interface{} { return &HelloJsonBodyForm{} }}

type HelloJsonBodyResp struct {
    CodeInfo
    Tip string `json:"tip"`
}

var PoolHelloJsonBodyResp = &sync.Pool{New: func() interface{} { return &HelloJsonBodyResp{} }}

func HelloJsonBody(req *HelloJsonBodyForm) *HelloJsonBodyResp {
    resp := PoolHelloJsonBodyResp.Get().(*HelloJsonBodyResp)
    defer PoolHelloJsonBodyResp.Put(resp)

    resp.Tip = fmt.Sprintf("JSON-Body Hello, %s! your age[%d] is valid to access", req.Name, req.Age)

    Response(resp, NewCodeInfo(CodeOk, ""))
    return resp
}

/*
 * File Hanlder demo
 */

type HelloFileForm struct {
    FILES map[string]mw.ParamFile `schema:"-" json:"-"` // 注意schema标签设置“-”和FILES的type保持一直
    Name  string                  `schema:"name" valid:"Required"`
    Age   int                     `schema:"age" valid:"Required"`
}

var PoolHelloFileForm = &sync.Pool{New: func() interface{} { return &HelloFileForm{} }}

type HelloFileResp struct {
    CodeInfo
    Data struct {
        Tip  string `json:"tip"`
        Name string `json:"name"`
        Age  int    `json:"age"`
    } `json:"data"`
}

var PoolHelloFileResp = &sync.Pool{New: func() interface{} { return &HelloFileResp{} }}

func HelloFile(req *HelloFileForm) *HelloFileResp {
    resp := PoolHelloFileResp.Get().(*HelloFileResp)
    defer PoolHelloFileResp.Put(resp)

    resp.Data.Tip = "foo"
    for key, paramFile := range req.FILES {
        AppL.Infof("%s:%s\n", key, paramFile.FileHeader.Filename)
        s, _ := bufio.NewReader(paramFile.File).ReadString(0)
        resp.Data.Tip += s
    }

    resp.Data.Name = req.Name
    resp.Data.Age = req.Age

    Response(resp, NewCodeInfo(CodeOk, ""))
    return resp
}

响应(response.go)

gweb目的在于总结一个使用Json数据格式来进行交互的web服务结构。响应体设计如下:

{
    "code": 0,     // 错误码,或许应该使用“error_code”, 不过不影响
    "message": ""  // 错误消息
    "user": {
        "name": "yep",
        // ... other
    }
}

结合上面的Demo,大概看出来了,响应并没什么花里胡哨的功能。只是需要将*resp使用json.Marshal转为字符串,并发送给客户端就了事。

    // ...
    //// Call web server handle function
    out := Fn.Call(in)

    //// response to client
    resp := out[0].Interface()
    defer route.ResPool.Put(resp)

    middleware.ResponseJson(w, resp)

路由到这里也就结束了,虽然最重要,但依然比较简单。

最后可能需要一个图来说明?

图片描述


yeqown
298 声望10 粉丝

coding, coding, coding in any way