zero路由与gin的区别
官网go-zero
go-zero是一个集成了各种工程实践的微服务框架,集多种功能于一体,如服务主要的API服务,RPC服务等。除了构建微服务工程外,zero也是一款性能优良的web框架,也可以构建单体web应用。
更多移步www.w3cschool.cn/go-zero。
go的web框架是很多的,例如github较为流行的有:
- Gin Go语言编写的HTTP Web框架,它以更好的性能实现了类似Martini的API,性能更好。
- Beego 面向Go编程语言的开源高性能web框架。
- Iris最快的Go语言Web框架,完备MVC支持
- Echo 高性能、极简Go语言Web框架。
这些的框架的路由方式都极具相似,一般步骤都为通过工具库提供的方法构建web引擎,配置路由,启动web服务器,配置监听端口等。如下:
//gin框架
r := gin.Default()
_ = r.Run()
// 或者启动原生服务
manners.ListenAndServe(":8888", r)
//gin框架配置路由
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
//路由组和其他路由方式
//通过路由将处理函数配置
gin框架中路由的处理函数是不限个数,从而实现了中间件的功能,另外
gin.Context
是路由的上下文连接,在不同路由路径下会自动解析该路径下的请求参数。
//iris服务
app := iris.New()
app.Run(iris.Addr(":8080"))
// 或者自定义链接方式与端口号
l, err := listenerCfg.NewListener("tcp", ":8080")
if err != nil {
app.Logger().Fatal(err)
}
app.Run(iris.Listener(l))
// 或者启动原生服务
app.Run(iris.Raw(&http.Server{Addr:":8080"}).ListenAndServe)
//iris路由
app.Get("/", func(ctx iris.Context) {
ctx.HTML("<h1> Hello from /contact </h1>")
})
这些框架路由原理很相似通过上下文连web容器,每个控制器独立解析该路由路径下的参数。
在实例项目中,路由需要分离出来,处理逻辑也是,对于这样原理的web框架,同一系列的路由路径下由一个接口实现或者类成员方法实现(所有方法分散不利于代码维护)。那么该实现方式就类似Java的实现方式。
如图所示/a1,/a2…都是/a系的,对应A类,分发到对应的路由就通过A类调用对应的方法即可。
//main
import (
"github.com/gin-gonic/gin"
"backend/controller"
)
func main() {
//创建一个服务器引擎
engine := gin.Default()
//控制器传参
c := new(controller.IndexController)
c.RegisterRoute(engine)
//配置服务器端口
engine.Run("127.0.0.1:8080")
}
// controller
type IndexController struct{}
func (self IndexController) RegisterRoute(g *gin.Engine) {
g.GET("/user", self.getUer)
g.GET("/banner")
}
func (IndexController) getUer(c *gin.Context) {
lo := logic.IndexLogic{}
notice := lo.GetUser()
fmt.Println(notice)
}
gin路由分发案例
不同gin等路由分发的方式go-zero自成一派,具体请看go-zero的路由机制解析。zero是使用路由注册的方式,如下图所示:
编写该路由下的逻辑处理函数通过通过服务器引擎提供的方法将处理逻辑注册到对应路由中。这种方法的耦合度更低,显然这也更贴合go语言的特性。
//zero web服务
import (
"fmt"
"demo/apiservice/internal/config"
"demo/apiservice/internal/handler"
"demo/apiservice/internal/svc"
"github.com/zeromicro/go-zero/rest"
)
func main() {
var c config.Config
c.Host = "0.0.0.0"
c.Port = 8000
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
// head
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
// boot
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}
在上述的代码中head到boot部分是服务器配置的不是实现web服务器实例的主要方法,该部分主要是配置第三方框架时需要,如日志文件等。核心代码也就,如下
//c.RestConf服务器配置(略)
server := rest.MustNewServer(c.RestConf)
server.Start()
路由如下:
import (
"net/http"
"demo/apiservice/internal/svc"
"github.com/zeromicro/go-zero/rest"
)
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodGet,
Path: "/from/:name",
Handler: ApiserviceHandler(serverCtx),
},
//自定义路由
{
Method: http.MethodPost,
Path: "/create",
Handler: OrderCreateController(),
},
},
)
}
通过注册的方式直接将路由与函数绑定,完全不需要结构体方法的继承关系,直接以函数作为第一操作单位。
路由处理函数
zero的路由处理函数满足的条件是返回类型为http.HandlerFunc
的函数。细心的小伙伴可以就发现了这不是gin的路由处理函数的返回类型吗?实际上zero中HandlerFunc
是基于Go原生的http库,而gin框架中该类型是对http.HandlerFunc的进一步封装。
http.HandlerFunc
是一个参数为ResponseWriter
,*Request
的函数,学过Java的是不是秒懂了,在Java的Servlet中也存在HttpRquest
和HttpReponse
,且该函数是请求报文与响应报文的封装对象,通过该对象可以直接获取请求参数和设置响应参数。
在zero中也是同样的原理,对于这样个参数如何获取获取请求参数和设置响应参数就不做过多介绍,有兴趣的看看源码。zero封装了httpx
对这两个对象实现了解析,通过httpx库会更加方法的使用这两个参数。
只要满足返回类型为
http.HandlerFunc
就是zero的逻辑处理函数。
zero获取请求参数
前一节说到http.HandlerFunc
类型的函数为zero的逻辑处理函数,而该类型又是参数必须是func(w http.ResponseWriter, r *http.Request)
的函数。zero封装了httpx
库提供了更加方便简洁方式使用这两个参数。
官方教程:github.com/zeromicro/go-zero/rest/httpx
httpx
库内容不多,看源码很容器看懂,就是四种请求参数的解析。
package httpx
import (
"io"
"net/http"
"strings"
"sync/atomic"
"github.com/zeromicro/go-zero/core/mapping"
"github.com/zeromicro/go-zero/core/validation"
"github.com/zeromicro/go-zero/rest/internal/encoding"
"github.com/zeromicro/go-zero/rest/internal/header"
"github.com/zeromicro/go-zero/rest/pathvar"
)
const (
formKey = "form"
pathKey = "path"
maxMemory = 32 << 20 // 32MB
maxBodyLen = 8 << 20 // 8MB
separator = ";"
tokensInAttribute = 2
)
var (
formUnmarshaler = mapping.NewUnmarshaler(formKey, mapping.WithStringValues())
pathUnmarshaler = mapping.NewUnmarshaler(pathKey, mapping.WithStringValues())
validator atomic.Value
)
// Validator defines the interface for validating the request.
type Validator interface {
// Validate validates the request and parsed data.
Validate(r *http.Request, data any) error
}
// Parse parses the request.
func Parse(r *http.Request, v any) error {
if err := ParsePath(r, v); err != nil {
return err
}
if err := ParseForm(r, v); err != nil {
return err
}
if err := ParseHeaders(r, v); err != nil {
return err
}
if err := ParseJsonBody(r, v); err != nil {
return err
}
if valid, ok := v.(validation.Validator); ok {
return valid.Validate()
} else if val := validator.Load(); val != nil {
return val.(Validator).Validate(r, v)
}
return nil
}
// ParseHeaders parses the headers request.
func ParseHeaders(r *http.Request, v any) error {
return encoding.ParseHeaders(r.Header, v)
}
// ParseForm parses the form request.
func ParseForm(r *http.Request, v any) error {
params, err := GetFormValues(r)
if err != nil {
return err
}
return formUnmarshaler.Unmarshal(params, v)
}
// ParseHeader parses the request header and returns a map.
func ParseHeader(headerValue string) map[string]string {
ret := make(map[string]string)
fields := strings.Split(headerValue, separator)
for _, field := range fields {
field = strings.TrimSpace(field)
if len(field) == 0 {
continue
}
kv := strings.SplitN(field, "=", tokensInAttribute)
if len(kv) != tokensInAttribute {
continue
}
ret[kv[0]] = kv[1]
}
return ret
}
// ParseJsonBody parses the post request which contains json in body.
func ParseJsonBody(r *http.Request, v any) error {
if withJsonBody(r) {
reader := io.LimitReader(r.Body, maxBodyLen)
return mapping.UnmarshalJsonReader(reader, v)
}
return mapping.UnmarshalJsonMap(nil, v)
}
// ParsePath parses the symbols reside in url path.
// Like http://localhost/bag/:name
func ParsePath(r *http.Request, v any) error {
vars := pathvar.Vars(r)
m := make(map[string]any, len(vars))
for k, v := range vars {
m[k] = v
}
return pathUnmarshaler.Unmarshal(m, v)
}
// SetValidator sets the validator.
// The validator is used to validate the request, only called in Parse,
// not in ParseHeaders, ParseForm, ParseHeader, ParseJsonBody, ParsePath.
func SetValidator(val Validator) {
validator.Store(val)
}
func withJsonBody(r *http.Request) bool {
return r.ContentLength > 0 && strings.Contains(r.Header.Get(header.ContentType), header.ApplicationJson)
}
xxx?a=xxx?&b=xxx
类型
该类型可以分为超链接和表单,对应的方法为httpx.ParseForm
。
/xxx/:a/:b
类型参数
对应方法为httpx.ParsePath
。
- 请求头参数
对应方法为http.ParseHeaders
。
4.请求体参数
对应方法为httpx.ParseJsonBody
。
httpx
为zero库下的github.com/zeromicro/go-zero/rest/httpx
。
另外还有httpx.Parse
方法,能够自动解析,对应参数,源码如下:
// Parse parses the request.
func Parse(r *http.Request, v any) error {
if err := ParsePath(r, v); err != nil {
return err
}
if err := ParseForm(r, v); err != nil {
return err
}
if err := ParseHeaders(r, v); err != nil {
return err
}
if err := ParseJsonBody(r, v); err != nil {
return err
}
if valid, ok := v.(validation.Validator); ok {
return valid.Validate()
} else if val := validator.Load(); val != nil {
return val.(Validator).Validate(r, v)
}
return nil
}
zero设置响应参数
由http.HandlerFunc
函数可知,路由处理函数是一个字符流,用于写入响应数据。响应数据就相对复杂了,应为响应数据一般要包含响应码,而响应吗数量非常多从100~600之间都有响应码,因此返回携带准确响应码的就十分棘手。
HTTP
一般情况下大多数框架响应码都是开发者定义的。响应分为五类:信息响应(100–199),成功响应(200–299),重定向(300–399),客户端错误(400–499)和服务器错误 (500–599)。
最常用的响应码有:
- 200 - 请求成功
- 301 - 资源(网页等)被永久转移到其它URL
- 400 - 客户端请求的语法错误,服务器无法理解
- 403 - 服务器理解请求客户端的请求,但是拒绝执行此请求
- 404 - 请求的资源(网页等)不存在
- 500 - 内部服务器错误
- 502 - 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应
在httpx
库中只有三种种响应状态,成功、失败和自定义。前者是以Ok
为前缀的方法,后者是以Error
为前缀的方法。
// Error writes err into w.
func Error(w http.ResponseWriter, err error, fns ...func(w http.ResponseWriter, err error)) {
lock.RLock()
handler := errorHandler
lock.RUnlock()
doHandleError(w, err, handler, WriteJson, fns...)
}
// ErrorCtx writes err into w.
func ErrorCtx(ctx context.Context, w http.ResponseWriter, err error,
fns ...func(w http.ResponseWriter, err error)) {
lock.RLock()
handlerCtx := errorHandlerCtx
lock.RUnlock()
var handler func(error) (int, any)
if handlerCtx != nil {
handler = func(err error) (int, any) {
return handlerCtx(ctx, err)
}
}
writeJson := func(w http.ResponseWriter, code int, v any) {
WriteJsonCtx(ctx, w, code, v)
}
doHandleError(w, err, handler, writeJson, fns...)
}
// Ok writes HTTP 200 OK into w.
func Ok(w http.ResponseWriter) {
w.WriteHeader(http.StatusOK)
}
// OkJson writes v into w with 200 OK.
func OkJson(w http.ResponseWriter, v any) {
WriteJson(w, http.StatusOK, v)
}
// OkJsonCtx writes v into w with 200 OK.
func OkJsonCtx(ctx context.Context, w http.ResponseWriter, v any) {
WriteJsonCtx(ctx, w, http.StatusOK, v)
}
当响应数据需求不高可以直接使用这两个方法,当需要定制化响应参数时就需要使用自定义的返回响应报文了。
// WriteJson writes v as json string into w with code.
func WriteJson(w http.ResponseWriter, code int, v any) {
if err := doWriteJson(w, code, v); err != nil {
logx.Error(err)
}
}
// WriteJsonCtx writes v as json string into w with code.
func WriteJsonCtx(ctx context.Context, w http.ResponseWriter, code int, v any) {
if err := doWriteJson(w, code, v); err != nil {
logx.WithContext(ctx).Error(err)
}
}
WriteJson
和WriteJsonCtx
可以定义返回状态码即code参数。前者不带配置项,后者携带配置项。v
为响应体数据,为任意类型。由函数名可以看出返回类型为json字符串,由于http时字符流传输,json字符串是前后端传输的最轻量级的数据传输格式。
v
是任意类型,也就是说,任何类型传参后都由框架自动序列化为json字符串。
// create
func OrderCreateController() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
//获取请求参数
var req models.Order
err := httpx.ParseJsonBody(r, &req)
if err != nil {
//fmt.Printf("ordercontoller err:%v", err)
httpx.WriteJson(w, 500, fmt.Sprintf("ordercontoller err:%v", err))
return
}
err = orderlogic.OrderLogic.Create(req)
if err != nil {
//fmt.Printf("order create err:%v", err)
httpx.WriteJson(w, 500, fmt.Sprintf("order create err:%v", err))
return
}
httpx.OkJson(w, map[string]string{"code": "200", "message": "插入成功!"})
}
}