代码由此去
代码结构
.- 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,用于支持404
和405
请求。
!!!! 重点来了,我们为什么要定一个那样的路由?又怎么具体的解析参数,响应,处理请求呢?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第三方库,这也是我用于参数解析和校验的工具:
- schema, converts structs to and from form values.
- 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)
路由到这里也就结束了,虽然最重要,但依然比较简单。
最后可能需要一个图来说明?
有疑问加站长微信联系(非本文作者)