代码由此去
代码结构
.- 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 URIMethod string // GET,POST...Fn interface{} // URI_METHOD hanlde FuncReqPool *sync.Pool // req form poolResPool *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.HandlerMethodNotAllowed 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.Pathroute, ok := foundRoute(path, req.Method)handle 404if !ok {if a.NotFound != nil {a.NotFound.ServeHTTP(w, req)} else {http.NotFound(w, req)}return}// not nil and to, ref to foundRouteif route != nil {goto Found}handle 405if !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 handlereqRes := route.ReqPool.Get()defer route.ReqPool.Put(reqRes)// parse paramsif errs := middleware.ParseParams(w, req, reqRes); len(errs) != 0 {je := new(middleware.JsonErr)Response(je, NewCodeInfo(CodeParamInvalid, ""))je.Errs = errsmiddleware.ResponseErrorJson(w, je)return}in := make([]reflect.Value, 1)in[0] = reflect.ValueOf(reqRes)Fn := reflect.ValueOf(route.Fn)Call web server handle functionout := Fn.Call(in)response to clientresp := 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 requestlogReq(req)// if should parse Json body// parse json into reqResif shouldParseJson(reqRes) {data, err :&#61; getJsonData(req)if err !&#61; nil {errs &#61; append(errs, NewParamError("parse.json", err.Error(), ""))return}if err &#61; json.Unmarshal(data, reqRes); err !&#61; nil {errs &#61; append(errs, NewParamError("json.unmarshal", err.Error(), ""))return}bs, _ :&#61; json.Marshal(reqRes)ReqL.Info("pasing json body: " &#43; string(bs))goto Valid}// if has FILES field,// so parese req to get attachment filesif shouldParseFile(reqRes) {AppL.Info("should parse files")if req.MultipartForm &#61;&#61; nil || req.MultipartForm.File &#61;&#61; nil {errs &#61; append(errs, NewParamError("FILES", "empty file param", ""))return}rv :&#61; reflect.ValueOf(reqRes).Elem().FieldByName("FILES")// typ :&#61; reflect.ValueOf(reqRes).Elem().FieldByName("FILES").Type()filesMap :&#61; reflect.MakeMap(rv.Type())// parse file loopfor key, _ :&#61; range req.MultipartForm.File {file, file_header, err :&#61; req.FormFile(key)if err !&#61; nil {errs &#61; 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 &#96;FILES&#96;rv.Set(filesMap)if len(errs) !&#61; 0 {return}}// decodeif err :&#61; decoder.Decode(reqRes, req.Form); err !&#61; nil {errs &#61; append(errs, NewParamError("decoder", err.Error(), ""))return}Valid:// validv :&#61; poolValid.Get().(*valid.Validation)if ok, err :&#61; v.Valid(reqRes); err !&#61; nil {errs &#61; append(errs, NewParamError("validation", err.Error(), ""))} else if !ok {for _, err :&#61; range v.Errors {errs &#61; append(errs, NewParamErrorFromValidError(err))}}return
}
或许有人会关心shouldParseJson
是怎么弄的&#xff1f;如下:
// shouldParseJson check &#96;i&#96; has field &#96;JSON&#96;
func shouldParseJson(i interface{}) bool {v :&#61; reflect.ValueOf(i).Elem()if _, ok :&#61; v.Type().FieldByName("JSON"); !ok {return false}return true
}
这里强制设定了reqRes必须含有JSON
字段&#xff0c;才会解析jsonbody&#xff1b;必须含有FILES
才会解析请求中的文件。因此在写业务逻辑的时候&#xff0c;要写成这个样子了,这些示例都在demo&#xff1a;
/** JSON-Body Demo*/
type HelloJsonBodyForm struct {JSON bool &#96;schema:"-" json:"-"&#96; // 注意schema标签要设置“-”Name string &#96;schema:"name" valid:"Required" json:"name"&#96;Age int &#96;schema:"age" valid:"Required;Min(0)" json:"age"&#96;
}var PoolHelloJsonBodyForm &#61; &sync.Pool{New: func() interface{} { return &HelloJsonBodyForm{} }}type HelloJsonBodyResp struct {CodeInfoTip string &#96;json:"tip"&#96;
}var PoolHelloJsonBodyResp &#61; &sync.Pool{New: func() interface{} { return &HelloJsonBodyResp{} }}func HelloJsonBody(req *HelloJsonBodyForm) *HelloJsonBodyResp {resp :&#61; PoolHelloJsonBodyResp.Get().(*HelloJsonBodyResp)defer PoolHelloJsonBodyResp.Put(resp)resp.Tip &#61; 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 &#96;schema:"-" json:"-"&#96; // 注意schema标签设置“-”和FILES的type保持一直Name string &#96;schema:"name" valid:"Required"&#96;Age int &#96;schema:"age" valid:"Required"&#96;
}var PoolHelloFileForm &#61; &sync.Pool{New: func() interface{} { return &HelloFileForm{} }}type HelloFileResp struct {CodeInfoData struct {Tip string &#96;json:"tip"&#96;Name string &#96;json:"name"&#96;Age int &#96;json:"age"&#96;} &#96;json:"data"&#96;
}var PoolHelloFileResp &#61; &sync.Pool{New: func() interface{} { return &HelloFileResp{} }}func HelloFile(req *HelloFileForm) *HelloFileResp {resp :&#61; PoolHelloFileResp.Get().(*HelloFileResp)defer PoolHelloFileResp.Put(resp)resp.Data.Tip &#61; "foo"for key, paramFile :&#61; range req.FILES {AppL.Infof("%s:%s\n", key, paramFile.FileHeader.Filename)s, _ :&#61; bufio.NewReader(paramFile.File).ReadString(0)resp.Data.Tip &#43;&#61; s}resp.Data.Name &#61; req.Nameresp.Data.Age &#61; req.AgeResponse(resp, NewCodeInfo(CodeOk, ""))return resp
}
响应(response.go)
gweb
目的在于总结一个使用Json
数据格式来进行交互的web服务结构。响应体设计如下&#xff1a;
{"code": 0, // 错误码&#xff0c;或许应该使用“error_code”, 不过不影响"message": "" // 错误消息"user": {"name": "yep",// ... other}
}
结合上面的Demo&#xff0c;大概看出来了&#xff0c;响应并没什么花里胡哨的功能。只是需要将*resp
使用json.Marshal
转为字符串&#xff0c;并发送给客户端就了事。
// ...Call web server handle functionout :&#61; Fn.Call(in)response to clientresp :&#61; out[0].Interface()defer route.ResPool.Put(resp)middleware.ResponseJson(w, resp)
路由到这里也就结束了&#xff0c;虽然最重要&#xff0c;但依然比较简单。