這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
代碼由此去
代碼結構
.- 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 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
才會解析請求中的檔案。因此在寫商務邏輯的時候,要寫成這個樣子了,這些樣本都在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)
路由到這裡也就結束了,雖然最重要,但依然比較簡單。
最後可能需要一個圖來說明?