由此去
代码结构
.- 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和文件的解析。因为一开始忙于搭框架是一方面,其次因为我用的 不支持(我也没仔细看,自己实现起来也很简单)。
这里就推荐两个我常用的golang第三方库,这也是我用于参数解析和校验的工具:
- , converts structs to and from form values.
- ,valid the struct
// ParseParams, parse params into reqRes from req.Form, and support// form-data, json-body// TODO: support parse filefunc 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
才会解析请求中的文件。因此在写业务逻辑的时候,要写成这个样子了,这些示例都在:
/* * 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)
路由到这里也就结束了,虽然最重要,但依然比较简单。
最后可能需要一个图来说明?