南京做網(wǎng)站外包省委副書記
?通過go語言原生http中響應(yīng)錯誤的實現(xiàn)方法,逐步了解和使用微服務(wù)框架 kratos 的錯誤處理方式,以及探究其實現(xiàn)原理。
一、go原生http響應(yīng)錯誤信息的處理方法
- 處理方法:
①定義返回錯誤信息的結(jié)構(gòu)體 ErrorResponse
// 定義http返回錯誤信息的結(jié)構(gòu)體
type ErrorResponse struct {Code int `json:"code"`Message string `json:"message"`
}
②根據(jù)業(yè)務(wù)邏輯,為結(jié)構(gòu)體賦值相應(yīng)的錯誤信息
//這里為了簡化函數(shù),不進(jìn)行業(yè)務(wù)邏輯判斷,而直接返回錯誤信息
er := &ErrorResponse{Code: 403,Message: "用戶名不能為空",
}
③將錯誤信息序列化,并寫入到?http.ResponseWriter 中
// 設(shè)置響應(yīng)頭為JSON類型
w.Header().Set("Content-Type", "application/json")// 設(shè)置響應(yīng)狀態(tài)碼為400
w.WriteHeader(http.StatusBadRequest)// 將ErrorResponse轉(zhuǎn)換為JSON并寫入響應(yīng)體
//json.NewEncoder(w).Encode(he)//將錯誤信息結(jié)構(gòu)體序列化,并返回
res, _ := json.Marshal(er)
w.Write(res)
- 代碼示例:
package mainimport ("encoding/json""net/http"
)// 定義http返回錯誤信息的結(jié)構(gòu)體
type ErrorResponse struct {Code int `json:"code"`Message string `json:"message"`
}func Login(w http.ResponseWriter, r *http.Request) {//這里為了簡化函數(shù),不進(jìn)行業(yè)務(wù)邏輯判斷,而直接返回錯誤信息er := &ErrorResponse{Code: 403,Message: "用戶名不能為空",}// 設(shè)置響應(yīng)頭為JSON類型w.Header().Set("Content-Type", "application/json")// 設(shè)置響應(yīng)狀態(tài)碼為400w.WriteHeader(http.StatusBadRequest)// 將ErrorResponse轉(zhuǎn)換為JSON并寫入響應(yīng)體//json.NewEncoder(w).Encode(he)//將錯誤信息結(jié)構(gòu)體序列化,并返回res, _ := json.Marshal(er)w.Write(res)
}func main() {//創(chuàng)建一個 HTTP 請求路由器mux := http.NewServeMux()mux.Handle("/login", http.HandlerFunc(Login))http.ListenAndServe(":8081", mux)
}
- 效果演示:
二、微服務(wù)框架kratos響應(yīng)錯誤的方式
Kratos官網(wǎng)有關(guān)錯誤處理的介紹:錯誤處理 | Kratos
Kratos 有關(guān)錯誤處理的 examples 代碼見:examples/errors?、examples/http/errors
1、kratos默認(rèn)的錯誤信息格式
- kratos響應(yīng)錯誤信息的默認(rèn)JSON格式為:
{// 錯誤碼,跟 http-status 一致,并且在 grpc 中可以轉(zhuǎn)換成 grpc-status"code": 500,// 錯誤原因,定義為業(yè)務(wù)判定錯誤碼"reason": "USER_NOT_FOUND",// 錯誤信息,為用戶可讀的信息,可作為用戶提示內(nèi)容"message": "invalid argument error",// 錯誤元信息,為錯誤添加附加可擴(kuò)展信息"metadata": {"foo": "bar"}
}
- 使用方法:
①導(dǎo)入 kratos 的 errors 包
import "github.com/go-kratos/kratos/v2/errors"
②在業(yè)務(wù)邏輯中需要響應(yīng)錯誤時,用 New 方法生成錯誤信息(或通過 proto 生成的代碼響應(yīng)錯誤)
注意:這里的 New 方法是 Kratos 框架中的 errros.New,而不是 go 原生的 errors.New
func NewLoginRequest(username, password string) (*LoginRequest, error) {// 校驗參數(shù)if username == "" {//通過 New 方法創(chuàng)建一個錯誤信息err := errors.New(500, "USER_NAME_EMPTY", "用戶名不能為空")// 傳遞metadataerr = err.WithMetadata(map[string]string{ "remark": "請求參數(shù)中的 phone 字段為空",})return nil, err}if password == "" {// 通過 proto 生成的代碼響應(yīng)錯誤,并且包名應(yīng)替換為自己生成代碼后的 package name return nil, api.ErrorPasswordIsEmpty("密碼不能為空")}return &LoginRequest{username: username,password: password,}, nil
}
- 返回結(jié)果:
2、自定義錯誤信息格式
如果不想使用 kratos 默認(rèn)的錯誤響應(yīng)格式,可以自定義錯誤信息處理格式,方法如下:
①自定義錯誤信息結(jié)構(gòu)體
type HTTPError struct {Code int `json:"code"`Message string `json:"message"`MoreInfo string `json:"moreInfo"`
}
②實現(xiàn)?FromError、errorEncoder 等方法
func (e *HTTPError) Error() string {return fmt.Sprintf("HTTPError code: %d message: %s", e.Code, e.Message)
}// FromError try to convert an error to *HTTPError.
func FromError(err error) *HTTPError {if err == nil {return nil}if se := new(HTTPError); errors.As(err, &se) {return se}return &HTTPError{Code: 500}
}func errorEncoder(w stdhttp.ResponseWriter, r *stdhttp.Request, err error) {se := FromError(err)codec, _ := http.CodecForRequest(r, "Accept")body, err := codec.Marshal(se)if err != nil {w.WriteHeader(500)return}w.Header().Set("Content-Type", "application/"+codec.Name())w.WriteHeader(se.Code)_, _ = w.Write(body)
}
③創(chuàng)建 http.Server 時,使用函數(shù) http.ErrorEncoder()?將上述?errorEncoder 添加到 ServerOption 中
httpSrv := http.NewServer(http.Address(":8000"),http.ErrorEncoder(errorEncoder),
)
④業(yè)務(wù)邏輯中需要響應(yīng)錯誤的地方返回自定義消息對象
return &HTTPError{Code: 400, Message: "用戶名不存在", MoreInfo: "請求參數(shù)中 userName = 張三"}
- 完整代碼示例為:
package mainimport ("errors""fmt""log"stdhttp "net/http""github.com/go-kratos/kratos/v2""github.com/go-kratos/kratos/v2/transport/http"
)// HTTPError is an HTTP error.
type HTTPError struct {Code int `json:"code"`Message string `json:"message"`MoreInfo string `json:"moreInfo"`
}func (e *HTTPError) Error() string {return fmt.Sprintf("HTTPError code: %d message: %s", e.Code, e.Message)
}// FromError try to convert an error to *HTTPError.
func FromError(err error) *HTTPError {if err == nil {return nil}if se := new(HTTPError); errors.As(err, &se) {return se}return &HTTPError{Code: 500}
}func errorEncoder(w stdhttp.ResponseWriter, r *stdhttp.Request, err error) {se := FromError(err)codec, _ := http.CodecForRequest(r, "Accept")body, err := codec.Marshal(se)if err != nil {w.WriteHeader(500)return}w.Header().Set("Content-Type", "application/"+codec.Name())w.WriteHeader(se.Code)_, _ = w.Write(body)
}func main() {httpSrv := http.NewServer(http.Address(":8082"),http.ErrorEncoder(errorEncoder),)router := httpSrv.Route("/")router.GET("login", func(ctx http.Context) error {return &HTTPError{Code: 400, Message: "用戶名不存在", MoreInfo: "請求參數(shù)中 userName = 張三"}})app := kratos.New(kratos.Name("mux"),kratos.Server(httpSrv,),)if err := app.Run(); err != nil {log.Fatal(err)}
}
- 返回結(jié)果:
3、kratos返回錯誤信息JSON的源碼探究
至此,了解了 go 原生 http 和微服務(wù)框架 kratos 響應(yīng)錯誤信息的處理方式,對比可發(fā)現(xiàn):
①在原生http響應(yīng)處理中,我們先將錯誤消息結(jié)構(gòu)體序列化 res, _ := json.Marshal(er),然后通過?http.ResponseWriter.Write(res) 寫入錯誤信息JSON并返回
②在 kratos 中,我們在業(yè)務(wù)處理函數(shù)中僅僅通過?return errors.New() 返回了一個 error,并沒有將其序列化,但整個http請求卻返回了一個有關(guān)錯誤信息的 json 字符串
是什么原因呢?原來是 kratos 框架內(nèi)部完成了將錯誤信息結(jié)構(gòu)體序列化并寫入http.ResponseWriter的過程。
具體實現(xiàn)方式如下:
- http server 結(jié)構(gòu)體 Server 中含有一個字段 ene?EncodeErrorFunc,專門用來進(jìn)行錯誤處理
//http/server.go
// Server is an HTTP server wrapper.
type Server struct {*http.Serverlis net.ListenertlsConf *tls.Configendpoint *url.URLerr errornetwork stringaddress stringtimeout time.Durationfilters []FilterFuncmiddleware matcher.MatcherdecVars DecodeRequestFuncdecQuery DecodeRequestFuncdecBody DecodeRequestFuncenc EncodeResponseFuncene EncodeErrorFunc // 用于錯誤處理strictSlash boolrouter *mux.Router
}//http/codec.go
// EncodeErrorFunc is encode error func.
type EncodeErrorFunc func(http.ResponseWriter, *http.Request, error)
- 使用 NewServer() 創(chuàng)建時 http Server 時,ene 屬性會默認(rèn)為?DefaultErrorEncoder,該函數(shù)會將序列化錯誤信息,并寫入到?http.ResponseWriter 中
//http/server.go
// NewServer creates an HTTP server by options.
func NewServer(opts ...ServerOption) *Server {srv := &Server{network: "tcp",address: ":0",timeout: 1 * time.Second,middleware: matcher.New(),decVars: DefaultRequestVars,decQuery: DefaultRequestQuery,decBody: DefaultRequestDecoder,enc: DefaultResponseEncoder,ene: DefaultErrorEncoder, //默認(rèn)的錯誤處理函數(shù)strictSlash: true,router: mux.NewRouter(),}for _, o := range opts {o(srv)}srv.router.StrictSlash(srv.strictSlash)srv.router.NotFoundHandler = http.DefaultServeMuxsrv.router.MethodNotAllowedHandler = http.DefaultServeMuxsrv.router.Use(srv.filter())srv.Server = &http.Server{Handler: FilterChain(srv.filters...)(srv.router),TLSConfig: srv.tlsConf,}return srv
}//http/codec.go
// DefaultErrorEncoder encodes the error to the HTTP response.
func DefaultErrorEncoder(w http.ResponseWriter, r *http.Request, err error) {se := errors.FromError(err)codec, _ := CodecForRequest(r, "Accept")body, err := codec.Marshal(se) //序列化錯誤信息結(jié)構(gòu)體if err != nil {w.WriteHeader(http.StatusInternalServerError)return}w.Header().Set("Content-Type", httputil.ContentType(codec.Name()))w.WriteHeader(int(se.Code))_, _ = w.Write(body) //將序列化的結(jié)果寫入到響應(yīng)中
}
- 路由處理函數(shù) router.GET() 等會調(diào)用 Router.Handle,?其中會判斷 HandlerFunc 是否有返回錯誤,如果有,則會調(diào)用 server.ene 函數(shù),從而完成錯誤信息序列化并返回
//main.go
func main() {httpSrv := http.NewServer(http.Address(":8082"),)router := httpSrv.Route("/")router.GET("login", func(ctx http.Context) error {return errors.New(500, "USER_NOT_FOUND", "用戶名不存在")})
}//http/router.go
// GET registers a new GET route for a path with matching handler in the router.
func (r *Router) GET(path string, h HandlerFunc, m ...FilterFunc) {r.Handle(http.MethodGet, path, h, m...)
}//http/router.go
// Handle registers a new route with a matcher for the URL path and method.
func (r *Router) Handle(method, relativePath string, h HandlerFunc, filters ...FilterFunc) {next := http.Handler(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) {ctx := r.pool.Get().(Context)ctx.Reset(res, req)//重點:這里判斷路由處理函數(shù)是否返回了 error,如果是,則調(diào)用 server.ene 函數(shù),序列化錯誤信息并返回if err := h(ctx); err != nil {r.srv.ene(res, req, err)}ctx.Reset(nil, nil)r.pool.Put(ctx)}))next = FilterChain(filters...)(next)next = FilterChain(r.filters...)(next)r.srv.router.Handle(path.Join(r.prefix, relativePath), next).Methods(method)
}
- 自定義錯誤消息結(jié)構(gòu)體后,創(chuàng)建 http server 時,通過 http.ErrorEncoder(errorEncoder) 將自定義的錯誤處理函數(shù)賦值給 server.ene,替換了默認(rèn)的?DefaultErrorEncoder
// main.go
//自定義的錯誤處理函數(shù)
func errorEncoder(w stdhttp.ResponseWriter, r *stdhttp.Request, err error) {se := FromError(err)codec, _ := http.CodecForRequest(r, "Accept")body, err := codec.Marshal(se)if err != nil {w.WriteHeader(500)return}w.Header().Set("Content-Type", "application/"+codec.Name())w.WriteHeader(se.Code)_, _ = w.Write(body)
}// main.go
func main() {httpSrv := http.NewServer(http.Address(":8082"),http.ErrorEncoder(errorEncoder), // 將自定義的錯誤處理函數(shù)賦值給 sever)
}// http/server.go
// ErrorEncoder with error encoder.
func ErrorEncoder(en EncodeErrorFunc) ServerOption {return func(o *Server) {o.ene = en}
}