代碼由此去git
.- router包 ├── middleware │ ├── param.go // 參數解析支持 │ ├── readme.md // 文檔 │ ├── reqlog.go // 記錄請求日誌 │ ├── response.go // 響應的相關函數 │ └── safe.go // safe recover功能 └── router.go // 入口和request處理邏輯
整個router與gweb其餘模塊並不耦合,只會依賴於logger。其中
router.go
是整個路由的入口的,而middleware提供一些工具函數和簡單的封裝。
router.go
主要作了如下工做:github
http.Handler
, 也就是ApiHandler
,實現ServeHTTP
方法。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對應的路由處理函數。腦補一下,實際的存儲是這樣的:golang
{ "/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,其餘的暫時沒有考慮
router.go
定義了一個ApiHandler
以下:web
type ApiHandler struct { NotFound http.Handler MethodNotAllowed http.Handler }
只是簡單的包含了兩個hander,用於支持404
和405
請求。json
!!!! 重點來了,咱們爲何要定一個那樣的路由?又怎麼具體的解析參數,響應,處理請求呢?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
。關於參數解析和響應,立刻就到。mvc
參數的解析,一開始考慮的只有GET,POST,PUT,DELETE 沒有考慮JSON和文件的解析。由於一開始忙於搭框架是一方面,其次由於我用的 schema不支持(我也沒仔細看,本身實現起來也很簡單)。
這裏就推薦兩個我經常使用的golang第三方庫,這也是我用於參數解析和校驗的工具:app
// 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 }
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)
路由到這裏也就結束了,雖然最重要,但依然比較簡單。