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相关: 二. 服务启动与路由,中间件注册,请求接收底层原理 的相关文章

  • 如何将 NSDecimal 值转换为 NSInteger 值?

    我遇到一种情况 我得到一个 NSDecimal 并且我需要一个 NSInteger 我确实知道这是一个非常小的值 这是绝对肯定的 它不会大于 100 所以将它转换为 NSInteger 就完全没问题 不会发生溢出 这怎么可能做到呢 NSDe
  • 持久化 UINavigationItem 的 rightBarButtonItem 属性

    这可能是一个愚蠢的问题 但是有可能保持正确的吗 UIBarButtonItem跨由一个管理的多个视图UINavigationController 我的一系列观点通常具有相同的权利UIBarButtonItem 但是当我将新视图推送到我的UI
  • 是否可以使用 Firebase 安排推送通知? [复制]

    这个问题在这里已经有答案了 我已经阅读了我能找到的所有文档 但仍然不知道这是否可行 如果我是用户 我可以安排特定时间的推送通知吗 Example 1 我是用户并打开应用程序 2 我允许通知并转到 pickerView 或其他任何内容 并设置
  • 在 iPhone 中缝合图片

    我想并排缝合2张png 在Cocoa中 我会使用 NSImage initWithSize 然后只是drawInRect 但是 UIImage 没有 initWithSize 类 我现在该怎么做 Use UIGraphicsBeginIma
  • 在发生更改事件时将货币格式重新应用到 UITextField

    我正在使用一个包含本地化货币值的 UITextField 我看过很多关于如何使用此功能的帖子 但我的问题是 如何在每次按键后将货币格式重新应用到 UITextField 我知道我可以通过以下方式设置和使用货币格式化程序 NSNumberFo
  • 在 UIScrollview 上显示缩略图的最佳方法是什么(从服务器下载)

    我想在 UIScrollview 如照片应用程序 上显示许多图像 作为缩略图 所有图像将从服务器下载 据我所知 有几种选择 1 通过创建 UIImageviews 然后将它们添加为主滚动视图上的子视图 2 通过子类化一个UIView类 然后
  • 有没有类似于 iOS 的 canOpenURL 来检查移动浏览器的 URL 方案? [复制]

    这个问题在这里已经有答案了 可能的重复 iPhone Safari 检查 JavaScript 是否支持 URL 方案 https stackoverflow com questions 627916 iphone safari check
  • Flutter 应用程序在 iOS 平台的 firebase 电话身份验证中崩溃

    我在我的项目中实现了 Firebase Phone auth 在 Android 端 一切正常 但对于 iOS 端 当我尝试从我的端发送验证码时 应用程序崩溃并失去连接 我已在下面提交了我的代码 主程序 dart class MyApp e
  • 在 iOS 7 中 viewForHeaderInSection 部分是从 1 开始而不是从 0 开始

    我正在处理UITableView在我的项目中 这个项目是在 Xcode 4 5 中创建的 现在我正在使用 Xcode 5 所以我的问题是何时在 iOS 6 中运行我的项目 viewForHeaderInSection方法部分从 0 开始没问
  • 如何删除 UITableView 中的缩进?

    首先 我对此很陌生 我很可能忘记了一些非常简单的事情 问题 我正在制作一个应用程序 在 a 中显示来自 imgur com 的随机图像tableView 由于某种原因 所有单元格都会缩进少量 如下图所示 我摆弄了许多设置storyboard
  • iPhone 上的语音识别

    我需要开发一个识别语音的 iPhone 应用程序 并根据结果执行进一步的任务 我知道iPhone 3 0不支持语音识别 我需要在服务器端实现语音识别软件 我只知道这个事情 因为我是新手 所以我不知道如何处理 意味着我需要购买哪些软件并在服务
  • 自定义相机视图 Swift iOS 8 iPhone Xcode 6.1

    我想在 iPhone 的 View 中使用相机 我不想使用典型的全屏相机视图 而是我自己的 例如 我想在屏幕中间有一个 200x200 的正方形 并且有一个相机预览 在这个方块下面我想要一个拍照按钮 怎么做 我是新手 速度很快 你会想要使用
  • 如何通过填充 NSDictionary 以 JSON 格式发送 UIImage

    我正在尝试使用 JSON 将数据发送到服务器 我可以使用我的对象和关键参数创建 NSDictionary 但我想发送我的图片 图片是UIImage NSDictionary mainJSON NSDictionary dictionaryW
  • UITableViewCell 的 viewDidAppear

    我通常使用viewDidAppear方法在视图完成出现后在视图上执行一些 UI 操作 我在各种情况下使用了此方法 它非常有用 但是 我需要在视图上进行一些 UI 更改UITableViewCell当它完成出现后 SDK中是否有任何可用的方法
  • Cocos2D复杂动画[关闭]

    Closed 这个问题正在寻求书籍 工具 软件库等的推荐 不满足堆栈溢出指南 help closed questions 目前不接受答案 我正在使用 Cocos2D 将我的游戏从 Flash 移植到 iOS 我现在有一个工作版本 我很高兴我
  • iOS:加载时的设备方向

    似乎当我的应用程序加载时 它不知道其当前方向 UIInterfaceOrientation orientation UIDevice currentDevice orientation if orientation UIDeviceOrie
  • 在 iOS 中从 ACAccountStore 获取 Facebook uid?

    您好 我想获取 Facebook 用户的 UIDAC帐户商店在 iOS 6 中 self accountStore ACAccountStore alloc init ACAccountType FBaccountType self acc
  • 无法从 iOS 中的框架访问 .nib(XIB) 文件

    我已经从现有的代码库中创建了一个框架 并尝试在新的代码库中使用它 这很好用 但是当我尝试访问属于我的框架包的一部分的 nib 文件时 我的应用程序崩溃了 这是我用来访问视图控制器 XIB 文件的代码 testViewController c
  • 如何将图像放入此 UIPickerView 中?

    我不知道如何创建一个在文本一侧带有图像的自定义 UIPickerView 我一直在寻找一种方法 我刚刚发现了这个 UIView pickerView UIPickerView pickerView viewForRow NSInteger
  • 如何使用 NSUserDefaults 正确工作(检索值)

    我的代码中有一个简单的方法 如下所示 BOOL isFirstTimeLogin NSString t gName NSString stringWithFormat NSUserDefaults standardUserDefaults

随机推荐

  • 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