通过go语言原生http中响应错误的实现方法,逐步了解和使用微服务框架 kratos 的错误处理方式,以及探究其实现原理。
一、go原生http响应错误信息的处理方法
- 处理方法:
①定义返回错误信息的结构体 ErrorResponse
// 定义http返回错误信息的结构体
type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
②根据业务逻辑,为结构体赋值相应的错误信息
//这里为了简化函数,不进行业务逻辑判断,而直接返回错误信息
er := &ErrorResponse{
Code: 403,
Message: "用户名不能为空",
}
③将错误信息序列化,并写入到 http.ResponseWriter 中
// 设置响应头为JSON类型
w.Header().Set("Content-Type", "application/json")
// 设置响应状态码为400
w.WriteHeader(http.StatusBadRequest)
// 将ErrorResponse转换为JSON并写入响应体
//json.NewEncoder(w).Encode(he)
//将错误信息结构体序列化,并返回
res, _ := json.Marshal(er)
w.Write(res)
- 代码示例:
package main
import (
"encoding/json"
"net/http"
)
// 定义http返回错误信息的结构体
type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
func Login(w http.ResponseWriter, r *http.Request) {
//这里为了简化函数,不进行业务逻辑判断,而直接返回错误信息
er := &ErrorResponse{
Code: 403,
Message: "用户名不能为空",
}
// 设置响应头为JSON类型
w.Header().Set("Content-Type", "application/json")
// 设置响应状态码为400
w.WriteHeader(http.StatusBadRequest)
// 将ErrorResponse转换为JSON并写入响应体
//json.NewEncoder(w).Encode(he)
//将错误信息结构体序列化,并返回
res, _ := json.Marshal(er)
w.Write(res)
}
func main() {
//创建一个 HTTP 请求路由器
mux := http.NewServeMux()
mux.Handle("/login", http.HandlerFunc(Login))
http.ListenAndServe(":8081", mux)
}
- 效果演示:
二、微服务框架kratos响应错误的方式
Kratos官网有关错误处理的介绍:错误处理 | Kratos
Kratos 有关错误处理的 examples 代码见:examples/errors 、examples/http/errors
1、kratos默认的错误信息格式
- kratos响应错误信息的默认JSON格式为:
{
// 错误码,跟 http-status 一致,并且在 grpc 中可以转换成 grpc-status
"code": 500,
// 错误原因,定义为业务判定错误码
"reason": "USER_NOT_FOUND",
// 错误信息,为用户可读的信息,可作为用户提示内容
"message": "invalid argument error",
// 错误元信息,为错误添加附加可扩展信息
"metadata": {
"foo": "bar"
}
}
- 使用方法:
①导入 kratos 的 errors 包
import "github.com/go-kratos/kratos/v2/errors"
②在业务逻辑中需要响应错误时,用 New 方法生成错误信息(或通过 proto 生成的代码响应错误)
注意:这里的 New 方法是 Kratos 框架中的 errros.New,而不是 go 原生的 errors.New
func NewLoginRequest(username, password string) (*LoginRequest, error) {
// 校验参数
if username == "" {
//通过 New 方法创建一个错误信息
err := errors.New(500, "USER_NAME_EMPTY", "用户名不能为空")
// 传递metadata
err = err.WithMetadata(map[string]string{
"remark": "请求参数中的 phone 字段为空",
})
return nil, err
}
if password == "" {
// 通过 proto 生成的代码响应错误,并且包名应替换为自己生成代码后的 package name
return nil, api.ErrorPasswordIsEmpty("密码不能为空")
}
return &LoginRequest{
username: username,
password: password,
}, nil
}
- 返回结果:
2、自定义错误信息格式
如果不想使用 kratos 默认的错误响应格式,可以自定义错误信息处理格式,方法如下:
①自定义错误信息结构体
type HTTPError struct {
Code int `json:"code"`
Message string `json:"message"`
MoreInfo string `json:"moreInfo"`
}
②实现 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)
}
③创建 http.Server 时,使用函数 http.ErrorEncoder() 将上述 errorEncoder 添加到 ServerOption 中
httpSrv := http.NewServer(
http.Address(":8000"),
http.ErrorEncoder(errorEncoder),
)
④业务逻辑中需要响应错误的地方返回自定义消息对象
return &HTTPError{Code: 400, Message: "用户名不存在", MoreInfo: "请求参数中 userName = 张三"}
- 完整代码示例为:
package main
import (
"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: "请求参数中 userName = 张三"}
})
app := kratos.New(
kratos.Name("mux"),
kratos.Server(
httpSrv,
),
)
if err := app.Run(); err != nil {
log.Fatal(err)
}
}
- 返回结果:
3、kratos返回错误信息JSON的源码探究
至此,了解了 go 原生 http 和微服务框架 kratos 响应错误信息的处理方式,对比可发现:
①在原生http响应处理中,我们先将错误消息结构体序列化 res, _ := json.Marshal(er),然后通过 http.ResponseWriter.Write(res) 写入错误信息JSON并返回
②在 kratos 中,我们在业务处理函数中仅仅通过 return errors.New() 返回了一个 error,并没有将其序列化,但整个http请求却返回了一个有关错误信息的 json 字符串
是什么原因呢?原来是 kratos 框架内部完成了将错误信息结构体序列化并写入http.ResponseWriter的过程。
具体实现方式如下:
- http server 结构体 Server 中含有一个字段 ene EncodeErrorFunc,专门用来进行错误处理
//http/server.go
// Server is an HTTP server wrapper.
type Server struct {
*http.Server
lis net.Listener
tlsConf *tls.Config
endpoint *url.URL
err error
network string
address string
timeout time.Duration
filters []FilterFunc
middleware matcher.Matcher
decVars DecodeRequestFunc
decQuery DecodeRequestFunc
decBody DecodeRequestFunc
enc EncodeResponseFunc
ene EncodeErrorFunc // 用于错误处理
strictSlash bool
router *mux.Router
}
//http/codec.go
// EncodeErrorFunc is encode error func.
type EncodeErrorFunc func(http.ResponseWriter, *http.Request, error)
- 使用 NewServer() 创建时 http Server 时,ene 属性会默认为 DefaultErrorEncoder,该函数会将序列化错误信息,并写入到 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, //默认的错误处理函数
strictSlash: true,
router: mux.NewRouter(),
}
for _, o := range opts {
o(srv)
}
srv.router.StrictSlash(srv.strictSlash)
srv.router.NotFoundHandler = http.DefaultServeMux
srv.router.MethodNotAllowedHandler = http.DefaultServeMux
srv.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) //序列化错误信息结构体
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) //将序列化的结果写入到响应中
}
- 路由处理函数 router.GET() 等会调用 Router.Handle, 其中会判断 HandlerFunc 是否有返回错误,如果有,则会调用 server.ene 函数,从而完成错误信息序列化并返回
//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)
//重点:这里判断路由处理函数是否返回了 error,如果是,则调用 server.ene 函数,序列化错误信息并返回
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)
}
- 自定义错误消息结构体后,创建 http server 时,通过 http.ErrorEncoder(errorEncoder) 将自定义的错误处理函数赋值给 server.ene,替换了默认的 DefaultErrorEncoder
// main.go
//自定义的错误处理函数
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), // 将自定义的错误处理函数赋值给 sever
)
}
// http/server.go
// ErrorEncoder with error encoder.
func ErrorEncoder(en EncodeErrorFunc) ServerOption {
return func(o *Server) {
o.ene = en
}
}