go 进阶 go-zero相关: 二. 服务启动与路由,中间件注册,请求接收底层原理

2023-11-13

一. 问题概述

  1. 了解go-zero底层也是基于net/http标准库实现http的,是怎么实现的,怎么触发到net/http的
  2. go-zero也是基于前缀树进行路由注册的,是怎么注册的,注册过程中有哪些注意点
  3. go-zero中支持中间件, 在服务启动时,中间件,路由是如何保存的,接收请求时是如何执行的
  4. 先看一下基础go-zero服务示例
package main

import (
	"fmt"
	"github.com/zeromicro/go-zero/rest/chain"
	"github.com/zeromicro/go-zero/rest/httpx"
	"github.com/zeromicro/go-zero/core/logx"
	"github.com/zeromicro/go-zero/core/service"
	"github.com/zeromicro/go-zero/rest"
	"log"
	"net/http"
)

func main() {
	//1.创建服务句柄
	//此处也可以替换为通过conf/MustLoad()加载yaml,通过rest/MustNewServer()创建服务
	srv, err := rest.NewServer(rest.RestConf{
		Port: 8080, // 侦听端口
		ServiceConf: service.ServiceConf{
			Log: logx.LogConf{Path: "./logs"}, // 日志路径
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	defer srv.Stop()

	//2.使用server上的Use()方法添加全局中间件
	srv.Use(func(next http.HandlerFunc) http.HandlerFunc {
		return func(w http.ResponseWriter, r *http.Request) {
			// 在请求处理前执行一些逻辑
			fmt.Println("before request")
			// 调用下一个处理函数
			next(w, r)
			// 在请求处理后执行一些逻辑
			fmt.Println("after request")
		}
	})

	//2.注册路由
	srv.AddRoutes([]rest.Route{
		{
			Method:  http.MethodGet,
			Path:    "/user/info",
			Handler: userInfo,
		},
	})

	//===================下方是一些路由分组,中间件注册,拦截器注册的示例,不是真实代码会报错,使用时直接删除即可===============================
	//3.路由分组示例只是示例会报错
	srv.AddRoutes(
		[]rest.Route{
			{
				Method:  http.MethodPost,
				Path:    "/afsdfa",
				Handler: thirdPayment.ThirdPaymentWxPayCallbackHandler(serverCtx),
			},
		},
		//为一组路由开启jwt验证功能,并指定密钥
		rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret),
		//为一组路由添加一个公共的前缀
		rest.WithPrefix("/afas/v1"),
	)

	srv.AddRoutes(
		rest.WithMiddlewares(
			[]rest.Middleware{
				func(next http.HandlerFunc) http.HandlerFunc {
					return func(w http.ResponseWriter, r *http.Request) {
					}
				},
				func(next http.HandlerFunc) http.HandlerFunc {
					return func(w http.ResponseWriter, r *http.Request) {
					}
				},
				//ToMiddleware()用于将一个接收和返回http.Handler的函数转换为一个Middleware类型的函数
				rest.ToMiddleware(func(next http.Handler) http.Handler {
					return next
				}),
			},
			rest.Route{
				Method:  http.MethodGet,
				Path:    "/user/info2",
				Handler: userInfo,
			},
			rest.Route{
				Method:  http.MethodGet,
				Path:    "/user/info3",
				Handler: userInfo,
			}),
		rest.WithPrefix("/afas/v2"),
	)
	//====================================================================

	srv.Start() // 启动服务
}

type User struct {
	Name  string `json:"name"`
	Addr  string `json:"addr"`
	Level int    `json:"level"`
}

func userInfo(w http.ResponseWriter, r *http.Request) {
	var req struct {
		UserId int64 `form:"user_id"` // 定义参数
	}
	if err := httpx.Parse(r, &req); err != nil { // 解析参数
		httpx.Error(w, err)
		return
	}
	users := map[int64]*User{
		1: &User{"go-zero", "shanghai", 1},
		2: &User{"go-queue", "beijing", 2},
	}
	httpx.WriteJson(w, http.StatusOK, users[req.UserId]) // 返回结果
}

二. 底层源码分析

涉及到的一些结构体简介

  1. 了解go-zero服务的启动与路由注册,首先要了解几个结构体比如: engine,patRouter,featuredRoutes,Route
  2. engine: 服务引擎,构建go-zero服务时首先要创建这个引擎
  1. 服务其中时会将服务的配置相关信息存储到engine的conf 属性中
  2. 在路由注册时会先将路由保存到engine的routes属性中,后续再通过这个属性获取所有路由构建前缀树
  3. 会将通过Server的Use()或rest下WithMiddleware()/WithMiddlewares()函数主动注册中间件存储到middlewares属性中
  4. 在后续处理时会获取middlewares属性中保存的中间件,engine的chain处理器链中
type engine struct {
	//RestConf结构体变量,内部存储了服务需要的配置信息,,比如监听地址,超时时间,鉴权等
	conf   RestConf
	//一个featuredRoutes切片,用于存储rest服务的路由信息,每个路由包含了请求方法,路径,处理函数和特征值
	routes []featuredRoutes
	//表示rest服务的最大超时时间,它是根据所有路由的超时时间计算得到的
	timeout              time.Duration
	//用于处理未授权的请求,比如返回401状态码或者重定向到登录页面
	unauthorizedCallback handler.UnauthorizedCallback
	//用于处理未签名的请求,比如返回403状态码或者提示用户签名
	unsignedCallback     handler.UnsignedCallback
	//用于管理rest服务的中间件链,可以在请求处理前后执行一些逻辑,比如日志、监控、限流等
	chain                chain.Chain
	//用于存储rest服务的中间件
	middlewares          []Middleware
	//用于实现自适应限流功能,根据CPU负载和请求数动态调整限流阈值
	shedder              load.Shedder
	//用于实现优先级限流功能,根据请求的优先级和请求数动态调整限流阈值
	priorityShedder      load.Shedder
	//用于配置rest服务的TLS加密通信参数,比如证书、密钥等
	tlsConfig            *tls.Config
}
  1. patRouter: 通过NewServer()函数创建go-zero服务时内部会调用NewRouter()先初始化这个结构体变量,初始化内部的trees属性,这个trees属性就是多个以路由method为key的前缀树,patRouter是路由注册与请求执行的核心,该结构体实现了ServeHTTP()方法,会通过这个方法处理用户的请求
type patRouter struct {
	//键是请求方法,值是一个search.Tree类型的对象
	trees      map[string]*search.Tree
	//用于处理未找到匹配路由的请求,比如返回404状态码或者自定义的错误页面
	notFound   http.Handler
	//用于处理不允许的请求方法,比如返回405状态码或者自定义的错误页面
	notAllowed http.Handler
}

type Tree struct {
	root *node
}

type node struct {
	item     any
	children [2]map[string]*node
}
  1. featuredRoutes
type featuredRoutes struct {
	//表示这组路由的超时时间,如果请求处理超过这个时间,会返回超时错误
	timeout   time.Duration
	//表示这组路由是否具有优先级,如果为true,这组路由会使用优先级限流器进行限流,否则使用普通限流器
	priority  bool
	//包含了jwt验证相关的配置信息,比如是否开启jwt验证、密钥等
	jwt       jwtSetting
	//包含了签名验证相关的配置信息,比如是否开启签名验证、签名算法等
	signature signatureSetting
	//用于存储这组路由的具体信息,每个Route包含了请求方法、路径和处理函数
	routes    []Route
	//表示这组路由允许的最大请求体大小,如果请求体超过这个大小,会返回错误
	maxBytes  int64
}
  1. Route
type Route struct {
	//请求方法,比如GET、POST、PUT等
	Method  string
	//请求路径,可以包含模式匹配的参数,比如/user/:id
	Path    string
	//处理请求的函数,用于处理匹配到该路由的请求,并返回响应
	Handler http.HandlerFunc
}

初始化

  1. 在编写go-zero服务时,可以编写yaml,通过conf/MustLoad()读取yaml配置,通过rest/MustNewServer()创建服务,也可以直接封装rest.RestConf配置变量,调用rest/NewServer()创建服务(实际MustLoad()内部也会调用这个NewServer()),查看NewServer()
  1. 首先调用调用newEngine()创建Engine服务引擎
  2. 调用NewRouter()函数,初始化patRouter,初始化patRouter中的trees路由前缀树
func NewServer(c RestConf, opts ...RunOption) (*Server, error) {
	if err := c.SetUp(); err != nil {
		return nil, err
	}

	server := &Server{
		ngin:   newEngine(c),
		router: router.NewRouter(),
	}

	opts = append([]RunOption{WithNotFoundHandler(nil)}, opts...)
	for _, opt := range opts {
		opt(server)
	}

	return server, nil
}

中间件的预设置

  1. 有两种方式注册中间件:
  1. 通过Server的use方法注册全局中间件,
  2. 通过github.com\zeromicro\go-zero\rest\server.go中的WithMiddlewares/WithMiddleware()函数注册针对一组路由的中间件
  1. Server.Use()注册全局中间件,很简单,会调用engine的use()方法,将中间件函数保存到engine引擎的中间件切片属性middlewares中
func (s *Server) Use(middleware Middleware) {
	//调用engine的user()方法
	s.ngin.use(middleware)
}

func (ng *engine) use(middleware Middleware) {
	ng.middlewares = append(ng.middlewares, middleware)
}
  1. 查看WithMiddlewares/WithMiddleware()函数针对一组路由注册中间件函数源码,最终会将中间件封装到每个路由的Handler属性中
//WithMiddlewares内部实际就是调用的WithMiddleware()只关注这一个即可
func WithMiddleware(middleware Middleware, rs ...Route) []Route {
	routes := make([]Route, len(rs))
	for i := range rs {
		route := rs[i]
		routes[i] = Route{
			Method:  route.Method,
			Path:    route.Path,
			Handler: middleware(route.Handler),
		}
	}
	return routes
}
  1. 另外rest下有一个WithChain()函数,直接将中间件添加到chain处理器链中(可以看为将中间件又封装了一层),如下,在创建Server时添加两个拦截器
server := MustNewServer(RestConf{}, rest.WithChain(chain.New(中间件函数1, 中间件函数2)))

路由注册与中间件的处理

  1. 在通过go-zero提供服务时,首先执行rest下的NewServer()函数,读取配置,创建服务端Server, 提供对外的接口时,需要将接口Method,接口函数,接口路径封装为Route结构体变量,调用Server的AddRoutes()方法将这个结构体变量添加到engine引擎的routes路由切片属性中
func (s *Server) AddRoutes(rs []Route, opts ...RouteOption) {
	r := featuredRoutes{
		routes: rs,
	}
	for _, opt := range opts {
		opt(&r)
	}
	s.ngin.addRoutes(r)
}

func (ng *engine) addRoutes(r featuredRoutes) {
	ng.routes = append(ng.routes, r)
}
  1. 路由添加完成后,会调用Server的Start()方法启动服务,查看源码.会调用engine的start()方法–>调用bindRoutes()
func (s *Server) Start() {
	handleError(s.ngin.start(s.router))
}

// 定义一个 start 方法,接收一个 router 参数和可变数量的 StartOption 参数,返回一个 error 类型的值
func (ng *engine) start(router httpx.Router, opts ...StartOption) error {
	// 调用 bindRoutes 方法,将路由绑定到 ng 上,如果出错则返回错误
	if err := ng.bindRoutes(router); err != nil {
		return err
	}

	// 将 ng.withTimeout 方法作为一个 StartOption 参数添加到 opts 切片的开头
	opts = append([]StartOption{ng.withTimeout()}, opts...)

	// 如果配置文件中没有证书文件和密钥文件,则调用 internal 包中的 StartHttp 方法,启动一个 http 服务
	if len(ng.conf.CertFile) == 0 && len(ng.conf.KeyFile) == 0 {
		return internal.StartHttp(ng.conf.Host, ng.conf.Port, router, opts...)
	}

	// 如果配置文件中有证书文件和密钥文件,则创建一个匿名函数,将 ng 的 tlsConfig 属性赋值给 svr 的 TLSConfig 属性,然后将这个匿名函数作为一个 StartOption 参数添加到 opts 切片的末尾
	opts = append([]StartOption{
		func(svr *http.Server) {
			if ng.tlsConfig != nil {
				svr.TLSConfig = ng.tlsConfig
			}
		},
	}, opts...)

	// 调用 internal 包中的 StartHttps 方法,启动一个 https 服务
	return internal.StartHttps(ng.conf.Host, ng.conf.Port, ng.conf.CertFile,
		ng.conf.KeyFile, router, opts...)
}
  1. 查看engine的bindRoutes()方法,获取engine的routes属性中保存的每一个路由,遍历调用bindFeaturedRoutes()–>调用bindRoute()方法在该方法中重点完成了:
  1. 获取engine的chain属性,如果为空调用buildChainWithNativeMiddlewares()新建一个chain链,获取配置信息,根据配置判断添加中间件,比如判断是否配置了追踪请求的Trace中间件, 记录请求日志的Log中间件,收集请求指标数据Prometheus中间件,限制最大并发连接数的MaxConns中间件,实现熔断机制Breaker中间件 ,实现流量控制Shedding中间件,设置请求超时时间的Timeout中间件,恢复异常请求的Recover中间件…如果有配置则将这些中间件添加到chain中
  2. 获取engine的middlewares中间件,遍历添加到chain中,也就是我们通过Server的Use()方法,或rest下WithMiddleware()/WithMiddlewares()函数主动注册中间件,将这些中间件添加到chain中
  3. 调用chain的ThenFunc()方法,遍历所有中间件,调用中间件的Handle()方法,将中间件包装为Handler形成中间件Handler链,并将接口的处理Handler添加到了链条的末尾,在执行时匹配到handler后会基于这个链条向下调用到最终的接口处理函数
  4. 调用Router的Handle()方法,将路由注册到路由树上,也就是前缀树的构建
func (ng *engine) bindRoutes(router httpx.Router) error {
	//创建 metrics 对象,用于记录请求的统计信息
	metrics := ng.createMetrics()
	//遍历 engine.routes属性中的每一个路由
	for _, fr := range ng.routes {
		//调用 bindFeaturedRoutes方法进行绑定
		if err := ng.bindFeaturedRoutes(router, fr, metrics); err != nil {
			return err
		}
	}
	return nil
}

func (ng *engine) bindFeaturedRoutes(router httpx.Router, fr featuredRoutes, metrics *stat.Metrics) error {
	// 调用 signatureVerifier 方法,根据 fr.signature 的值创建一个签名验证器
	verifier, err := ng.signatureVerifier(fr.signature)
	if err != nil {
		return err
	}
	//遍历 fr.routes 中的每一个路由
	for _, route := range fr.routes {
		//调用 bindRoute 方法进行绑定
		if err := ng.bindRoute(fr, router, metrics, route, verifier); err != nil {
			return err
		}
	}
	return nil
}

//bindRoute()接收参数中: metrics是统计相关的,verifier 是一个签名验证器
//重点做了如下动作: 
//1.获取engine的chain属性,如果为空调用buildChainWithNativeMiddlewares()根据配置判断添加中间件
//2.获取engine的middlewares中间件,遍历添加到chain中
//3.调用chain的ThenFunc()方法将路由接口添加到chain中
//4.调用Router的Handle()方法,将路由注册到路由树上,也就是前缀树的构建
func (ng *engine) bindRoute(fr featuredRoutes, router httpx.Router, metrics *stat.Metrics,
	route Route, verifier func(chain.Chain) chain.Chain) error {
	//1.获取 engine的chain属性(也就是通过rest下的withChain()注册的连接器又封装了一层的中间件)
	chn := ng.chain
	if chn == nil {
		//如果为空调用 buildChainWithNativeMiddlewares()根据 fr, route, metrics 构建一个 chain 对象
		chn = ng.buildChainWithNativeMiddlewares(fr, route, metrics)
	}
	//2.调用appendAuthHandler()根据 fr, chn, verifier 在 chain 对象后面追加一个认证处理器
	chn = ng.appendAuthHandler(fr, chn, verifier)

	//3.遍历 engine.middlewares属性中保存的每一个中间件
	for _, middleware := range ng.middlewares {
		//调用 convertMiddleware()函数,将中间件转换为 chain.Handler 类型,
		//并追加到 chain 对象后面
		chn = chn.Append(convertMiddleware(middleware))
	}
	//4.调用chain的ThenFunc()将route.Handler也就是接口函数作为最后一个处理器,添加到chain中
	//该函数中会遍历c.middlewares中的每个中间件,将每个中间件包装为handle,形成一个handler链
	//最后返回这个handler链的入口,也就是第一个中间件包装的handler对象
	handle := chn.ThenFunc(route.Handler)
	
	//调用 router 的 Handle 方法,将 route.Method, route.Path 和 handle 作为参数,注册到路由树上
	return router.Handle(route.Method, route.Path, handle)
}

func (ng *engine) buildChainWithNativeMiddlewares(fr featuredRoutes, route Route,
	metrics *stat.Metrics) chain.Chain {
	//创建一个空的链式处理器
	chn := chain.New()

	//如果配置了Trace中间件,在链式处理器中添加TraceHandler,用于追踪请求的路径和忽略的路径
	if ng.conf.Middlewares.Trace {
		chn = chn.Append(handler.TraceHandler(ng.conf.Name,
			route.Path,
			handler.WithTraceIgnorePaths(ng.conf.TraceIgnorePaths)))
	}
	//如果配置了Log中间件,在链式处理器中添加getLogHandler,用于记录请求的日志
	if ng.conf.Middlewares.Log {
		chn = chn.Append(ng.getLogHandler())
	}
	//如果配置了Prometheus中间件,在链式处理器中添加PrometheusHandler,用于收集请求的指标数据
	if ng.conf.Middlewares.Prometheus {
		chn = chn.Append(handler.PrometheusHandler(route.Path, route.Method))
	}
	//如果配置了MaxConns中间件,在链式处理器中添加MaxConnsHandler,用于限制最大并发连接数
	if ng.conf.Middlewares.MaxConns {
		chn = chn.Append(handler.MaxConnsHandler(ng.conf.MaxConns))
	}
	// 如果配置了Breaker中间件,在链式处理器中添加BreakerHandler,用于实现熔断机制
	if ng.conf.Middlewares.Breaker {
		chn = chn.Append(handler.BreakerHandler(route.Method, route.Path, metrics))
	}
	//如果配置了Shedding中间件,在链式处理器中添加SheddingHandler,用于实现流量控制
	if ng.conf.Middlewares.Shedding {
		chn = chn.Append(handler.SheddingHandler(ng.getShedder(fr.priority), metrics))
	}
	//如果配置了Timeout中间件,在链式处理器中添加TimeoutHandler,用于设置请求的超时时间
	if ng.conf.Middlewares.Timeout {
		chn = chn.Append(handler.TimeoutHandler(ng.checkedTimeout(fr.timeout)))
	}
	//如果配置了Recover中间件,在链式处理器中添加RecoverHandler,用于恢复请求的异常
	if ng.conf.Middlewares.Recover {
		chn = chn.Append(handler.RecoverHandler)
	}
	//如果配置了Metrics中间件,在链式处理器中添加MetricHandler,用于记录请求的统计数据
	if ng.conf.Middlewares.Metrics {
		chn = chn.Append(handler.MetricHandler(metrics))
	}
	//如果配置了MaxBytes中间件,在链式处理器中添加MaxBytesHandler,用于限制请求的最大字节数
	if ng.conf.Middlewares.MaxBytes {
		chn = chn.Append(handler.MaxBytesHandler(ng.checkedMaxBytes(fr.maxBytes)))
	}
	//如果配置了Gunzip中间件,在链式处理器中添加GunzipHandler,用于解压缩请求的数据
	if ng.conf.Middlewares.Gunzip {
		chn = chn.Append(handler.GunzipHandler)
	}
	return chn
}

//添加一个授权验证的中间件
//入参verifier是一个函数类型,表示一个验证器,它接受一个链式处理器作为参数,并返回一个链式处理器作为结果
func (ng *engine) appendAuthHandler(fr featuredRoutes, chn chain.Chain,
	verifier func(chain.Chain) chain.Chain) chain.Chain {
	//1.判断是否启用了jwt
	if fr.jwt.enabled {
		if len(fr.jwt.prevSecret) == 0 {
			//如果没有设置前一个密钥,就使用当前的密钥进行授权验证,并在验证失败时调用unauthorizedCallback函数
			chn = chn.Append(handler.Authorize(fr.jwt.secret,
				handler.WithUnauthorizedCallback(ng.unauthorizedCallback)))
		} else {
			//如果设置了前一个密钥,就使用当前的密钥和前一个密钥进行授权验证,并在验证失败时调用unauthorizedCallback函数
			chn = chn.Append(handler.Authorize(fr.jwt.secret,
				handler.WithPrevSecret(fr.jwt.prevSecret),
				handler.WithUnauthorizedCallback(ng.unauthorizedCallback)))
		}
	}
	//返回经过验证器处理后的链式处理器
	return verifier(chn)
}

//这个方法在执行角度看还是比较重要的
//遍历所有中间件,调用中间件的Handle()方法,将中间件包装为一个新的handler对象,
//最终形成一个中间件的handler链,并将传入的h添加到了这个链条的末尾
func (c chain) Then(h http.Handler) http.Handler {
	if h == nil {
		h = http.DefaultServeMux
	}
	
	//遍历注册的所有中间件
	for i := range c.middlewares {
		//对每个中间件调用它的Handle(next http.HandlerFunc) http.HandlerFunc方法,
		//传入当前的h作为参数,会返回一个新的handler对象,最终形成一个handler链
		h = c.middlewares[len(c.middlewares)-1-i](h)
	}

	return h
}
  1. 查看patRouter的Handle()方法添加路由构建前缀树的源码,内部会
  1. patRouter是在NewServer()函数创建服务Server内部通过NewRouter()初始化的,内部有个trees属性,保存了以Method为维度的多个前缀树
  2. 在Handle()方法中会根据当前路由的Method判断是否存在该类型的前缀树,如果存在则调用Tree的Add()方法添加,如果不存在,则先调用调用一个NewTree()新建一个前缀树,然后调用Tree的Add()方法添加
  3. Tree的Add()方法中会调用一个add()函数,该函数中,会根据"/“截取路由path中的每一段,作为token标识判断当前前缀树中是否已经存在该节点,如果存在则递归调用根据”/"继续截取当当前节点作为子节点判断添加前缀树中,
  4. 如果不存在则调用newNode()函数新建node节点,.以当前路由截取到的token标识为key添加到前缀树中
func (pr *patRouter) Handle(method, reqPath string, handler http.Handler) error {
	if !validMethod(method) {
		return ErrInvalidMethod
	}

	if len(reqPath) == 0 || reqPath[0] != '/' {
		return ErrInvalidPath
	}

	cleanPath := path.Clean(reqPath)
	//根据method在patRouter的trees树中获取指定前缀树
	tree, ok := pr.trees[method]
	if ok {
		//如果存在调用Tree的Add()方法注册路由
		return tree.Add(cleanPath, handler)
	}
	//如果patRouter的trees中不存在当前method前缀树则创建
	tree = search.NewTree()
	pr.trees[method] = tree
	//然后调用Tree的Add()方法注册路由
	return tree.Add(cleanPath, handler)
}

func (t *Tree) Add(route string, item interface{}) error {
	if len(route) == 0 || route[0] != slash {
		return errNotFromRoot
	}

	if item == nil {
		return errEmptyItem
	}
	//注册路由
	err := add(t.root, route[1:], item)
	//异常判断
	switch err {
	case errDupItem:
		return duplicatedItem(route)
	case errDupSlash:
		return duplicatedSlash(route)
	default:
		return err
	}
}

func add(nd *node, route string, item interface{}) error {
	//如果路由为空,表示已经到达最后一个节点
	if len(route) == 0 {
		if nd.item != nil {
			// 如果当前节点已经有了项,就返回errDupItem错误
			return errDupItem
		}
		//否则,就把项赋值给当前节点,并返回nil
		nd.item = item
		return nil
	}
	
	//如果路由以斜杠开头,就返回errDupSlash错误
	//slash是"/"
	if route[0] == slash {
		return errDupSlash
	}

	//遍历路由中的每个字符
	for i := range route {
		//如果不是斜杠,continue跳过
		if route[i] != slash {
			continue
		}
		//截取路由中第一个斜杠之前的部分作为token(也就是当前写过之前的)
		token := route[:i]
		//获取当前节点对应token的子节点集合(可能是静态子节点或者动态子节点)
		//返回的是一个 map[string]*node 变量
		children := nd.getChildren(token)
		//如果子节点集合中存在当前token对应的子节点对象
		if child, ok := children[token]; ok {
			if child != nil {
				//并且子节点对象不为nil,递归调用add函数,在子节点上继续添加剩余的路由并返回结果
				return add(child, route[i+1:], item)
			}
			//子节点对象为nil,返回errInvalidState错误(这种情况不应该发生)
			return errInvalidState
		}

		//如果子节点集合中不存在当前token对应的子节点对象,创建一个新的空节点对象作为子节点
		child := newNode(nil)
		//把新创建的子节点对象添加到子节点集合中
		children[token] = child
		//递归调用add函数,在新创建的子节点上继续添加剩余的路由
		return add(child, route[i+1:], item)
	}
	
	//如果路由中没有斜杠了,表示已经到达最后一个token也就是路由的最后一个节点,获取当前节点对应token的子节点集合
	children := nd.getChildren(route)
	//如果子节点集合中存在token对应的子节点对象
	if child, ok := children[route]; ok {
		//如果子节点对象已经有了当前项,返回errDupItem错误
		if child.item != nil {
			return errDupItem
		}
		//没有,就把当前节点添加到子节点中
		child.item = item
	} else {
		//如果子节点集合中不存在当前token对应的子节点对象
		//创建一个新的节点对象作为子节点,然后添加到子节点集合中
		children[route] = newNode(item)
	}
	return nil
}

启动服务到触发net/http

  1. 在调用Start()方法启动服务时,内部会有一个判断:如果配置文件中没有证书文件和密钥文件,调用 internal 包中的 StartHttp()方法,如果有则调用StartHttps()启动一个 http 服务
func (s *Server) Start() {
	handleError(s.ngin.start(s.router))
}

// 定义一个 start 方法,接收一个 router 参数和可变数量的 StartOption 参数,返回一个 error 类型的值
func (ng *engine) start(router httpx.Router, opts ...StartOption) error {
	// 调用 bindRoutes 方法,将路由绑定到 ng 上,如果出错则返回错误
	if err := ng.bindRoutes(router); err != nil {
		return err
	}

	// 将 ng.withTimeout 方法作为一个 StartOption 参数添加到 opts 切片的开头
	opts = append([]StartOption{ng.withTimeout()}, opts...)

	// 如果配置文件中没有证书文件和密钥文件,则调用 internal 包中的 StartHttp 方法,启动一个 http 服务
	if len(ng.conf.CertFile) == 0 && len(ng.conf.KeyFile) == 0 {
		return internal.StartHttp(ng.conf.Host, ng.conf.Port, router, opts...)
	}

	// 如果配置文件中有证书文件和密钥文件,则创建一个匿名函数,将 ng 的 tlsConfig 属性赋值给 svr 的 TLSConfig 属性,然后将这个匿名函数作为一个 StartOption 参数添加到 opts 切片的末尾
	opts = append([]StartOption{
		func(svr *http.Server) {
			if ng.tlsConfig != nil {
				svr.TLSConfig = ng.tlsConfig
			}
		},
	}, opts...)

	// 调用 internal 包中的 StartHttps 方法,启动一个 https 服务
	return internal.StartHttps(ng.conf.Host, ng.conf.Port, ng.conf.CertFile,
		ng.conf.KeyFile, router, opts...)
}
  1. 以StartHttp()为例,查看该函数,内部调用了net/http下的ListenAndServe()
func StartHttp(host string, port int, handler http.Handler, opts ...StartOption) error {
	//start()源码在下方
	return start(host, port, handler, func(svr *http.Server) error {
		//封装了net/http下的ListenAndServe()
		return svr.ListenAndServe()
	}, opts...)
}

//函数类型的run入参就是net/http下的ListenAndServe()
func start(host string, port int, handler http.Handler, run func(svr *http.Server) error,
	opts ...StartOption) (err error) {
	//创建一个*http.Server类型的对象,并设置其地址和处理器属性
	server := &http.Server{
		Addr:    fmt.Sprintf("%s:%d", host, port),
		Handler: handler,
	}
	for _, opt := range opts {
		opt(server)
	}
	//创建一个健康管理器对象,用于检测服务器的健康状态
	healthManager := health.NewHealthManager(fmt.Sprintf("%s-%s:%d", probeNamePrefix, host, port))
	//添加一个进程结束时的监听器函数,用于关闭服务器和标记健康状态为不可用
	waitForCalled := proc.AddWrapUpListener(func() {
		healthManager.MarkNotReady()
		if e := server.Shutdown(context.Background()); e != nil {
			logx.Error(e)
		}
	})
	//使用延迟函数,在函数返回时执行以下逻辑
	defer func() {
		//如果返回的错误是http.ErrServerClosed,表示服务器已经关闭,就调用waitForCalled函数等待监听器函数执行完毕
		if err == http.ErrServerClosed {
			waitForCalled()
		}
	}()
	//标记健康状态为可用
	healthManager.MarkReady()
	//添加健康管理器对象到健康检测模块中
	health.AddProbe(healthManager)
	//调用run函数,启动服务器,并返回结果
	return run(server)
}
  1. 到这里就来到了go的net/http标准库,具体参考go 进阶 http标准库相关: 三. HttpServer 服务启动到Accept等待接收连接,简单复习一下net/http提供服务的流程:
  1. 在通过net/http编写服务端时, 首先调用NewServeMux()创建多路复用器,编写对外接收请求的接口函数也就是处理器,然后调用多路复用器上的HandleFunc()方法,将接口与接口路径进行绑定,注册路由, 最后调用ListenAndServe()函数在指定端口开启监听,启动服务
  2. ListenAndServe()方法内部重点调用了"net.Listen(“tcp”, addr)"多路复用相关初始化,初始化socket,端口连接绑定,开启监听,调用"srv.Serve(ln)”:等待接收客户端连接Accept(),与接收到连接后的处理流程
  3. 服务相关的我们先关注"srv.Serve(ln)",方法内通过for开启了一个死循环,在循环内部,调用Listener的Accept()方法,假设当前是TCP连接调用的就是TCPListener下的Accept(),监听客户端连接,当接收到客户端连接后,通过开启协程执行serve()方法处理请求,每一个连接开启一个goroutine来处理

接收请求的处理

  1. go-zero底层是基于net/http实现的,再看一下net/http接收请求时底层是如何执行的go 进阶 http标准库相关: 五. HttpServer 接收请求路由发现原理,
  2. 简单复习一下,基于net/http搭建服务时,底层会执行ListenAndServe()方法,最终会执行到Listener的Accept()方法,假设当前是TCP连接调用的就是TCPListener下的Accept(),阻塞监听客户端连接,当有接收到连接请求后Accept()方法返回,拿到一个新的net.Conn连接实例,然后开启协程调用Conn连接实例上的serve()方法处理客户端请求,查看这个serve()方法:
  1. 首先调用newBufioReader() 封装了一个bufio.Reader
  2. 开启了一个无限for循环,循环内
  3. 调用conn的readRequest(ctx)方法读取请求的内容,比如解析HTTP请求协议,读取请求头,请求参数,封装Request和response,在解析时会读取请求头的 Content-Length,不为 0会通过TCPConn.Read() 方法读取指定长度的数据并存入请求体中,如果 Content-Length 为 0 或者没有设置,则请求体为空
  4. 封装serverHandler调用serverHandler上的ServeHTTP(w, w.req)方法进行路由匹配,找到对应的处理函数,执行我们写的业务逻辑
  5. 调用response的finishRequest()方法进行最后处理工作,当底层 bufio.Writer 缓冲区的大小达到阈值或者Flush() 被显式调用时,就会将缓冲区内的数据写入到底层连接中,并触发 Conn 的 Write() 方法将数据发送到客户端,另外finishRequest()方法还会进行一些比如异常处理,资源回收,状态更新等操作
  6. 最后调用conn的setState()设置连接状态为StateIdle,方便后续重用连接
  1. 这里执行的ServeHTTP()就是匹配路由触发业务接口的函数,ServeHTTP是一个接口,绝大多数Web框架都是通过实现该接口,从而替换掉Golang默认的路由,这里执行的就是patRouter实现的ServeHTTP(),查看该函数源码
  1. 首先根据请求的method在trees中获取到指定前缀树,如果存在则根据请求的reqPath路径在前缀树中查找对应的handler对象,如果找到,则调用这个handler对象的ServeHTTP()方法
  2. 在服务启动时会将添加的中间件添加到chain处理器链中,然后遍历所有中间件转换为handler链,并将接口处理函数添加到handler链的末尾,这个过程是在chain类型的Then()方法中完成的。
  3. 在路由匹配时拿到第一个中间件handler开始执行,如果执行通过,中间件中会继续调用ServeHTTP(),也就是继续执行下一个中间件,一直执行到接口处理函数
func (pr *patRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	//对请求的路径进行清理.去除多余的斜杠和点
	reqPath := path.Clean(r.URL.Path) 
	//先通过请求的method获取到指定的前缀树
	if tree, ok := pr.trees[r.Method]; ok {
		//如果前缀树存在,则开始匹配路由
		if result, ok := tree.Search(reqPath); ok { 
			if len(result.Params) > 0 { 
				//如果结果对象中有参数,就把参数添加到请求的上下文中
				r = pathvar.WithVars(r, result.Params)
			}
			//匹配到指定路由后转换为http.Handler类型,并调用它的ServeHTTP方法处理请求
			result.Item.(http.Handler).ServeHTTP(w, r) 
			return
		}
	}
	
	//如果未找到对应的前缀树调用pr.methodsAllowed方法,获取请求的路径允许的方法列表,并判断请求的方法是否在其中
	allows, ok := pr.methodsAllowed(r.Method, reqPath) 
	if !ok { // 如果请求的方法不在允许的方法列表中
		//调用pr.handleNotFound方法,处理未找到的情况,并返回
		pr.handleNotFound(w, r) 
		return
	}
	//如果pr.notAllowed不为nil,表示有自定义的处理器对象用于处理不允许的方法的情况
	if pr.notAllowed != nil {
		//调用pr.notAllowed的ServeHTTP方法,处理请求,并返回
		pr.notAllowed.ServeHTTP(w, r)
	} else { 
		// 否则,就使用默认的处理逻辑
		w.Header().Set(allowHeader, allows) //设置响应头中的Allow字段为允许的方法列表
		w.WriteHeader(http.StatusMethodNotAllowed) //设置响应状态码为405 Method Not Allowed,并返回
	}
}

三. 总结

  1. go-zero http服务是怎么启动的, 是怎么整合net/http的, 前缀树是怎么构建的,路由,中间件是怎么注册的,当接收到请求后是怎么匹配路由执行的,当问到这些问题都可以由rest下的NewServer()函数创建服务Server开始说,下面总结一下
  2. 了解go-zero http服务首先要了解几个结构体,比如engine服务引擎,首先要创建这个引擎,内部有几个比较重要的属性比如
  1. RestConf类型的conf属性: 存储了服务启动运行所需的配置信息
  2. featuredRoutes类型的切片routes属性,在执行AddRoutes()注册接口时会将将接口封装为Router,然后将Router通过AddRoutes()保存到这个切片属性中,服务启动时会通过这个切片获取到所有路由,构建前缀树进行路由注册
  3. Middleware类型切片的middlewares属性,会将通过Server的Use()方法注册的全局中间件保存到这个切片属性中,后续会通过这个属性获取到注册的中间件,加上局部中间件,接口handler转换为chain执行链,转换为Handler处理器链
  1. 还有一个比较重要的结构体patRouter, 内部存在一个trees属性,内部存储了以路由的method为key的前缀树,patRouter是路由注册与请求执行的核心,该结构体实现了ServeHTTP()方法,会通过这个方法处理用户的请求
  2. 说一下服务启动的执行过程,在构建go-zero http服务时,首先需要将配置设置到RestConf结构体变量上,通过执行rest下的NewServer()函数,读取配置创建服务端的Server,查看这个NewServer()源码
  1. 首先调用调用newEngine()创建Engine服务引擎
  2. 调用NewRouter()函数,初始化patRouter,初始化patRouter中的trees路由前缀树
  3. 将这两个属性封装到了一个Server结构体变量,并返回,然后通过Server结构体变量调用Use()方法注册中间件,调用AddRoutes()方法注册路由,调用Start()方法启动服务
  1. 中间件的注册: 在go-zero中可以通过Server的Use()方法注册全局中间件,可以通过WithMiddlewares/WithMiddleware()函数注册针对一组路由的局部中间件
  1. 通过Use()方法注册的中间件会保存到Engine的middlewares属性中
  2. 通过WithMiddlewares/WithMiddleware()函数注册的局部中间,查看源码发现,中间件函数会封装到Route的Handler中,跟随Route路由一块注册
  3. 另外go-zero的rest包下还有一个WithChain()也可以用来注册中举中间件,但是通过该函数注册的中间件会保存到Engine的chain属性中,是一个中间件链条
  1. 路由的保存: 当通过NewServer()拿到服务Server以后,调用Server的AddRoutes()方法,将对外的接口封装为Route进行路由后注册,在AddRoutes()方法中,将路由封装为featuredRoutes,保存到了engine的routes 属性中,这是保存,后续构建前缀树的逻辑在Server的Start()启动服务方法中
  2. 当拿到服务Server以后,查看Server的Start()启动服务方法,内部会调用engine的start()方法,内部重点执行了:
  1. bindRoutes(): 注册路由中间件,构建前缀树
  2. 判断如果没有配置证书,执行StartHttp()启动http服务,有配置执行StartHttps()启动https服务,触发到net/http
  1. bindRoutes()中间件,路由的注册与前缀树的构建,通过engine的routes属性获取到所有路由,遍历调用engine的bindFeaturedRoutes()方法开始注册路由,内部会调用engine的bindRoute(),在该方法中
  1. 首先获取engine的chain属性,如果为空,会调用buildChainWithNativeMiddlewares()在该方法中根据配置信息判断添加一下默认的中间件,比如判断是否配置了追踪请求的Trace中间件, 记录请求日志的Log中间件,收集请求指标数据Prometheus中间件,限制最大并发连接数的MaxConns中间件,实现熔断机制Breaker中间件 ,实现流量控制Shedding中间件,设置请求超时时间的Timeout中间件,恢复异常请求的Recover中间件…如果有配置则将这些中间件添加到chain中
  2. 遍历engine的middlewares也就是拿到全局中间件,将这些中间件也添加到到engine的chain属性中整个中间件链条封装完毕
  3. 比较重要的一个步骤,拿到路由接口的处理器Handler,执行chain的ThenFunc()方法,将保存了中间件链条的chain转换为Handler链,并将接口的处理器Handler添加到Handler链的末尾(在接收请求时根据路由匹配拿到指定的Handler后会基于这个链条向下调用到最终的接口处理函数)
  4. 调用Router的Handle()方法,将路由注册到路由树上,也就是前缀树的构建,实际执行的是patRouter的Handle(),方法中:
  5. 根据当前路由的Method判断是否存在该类型的前缀树,如果存在则调用Tree的Add()方法添加,如果不存在,则先调用调用一个NewTree()新建一个前缀树,然后调用Tree的Add()方法添加
  6. Tree的Add()方法中会调用一个add()函数,该函数中,会根据"/“截取路由path中的每一段,作为token标识判断当前前缀树中是否已经存在该节点,如果存在则递归调用根据”/"继续截取当当前节点作为子节点判断添加前缀树中,
  7. 如果不存在则调用newNode()函数新建node节点,.以当前路由截取到的token标识为key添加到前缀树中
  1. Server的Start()方法服务的启动, 查看源码内部会根据是否配置了证书选择调用StartHttp()/StartHttps()启动http或https服务,以http为例,查看StartHttp()源码内最终封装调用了net/http标准库中的ListenAndServe()
  2. 了解go-zero http服务怎么接收请求执行的,要先了解net/http是怎么请求,路由匹配的,在net/http处理请求时会调用路由的ServeHTTP()方法,这里调用的就是patRouter上的这个方法,查看源码:
  1. 首先根据请求的method在trees中获取到指定前缀树,如果存在则根据请求的reqPath路径在前缀树中查找对应的handler对象,如果找到,则调用这个handler对象的ServeHTTP()方法
  2. 在服务启动时会将添加的中间件添加到chain处理器链中,然后遍历所有中间件转换为handler链,并将接口处理函数添加到handler链的末尾,这个过程是在chain类型的Then()方法中完成的。
  3. 在路由匹配时拿到第一个中间件handler开始执行,如果执行通过,中间件中会继续调用ServeHTTP(),也就是继续执行下一个中间件,一直执行到接口处理函数
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

go 进阶 go-zero相关: 二. 服务启动与路由,中间件注册,请求接收底层原理 的相关文章

  • 如何以编程方式从 iPhone 地址簿获取地址占位符文本?

    我试图为用户提供一种在基于位置的应用程序中输入地址的方法 并且我希望它看起来与 iPhone 联系人 地址簿中的地址完全相同 这意味着我需要根据所选国家 地区更新每个字段的占位符文本 例如 美国占位符是 街道 城市 State ZIP 英国
  • CGContextSetLineWidth(context, 1) - 宽度几乎总是至少 2 像素而不是 1

    With CGContextSetLineWidth context 1 宽度几乎总是至少 2 像素而不是 1 QQCandleStickLayer m id init self super init if self nil self de
  • UITextField:键盘出现时移动视图

    我目前正在开发一个具有单个视图的 iPhone 应用程序 该应用程序有多个 UITextFields 用于输入 当键盘显示时 它会覆盖底部的文本字段 所以我添加了相应的textFieldDidBeginEditing 方法 将视图向上移动
  • 在ios键盘上方显示建议工具栏

    我是iOS开发的新手 我正在尝试在 ios 5 1 中创建一个具有 textView 的拼写建议类型应用程序 这样如果用户点击键盘的某个键 则建议工具栏会出现在键盘顶部 其中包含所有建议 并且如果用户点击这些建议之一它将显示在 textVi
  • 高度在 IOS (iphone) 上无法正常工作

    我已经创建了this https codepen io salman15 project live DWbWpo Codepen 上的网站 在尝试使其响应所有平台时 我遇到了问题 看起来单个 div 覆盖了整个页面 仅在 IOS 上 并且并
  • 签名仅对临时无效

    我不确定我的临时项目发生了什么变化 但在尝试安装时出现此错误 应用程序未通过协同设计验证 签名无效 或者不是用Apple提交证书签名的 19011 设备调试构建良好 与我的临时配置文件关联的证书直到 2011 年才会过期 我搜索了 Goog
  • 使用Apple80211 api时如何知道OPEN、WPA、WPA2、WEP等安全类型?

    Cydia中的Wifi WiFi FoRum等wifi扫描应用可以知道安全类型 使用 Apple80211 api 时 应用程序如何知道 OPEN WPA WPA2 WEP 等安全类型 CAPABILITIES 的值为 1057 1025
  • 是否可以在“NSFetchRequest”中按子类排序而不添加其他属性?

    我想对结果进行分组NSFetchRequest按实体 这些实体都共享相同的抽象父级 例如 animal cat dog The NSFetchRequest has includesSubentities set TRUE and enti
  • 在 iPhone 应用程序中获取路线和路线导航

    我正在开发一款应用程序 该应用程序将重点关注在驾驶时为用户提供路线和逐段指示 他们在驾驶过程中留在应用程序中非常重要 因此我真的不想让他们离开应用程序并转到内置的地图应用程序 我最近对如何包含此功能进行了大量研究 众所周知 这并不容易 因为
  • iOS Facebook SDK - 远程定义 FacebookAppID

    使用iOS Facebook SDK 3 0 需要在应用程序的info plist中定义FacebookAppID和相关的URL Scheme 我想远程定义这些 向我自己的服务器请求应用程序 ID 所以有两个不同的问题 我可以在运行时更改应
  • 如何从 NSData 创建字节数组

    请任何人指导我如何从 nsdata 创建字节数组这是我创建 nsdata 的代码 NSData data UIImagePNGRepresentation img 如果您只想阅读它们 有一个非常简单的方法 unsigned char byt
  • 如何更改 iOS 5 中 UITabBarItem 中文本的颜色

    iOS 5 中有更多外观控制 我们如何更改 UITabBarItem 文本颜色 从默认白色变为其他颜色 编辑 工作解决方案 UITabBarItem appearance setTitleTextAttributes NSDictionar
  • 应用程序关闭时下载报刊亭应用程序

    我正在实现一个报摊杂志应用程序 它通过 Urban Airship 推送通知接收新期刊 只要应用程序位于前台或后台 这就可以正常工作 但据我所知 当应用程序完全关闭时也应该触发下载 但发送推送 content available 1如果我的
  • iphone相当于android打开其他应用程序的意图

    是否有像 iphone 中可用的 android 意图功能 Android 使用意图从调用应用程序打开其他应用程序 以使用其他应用程序已实现的功能 我在某处读到 iphone 有 url 方案 但找不到更多信息 thanks 尝试查看以下答
  • NSString 对象的最大长度是多少?

    NSString 对象中可以保存的最大字符串大小是多少 这会动态变化吗 我假设 NSString 的硬限制是 NSUIntegerMax 个字符 因为 NSString 的索引和大小相关的方法返回 NSUInteger 由于当前能够运行 i
  • 自定义 UINavigationController UINavigationBar

    基本上我想要一个定制UINavigationBar 我不希望它是 半透明 或任何东西 就像图片应用程序一样 我基本上想完全删除它 但我仍然希望能够在按下导航控制器时添加后退按钮等 并且我想要视图 例如 UITableViewControll
  • 检测 iPhone 屏幕是否打开/关闭

    有没有办法检测 iPhone 的屏幕是打开还是关闭 例如 当按下手机的屏幕锁定按钮时 我一直在使用 void applicationWillResignActive UIApplication application 为此类事件做准备 在大
  • iOS绘图3D图形库[关闭]

    Closed 这个问题不符合堆栈溢出指南 help closed questions 目前不接受答案 我正在搜索一个可以帮助我绘制 3D 图表的库 我想要类似的东西这一页 http www math uri edu bkaskosz fla
  • 如何在 iPhone 中使用 XMPPFramework 创建 MultiUserChatRoom

    我正在 iPhone 中使用 XMPPFramwwork 开发聊天应用程序 我成功完成了一对一聊天 现在我想在我的应用程序中开发多用户聊天 我尝试了以下代码 但未调用 XMPPRoomDelegate 的任何委托方法 我如何开始创建聊天室
  • 使用ios sdk在youtube上上传视频的方法[重复]

    这个问题在这里已经有答案了 可能的重复 如何从 iOS 应用程序中将视频上传到 YouTube https stackoverflow com questions 3528568 how do i upload a video to you

随机推荐

  • BES2300x笔记----TWS组对与蓝牙配对

    https me csdn net zhanghuaishu0 一 前言 看到有 道友 在评论区留言 对TWS组对 BT配对以及回连流程部分很迷糊 那这第二篇我们就来说说BES平台的相关流程和接口 PS 蓝牙基础部分就不再赘述了 网上有很多
  • jdbc mysql 重连_mysql重连的问题

    应用在长时间不连mysql后会与mysql断开 再次链接mysql时会报无法连接数据库的异常 所以连接的配置需要稍微改一下 factory org apache naming factory BeanFactory driverClass
  • LABVIEW连接MySQL进行读写更新查询操作并仿真

    相关软件的准备 欢迎访问我的小站 我的软件环境是LabVIEW 2018 32位 的 这个很重要 因为不同位数的labview需要安装不同位数的Connector odbc 还需要安装visio的运行环境 这个需要提前准备 Mysql的安装
  • 华为数字化转型之道 平台篇 第十三章 变革治理体系

    第十三章 变革治理体系 约翰 科特在 领导变革 一书中说 变革的领导团队既需要管理能力 也需要领导能力 他们必须结合起来 前面我们也谈到 数字化转型不仅是技术的创新 更是一项系统工程和企业真正的变革 企业要转型成功 既需要各个组织的积极参与
  • python---matplotlib详细教程(完结)

    文章每个图都带有案例 欢迎访问 目录 如何选择合适的图表 绘制简单的折线图 图表常用设置 颜色设置 线条样式和标记样式 画布设置 设置坐标轴标题 plt rcParams font sans serif SimHei 解决缺失字体 设置坐标
  • 【三】springboot整合token(超详细)

    springboot篇章整体栏目 一 springboot整合swagger 超详细 二 springboot整合swagger 自定义 超详细 三 springboot整合token 超详细 四 springboot整合mybatis p
  • 【华为OD机试真题 python】组装新的数组【2023 Q1

    题目描述 组装新的数组 给你一个整数M和数组N N中的元素为连续整数 要求根据N中的元素组装成新的数组R 组装规则 1 R中元素总和加起来等于M 2 R中的元素可以从N中重复选取 3 R中的元素最多只能有1个不在N中 且比N中的数字都要小
  • python格式化输出,format,数据类型转换。

    输出 计算机给用户输出的内容 是一个由里到外的一个过程 例如python语言中的print函数 输入 则相反 例如input函数 一 输出有普通的输出 也有格式化输出 普通输出 类似于 print hello word 这样直接打印 格式化
  • 为高尔夫比赛砍树

    为高尔夫比赛砍树 你被请来给一个要举办高尔夫比赛的树林砍树 树林由一个 m x n 的矩阵表示 在这个矩阵中 0 表示障碍 无法触碰 1 表示地面 可以行走 比 1 大的数 表示有树的单元格 可以行走 数值表示树的高度 每一步 你都可以向上
  • 系统篇: squashfs 文件系统

    一 squashfs简介 Squashfs是一套基于Linux内核使用的压缩只读文件系统 该文件系统能够压缩系统内的文档 inode以及目录 文件最大支持2 64字节 特点 数据 data 节点 inode 和目录 directories
  • 虚幻C++ http请求

    直接上代码 Fill out your copyright notice in the Description page of Project Settings pragma once include CoreMinimal h inclu
  • 测试岗?从功能测试进阶自动化测试开发,测试之路不迷茫...

    目录 导读 前言 一 Python编程入门到精通 二 接口自动化项目实战 三 Web自动化项目实战 四 App自动化项目实战 五 一线大厂简历 六 测试开发DevOps体系 七 常用自动化测试工具 八 JMeter性能测试 九 总结 尾部小
  • Mock框架应用(四)-Mock 重定向请求

    例一 先新建json配置文件重定向到www baidu com 启动mock服务 description 实现重定向的请求 request uri redirect redirectTo https www baidu com respon
  • Go并发(goroutine)及并发常用模型的实现

    前言 Go 语言最吸引人的地方是它内建的并发支持 作为天然支持高并发的语言 写并发比java和python要简单方便的多 在并发编程中 对共享资源的正确访问需要精确的控制 在目前的绝大多数语言中 都是通过加锁等线程同步方案来解决这一困难问题
  • 疯壳-MTK智能电话手表开发整板测试

    目录 内容简介 3 第一节 开机 4 第二节 绑定 5 第三节 功能测试 9 3 1 屏幕测试 9 3 2 SIM通信测试 11 3 3 SIM 测试 12 3 4 GPS测试 14 3 5 手表对时 18 官网地址 https www f
  • 1449 砝码称重 51NOD

    1449 砝码称重 题目来源 CodeForces 基准时间限制 1 秒 空间限制 131072 KB 分值 40 难度 4级算法题 现在有好多种砝码 他们的重量是 w0 w1 w2 每种各一个 问用这些砝码能不能表示一个重量为m的东西 样
  • flink中idea配置pom.xml

  • JS之预解析

    javascript 的预解析 个人理解 就是js代码在执行之前 会在相应的执行环境中 预先把 一些东西解析到内存 如果理解错误 请多多指正 一 那究竟预先解析哪些东西那 答 预先解析 function 和 var 二 还有就是预解析的顺序
  • 分布式一致算法

    一 拜占庭将军问题 拜占庭将军问题 拜占庭派多支军队去围攻一个敌人 将军不确定军队中是否有叛徒 叛徒可能擅自变更进攻决定 至少一半以上的军队同时进攻才可以取胜 在这种状态下 拜占庭将军们能否找到一种分布式的协议来让他们能够远程协商 从而就进
  • go 进阶 go-zero相关: 二. 服务启动与路由,中间件注册,请求接收底层原理

    目录 一 问题概述 二 底层源码分析 涉及到的一些结构体简介 初始化 中间件的预设置 路由注册与中间件的处理 启动服务到触发net http 接收请求的处理 三 总结 一 问题概述 了解go zero底层也是基于net http标准库实现h