golang-gin框架入门

news2024/12/25 1:02:48

基础

快速入门

  • gin完整支持路由框架
  • 支持全局异常(错误)处理
  • 内置渲染
  • 高可扩展

组件

  • 在gin框架中四个基本组件是:
    • Engine:是web server的根数据结构,也是基础容器;它包含复用器、中间件和配置设置类似SpringBoot的SpringApplication;
    • RouteGroup路由组,保存了路由路径,Radix Tree数据结构;
    • Handler:处理函数、中间件
    • Context一次请求/响应的完整上下文,封装了请求和响应的操作及需要的数据

Engine

基础
  • Engine 是框架的实例,它包含复用器、中间件和配置设置。一般使用New () 或Default () 创建 Engine 实例;
    • 默认包含:Logger和Recover中间件,用于日志和全局异常处理
  • 继承了RouterGroup
  • Engine一般使用Run(ip:port)直接启动
    • 同样支持通过其他方式启动
    • 支持设置server服务属性
image-20220610150939077
方法
  • Engine的方法大概可以分为
    • 全局处理
      • 异常处理
        • NoMethod:请求的路径的方式不对(405),在开启运行默认处理的时候,会调用这里设置的处理方法
        • NoRoute:没有路由路径(404),会调用这里设置的处理方法
      • 全局绑定
        • Use绑定全局调用链相当于spring的拦截器,其中的Context的Next方法决定拦截前后
        • LoadHtmlGloba&LoadHTMLFiles全局绑定加载的Html,避免重复书写路径(之后只需要书写html文件即可),当多个文件路径的时候可以使用LoadHTMLFiles
        • Delims设置一组用于HTML模板渲染的左右分隔符,默认是{{}}
      • 其他RouterGroup的StaticXXX方法,主要是绑定静态资源
        • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5qnmnKNf-1681483225389)(gin.assets/image-20220610153337575-16548464189554.png)]
    • 运行
      • RunXXX系列都是运行相关,即以什么方式运行服务、另外Handler也是
    • 扩展
      • SetHTMLTemplate:自定义模板解析引擎
      • SetFuncMap:模板解析方法
      • SetTrustedProxies:设置信任的代理的IP地址,默认都信任

RouteGroup和Handler

RouteGroup
  • 非常简单,就是配置各种路由,支持前缀路由(路由组)配置
image-20220610154102565
Handler
  • 就是一系列处理函数,类似Netty的Handler,自定义;
  • 在gin中,每个路由会绑定HandlerChain,管理handler

Context

  • 在go和go框架中,上下文是一个非常核心的概念,上下文具有线程安全和局部可见的特点,当然和原生Context有点不同

  • 上下文对象同样是gin的基础,在gin中context方法非常的多

    • //GET系列
      Get(key string) (value any, exists bool)
      MustGet(key string) any
      GetString(key string) (s string)
      GetBool(key string) (b bool)
      GetInt(key string) (i int)
      GetInt64(key string) (i64 int64)
      GetUint(key string) (ui uint)
      GetUint64(key string) (ui64 uint64)
      GetFloat64(key string) (f64 float64)
      GetTime(key string) (t time.Time)
      GetDuration(key string) (d time.Duration)
      GetStringSlice(key string) (ss []string)
      GetStringMap(key string) (sm map[string]any)
      GetStringMapString(key string) (sms map[string]string)
      GetStringMapStringSlice(key string) (smss map[string][]string)
      GetQuery(key string) (string, bool)
      GetQueryArray(key string) (values []string, ok bool)
      GetQueryMap(key string) (map[string]string, bool)
      GetHeader(key string) string
      GetRawData() ([]byte, error)
      
      
      //params
      Param(key string) string
      AddParam(key string, value string)
      
      
      
      //form
      PostForm(key string) (value string)
      DefaultPostForm(key string, defaultValue string) string
      GetPostForm(key string) (string, bool)
      PostFormArray(key string) (values []string)
      initFormCache()
      GetPostFormArray(key string) (values []string, ok bool)
      PostFormMap(key string) (dicts map[string]string)
      GetPostFormMap(key string) (map[string]string, bool)
      FormFile(name string) (*multipart.FileHeader, error)
      MultipartForm() (*multipart.Form, error)
      
      
      
      //获取cookie
      Cookie(name string) (string, error)
      
      
      //query查询是否存在
      Query(key string) (value string)
      DefaultQuery(key string, defaultValue string) string
      QueryArray(key string) (values []string)
      initQueryCache()
      QueryMap(key string) (dicts map[string]string)
      
      
      
      //handler处理器链
      reset()
      Copy() *Context
      HandlerName() string
      HandlerNames() []string
      Handler() HandlerFunc
      FullPath() string
      	//下一个
      Next()
      	//终止
      IsAborted() bool
      Abort()
      AbortWithStatus(code int)
      AbortWithStatusJSON(code int, jsonObj any)
      AbortWithError(code int, err error) *Error
      Error(err error) *Error
      Set(key string, value any)
      
      
      
      //参数继续引擎绑定,必须成功(BindXXXX),可以失败(ShouldBindXXX)
      Bind(obj any) error
      BindJSON(obj any) error
      BindXML(obj any) error
      BindQuery(obj any) error
      BindYAML(obj any) error
      BindTOML(obj interface{}) error
      BindHeader(obj any) error
      BindUri(obj any) error
      MustBindWith(obj any, b binding.Binding) error
      BindWith(obj any, b binding.Binding) error
      ShouldBind(obj any) error
      ShouldBindJSON(obj any) error
      ShouldBindXML(obj any) error
      ShouldBindQuery(obj any) error
      ShouldBindYAML(obj any) error
      ShouldBindTOML(obj interface{}) error
      ShouldBindHeader(obj any) error
      ShouldBindUri(obj any) error
      ShouldBindWith(obj any, b binding.Binding) error
      ShouldBindBodyWith(obj any, bb binding.BindingBody) (err error)
      
      
      
      //响应
      	//重定向
      Redirect(code int, location string)
      	//header、code、message、cookie
      Status(code int)
      Header(key string, value string)
      SetSameSite(samesite http.SameSite)
      SetCookie(name string, value string, maxAge int, path string, domain string, secure bool, httpOnly bool)
      Render(code int, r render.Render)
      	//data,将数据转为对应格式的data在写入resp
      HTML(code int, name string, obj any)
      IndentedJSON(code int, obj any)
      SecureJSON(code int, obj any)
      JSONP(code int, obj any)
      JSON(code int, obj any)
      AsciiJSON(code int, obj any)
      PureJSON(code int, obj any)
      XML(code int, obj any)
      YAML(code int, obj any)
      TOML(code int, obj interface{})
      ProtoBuf(code int, obj any)
      String(code int, format string, values ...any)
      DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)
      File(filepath string)
      FileFromFS(filepath string, fs http.FileSystem)
      FileAttachment(filepath string, filename string)
      Stream(step func(w io.Writer) bool) bool
      
      
      
      //信息
      ClientIP() string
      RemoteIP() string
      ContentType() string
      IsWebsocket() bool
      SSEvent(name string, message any)
      Negotiate(code int, config Negotiate)
      NegotiateFormat(offered ...string) string
      SetAccepted(formats ...string)
      SaveUploadedFile(file *multipart.FileHeader, dst string) error
      
      //go Context
      Deadline() (deadline time.Time, ok bool)
      Done() <-chan struct{}
      Err() error
      Value(key any) any
      
方法
  • 信息获取
    • Get系列
      • 可以指定类型获取参数、除了MustGet,其他在没有该参数不会有painc,而是返回false,可以通过GetQuery查询是否存在该参数
      • 获取信息
        • GetHeader:获取头部信息
        • GetBody:获取body的数据
    • Form系列
      • 获取表单数据
        • image-20220610160626059
    • Params:获取参数
    • 解析获取:即解析成指定的结构体/数据结构
      • 必须解析成功:BindXXX
      • 可以解析失败:ShouldBindXXX
  • 响应
    • 重定向
      • Redirect
    • 信息
      • SetCookie、Handler、SetStatus等
    • 返回的数据
      • HTML、JSON等
  • 信息和go的Context

使用

  • Engine:基本使用和iris没有很大的区别,即通过engine绑定host、路由、handler
  • Context:和iris的iris.Context一样,gin有gin.Context
  • gin支持自动解析(数据绑定),而不需要每个获取
  • gin封装了返回数据,可以使用框架的JSON格式
  • 支持路由组、restFul风格参数、最佳适配、静态资源和静态资源路由绑定
  • 支持中间件(和iris处理逻辑一样)
  • 支持模板引擎
  • 集成swagger-go
    • https://github.com/swaggo/swag/blob/master/README_zh-CN.md
//实例
package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
)

//结构体解析
type User struct {
	Name     string `json:"userName"`
	Password string `json:"password"`
	Other    string `json:"other"`
}


//统一封装返回
type Result struct {
	Code    int
	Message string
	Data    interface{}
}

func init() {

}

func login(c *gin.Context) {
	var user User
    //绑定JSON解析
	err := c.BindJSON(&user)
	if err != nil {
		return
	}
	fmt.Println(user)
	result := Result{
		Code:    200,
		Message: "SUCCESS",
		Data:    "ok",
	}
    //自动将JSON作为返回参数
	c.JSON(200, &result)
    //执行下一条语句
	c.Next()
}

func printHello(c *gin.Context) {
    //获取路径参数
	fmt.Println(c.Param("hello"))
	c.Next()
}

func printUser(c *gin.Context) {
	fmt.Println("user" + c.RemoteIP())
    c.Next()
}

func globalPrint(c *gin.Context) {
	fmt.Println("global")
	c.Next()
}

func main() {
	engine := gin.New()
    //绑定全局处理器
	engine.Use(globalPrint)
    //绑定静态资源
	engine.Static("/img", "./resource/static/img/*")
	//绑定全局Html前缀路径
    engine.LoadHTMLGlob("web/route/resource/web/*")
	engine.GET("/", func(context *gin.Context) {
        //html,通过模板引擎gin.H渲染  ---->//在index默认通过{{.name}}获取,可以自定义标识符替换{{}}
		context.HTML(200, "index.html", gin.H{
			"name": "lili",
		})
	})
    //绑定路由组
	group := engine.Group("/user", printUser)
    //:hello为restful风格的参数传递
	group.GET("/say/:hello", printHello)
	//表单提交
	engine.POST("/login", login)
    //运行
	err := engine.Run(":8888")
	if err != nil {
		fmt.Println("error")
	}
}

实现

preview

  • 很明显,gin框架就像SpringMVC一样,其实现主要包括
    • 路由注册和保存路由算法
    • 请求处理的过程
      • 执行的过程(调度的过程)
      • Request数据解析
      • Response数据绑定
      • 模板引擎渲染
    • 组件间的关系

启动

Engine

  • 实现了Http库的Handler接口,所以可以作为Handler直接使用http库
    • image-20220610170848159
type Engine struct {
	RouterGroup

	//是否自动重定向(即/a/,自动重定向为/a;默认为true
	RedirectTrailingSlash bool

	// 是否对书写(多余/、字母大小写不符)的自动重定向
	RedirectFixedPath bool

	// 是否对请求的方法不正确(405),通过NotMethod处理
	HandleMethodNotAllowed bool

	// 请求IP;默认为true
	ForwardedByClientIP bool


	// 开启路径参数
	UseRawPath bool
	// 路径参数转移;默认为true
	UnescapePathValues bool
	// 以从 URL 中解析参数,即使带有额外的斜杠。
	RemoveExtraSlash bool

	//允许的IP请求头
	RemoteIPHeaders []string
	// 信任的请求的平台
	TrustedPlatform string

	// 传入的参数最大占用空间
	MaxMultipartMemory int64

	//是否支持HTTP2
	UseH2C bool

	// Context是否可以回退
	ContextWithFallback bool

    //公共请求和响应设置(模板引擎、对象池、参数等)
	delims           render.Delims
	secureJSONPrefix string
	HTMLRender       render.HTMLRender
	FuncMap          template.FuncMap
	allNoRoute       HandlersChain
	allNoMethod      HandlersChain
	noRoute          HandlersChain
	noMethod         HandlersChain
	pool             sync.Pool
	trees            methodTrees
	maxParams        uint16
	maxSections      uint16
	trustedProxies   []string
	trustedCIDRs     []*net.IPNet
}

过程

New方法

  • 无论是通过Default还是New获取Engine都会调用New方法
    • Default绑定了默认的日志和全局异常处理
  • New方法
    • 绑定默认的Engine参数
    • 注入根RouteGroup

Run方法

  • 就是调用内置的http开启监听:http.ListenAndServe
func (engine *Engine) Run(addr ...string) (err error) {
	address := resolveAddress(addr)
    //注入engine作为http的handler,即作为监听的端口的所有请求的handler;
    	//engine.Handler()方法就是返回http1.x或者http2.0的engine
	err = http.ListenAndServe(address, engine.Handler())
}
  • 到这里启动基本完成,
    • 下面介绍启动前注入路由
    • 请求到达后的处理

路由

压缩前缀树

  • 压缩前缀树就是:通过合并节点实现的前缀树
    • 前缀树:即字典树,一般只需要:final、value、children数组/map(以及全量单词),显然前缀树的增删改查都是logn
    • 合并节点:将以当前节点为根的子树中所有节点都是只有一个节点的子节点合并为一个节点

路由表的压缩前缀树

//压缩前缀树节点
type node struct {
	path      string	//value
	indices   string	//节点与子节点的分裂的第一个字符
	wildChild bool		//是否为参数节点
	nType     nodeType	//节点类型:根节点、参数节点、通配符、普通节点
	priority  uint32	//handler数量
	children  []*node 	//子节点
	handlers  HandlersChain	//处理器链
	fullPath  string	//全量路径
}

//封装的获取到压缩前缀书查询结果
type nodeValue struct {
	handlers HandlersChain
	params   *Params
	tsr      bool
	fullPath string
}

路由生成

  • 在gin中使用动态路由策略,所以实现路由没有使用hash表,而是压缩前缀树
  • 路由生成主要包括
    • 路由路径生成:即将可能的前缀路由合并到当前路由
    • 处理器链:将当前路由的handler加入到处理器链
    • 加入路由表
      • 获取方法的根路由
      • 压缩前缀书方法加入路由到路由表
//生成路由
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	//路由路径(加入前缀)
    absolutePath := group.calculateAbsolutePath(relativePath)
	//处理器链
    handlers = group.combineHandlers(handlers)
	//加入路由表
    group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}


//获取根路由
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
	//根据方法类型获取根路径
	root := engine.trees.get(method)
	if root == nil {
		root = new(node)
		root.fullPath = "/"
		engine.trees = append(engine.trees, methodTree{method: method, root: root})
	}
    //加入路由表
	root.addRoute(path, handlers)
}

//添加路由路径
func (n *node) addRoute(path string, handlers HandlersChain) {
	fullPath := path
	n.priority++

	// 空树,直接使用压缩前缀即可
	if len(n.path) == 0 && len(n.children) == 0 {
		n.insertChild(path, fullPath, handlers)
		n.nType = root
		return
	}

	parentFullPathIndex := 0

walk:
	for {
		//最长公共路径(排除通配符和参数匹配)
		i := longestCommonPrefix(path, n.path)

		//生成节点
		if i < len(n.path) {
			child := node{
				//...
			}
            //修改n
             n.children = []*node{&child}
			// []byte for proper unicode char conversion, see #65
			n.indices = bytesconv.BytesToString([]byte{n.path[i]})
			n.path = path[:i]
			n.handlers = nil
			n.wildChild = false
			n.fullPath = fullPath[:parentFullPathIndex+i]
		}

		// 去除参数、通配符、多余/
		if i < len(path) {
			path = path[i:]
			c := path[0]

			// '/' after param
			if n.nType == param && c == '/' && len(n.children) == 1 {
				parentFullPathIndex += len(n.path)
				n = n.children[0]
				n.priority++
				continue walk
			}

			// Check if a child with the next path byte exists
			for i, max := 0, len(n.indices); i < max; i++ {
				if c == n.indices[i] {
					parentFullPathIndex += len(n.path)
					i = n.incrementChildPrio(i)
					n = n.children[i]
					continue walk
				}
			}
			if c != ':' && c != '*' && n.nType != catchAll {
				n.indices += bytesconv.BytesToString([]byte{c})
				child := &node{
					fullPath: fullPath,
				}
				n.addChild(child)
				n.incrementChildPrio(len(n.indices) - 1)
				n = child
			} 
            //插入子节点
			n.insertChild(path, fullPath, handlers)
			return
		}

		n.handlers = handlers
		n.fullPath = fullPath
		return
	}
}

路由查找

  • 路由查找非常简单
    • 找到压缩前缀树根节点
    • 找到相应的路由节点

请求处理

  • gin使用了原生的http库,实现了其中handler的ServeHTTP方法,通过handleHTTPRequest统一处理请求
  • gin使用了对象池技术,减少了对象的创建和释放
  • **handleHTTPRequest**主要就是根据请求方法+路径确定处理器链和参数、然后调用处理器链进行处理
    • 根据需要:对于无路由或者请求方法不正确进行处理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/422309.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GC 垃圾回收算法、垃圾回收器及 JVM 调优【JVM知识点-resu】

JVM知识点 详情请见&#xff1a;垃圾回收算法、垃圾收集器详情请见&#xff1a;JVM调优 1 GC垃圾回收算法 众所周知&#xff0c;Java的内存管理是交由了JVM&#xff0c;那么程序时时刻刻都在产生新对象&#xff0c;为了避免内存溢出&#xff0c;此时必然会涉及到垃圾回收&…

【MySQL数据库原理】Python3.7 中连接 MySQL 数据库

目录 1、安装mysql-connector-python2、连接 MySQL 数据库3、修改数据库1、安装mysql-connector-python 要在 Python 中连接 MySQL 数据库 “test”,可以使用 “mysql-connector-python” 包。首先,确保已经安装了该包。可以使用 pip 命令进行安装: pip install mysql-con…

[abc复盘] abc297 20230409

[atc复盘] abc297 20230409 一、本周周赛总结A - Double Click1. 题目描述2. 思路分析3. 代码实现B - chess9601. 题目描述2. 思路分析3. 代码实现C - PC on the Table1. 题目描述2. 思路分析3. 代码实现D - Count Subtractions1. 题目描述2. 思路分析3. 代码实现E - Kth Takoy…

Spring 04 -SpringAOP开发

SpringAOP开发SpringAOP1 原理2 动态代理2.1 JDK动态代理2.2.2 Cglib动态代理2.2.3 **JDK动态代理和Cglib动态代理**3 SpringAOP3.1 AOP专业术语3.2 环境搭建3.3 基于XML配置3.4 基于注解配置2.5 通知类型面向切面编程&#xff0c;在不修改源代码的情况加&#xff0c;对类功能实…

人工智能中的顶级会议

当搭建好了AI领域的知识架构&#xff0c;即具备了较好的数学、编程及专业领域知识后&#xff0c;如果想在AI领域追踪前沿研究&#xff0c;就不能再只看教材了。毕竟AI领域的发展一日千里&#xff0c;教材上的知识肯定不是最新的。此时&#xff0c;应该将关注的重点转向AI领域的…

JavaWeb - Web网站的组成,工作流程以及开发模式

一. Web Web&#xff1a;全球广域网&#xff0c;也称玩万维网(www Wrold Wide Web)&#xff0c;就是能够通过浏览器访问的网站学习Web开发&#xff0c;其实就是要使用Java这门语言来开发这样的Web网站&#xff0c;这也是现在Java语言最主流的企业级应用方式。使用Java语言开发…

Nginx基本配置 Nginx服务基础Nginx访问控制Nginx虚拟主机

本章结构 Nginx服务基础 Nginx访问控制 Nginx虚拟主机 原理&#xff1a; 关于Nginx 一款高性能、轻量级Web服务软件 稳定性高 系统资源消耗低 apache多线程或多进程实现连接&#xff08;多线程比多线程稍微好些&#xff0c;切换资源浪费少&#xff09; Nginx单线程 对HTTP并发…

自己动手写CPU——第二篇

1 ori指令说明 ori是进行逻辑 或 运算的指令&#xff0c;其指令格式如下所示 从以上的指令格式&#xff0c;我们可以直到&#xff0c;这是一个I类型指令&#xff0c;ori指令的指令码是 6‘b001101&#xff0c;所以当处理器发现正在处理的指令的高6bit 是 001101 的时候&#x…

vue3 css相关知识与动态style

scoped 当 <style> 标签带有 scoped attribute 的时候&#xff0c;它的 CSS 只会影响当前组件的元素&#xff0c;和 Shadow DOM 中的样式封装类似。 <style scoped lang"scss"> </style> 注意 &#xff1a; 作用域样式并没有消除对 class 的需求…

DN-DETR源码学习记录

DN-DETR是在DAB-DETR的基础上完成的&#xff0c;DN-DETR的作者认为导致DETR类模型收敛慢的原因在于匈牙利匹配所导致的二义性&#xff0c;即匈牙利算法匹配的离散性和模型训练的随机性&#xff0c;导致ground-truth的匹配变成了一个动态的、不稳定的过程。举个例子&#xff0c;…

字节青训营(前端)打卡day01_计网

计算机网络概论 1.前沿和课程介绍 自顶向下、自底向上 网络分层、网络协议、网络应用、HTTP123、CDN GPT. 计算机网络的发展历程可以分为四个阶段&#xff1a;单机、局域网、广域网、互联网 2.蟹堡王帝国 顾客&#xff1a;客户端 分店&#xff1a;服务端 小区转发点&…

【FFmpeg】编码器流程分析

目录1 编译2 调用关系2.1 第一帧没有获取到码流&#xff0c;第二帧获取到一帧码流2.2 送帧为NULL&#xff0c;刷新编码器获取剩余的全部码流3 总结4 感受1 编译 参考文件doc/examples/encode_video.c&#xff0c;使用x264作为编码器&#xff0c;需要先安装x264&#xff0c;编译…

如何用ChatGPT写毕业论文?

老早之前就听说有同学用ChatGPT写论文了 但是一直不觉得人工智能够真的替代人类 正好最近毕业论文开始降重了&#xff0c;超高的重复率愁得我快睡不着了 刷微博突然看到这个有关ChatGPT的问题。 出于好奇的我决定先来试试这个ChatGPT到底能不能帮我解决降重问题哈哈哈 点击…

shell脚本基础之处理脚本输入

处理输入命令行参数读取参数读取脚本名测试参数特殊参数变量参数统计获取所有参数移动变量处理选项查找选项处理简单选项分离选项和参数处理带值的选项getopt 命令命令格式在脚本中使用getoptgetopts命令脚本选项标准化获取用户的输入基本的读取超时隐藏式读取文件中读取在此之…

Linux命令·ifconfig

许多windows非常熟悉ipconfig命令行工具&#xff0c;它被用来获取网络接口配置信息并对此进行修改。Linux系统拥有一个类似的工具&#xff0c;也就是ifconfig(interfaces config)。通常需要以root身份登录或使用sudo以便在Linux机器上使用ifconfig工具。依赖于ifconfig命令中使…

OpenFeign#1 - FeignClient 是如何注册的?

文章目录EnableFeignClientsFeignClientsRegistrarregisterDefaultConfigurationregisterFeignClientsFeignClientFeignClientFactoryBeanFeignContextfeign(FeignContext)EnableFeignClients 该注解会导致 FeignClientsRegistrar 的注入. Retention(RetentionPolicy.RUNTIME…

Redis AOF

一、Redis AOF 1.简介 目前&#xff0c;redis的持久化主要应用AOF(Append Only File)和RDB两大机制。AOF以日志的形式来记录每个写操作(增量保存),将redis执行过的所有写指令全部记录下来(读操作不记录)。只许追加文件&#xff0c;但不可以改写文件。redis启动之初会读取该文…

自动化测试学习(七)-正则表达式,你真的会用吗?

目录 一、正则表达式在python中如何使用 二、用正则表达式匹配更多模式 三、常用字符分类的缩写代码 总结 所谓正则表达式&#xff08;regex&#xff09;&#xff0c;就是一种模式匹配&#xff0c;学会用正则匹配&#xff0c;就可以达到事半功倍的效果。 一、正则表达式在…

工程管理系统软件 自主研发,工程行业适用

Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下&#xff1a; 首页 工作台&#xff1a;待办工作、消息通知、预警信息&#xff0c;点击可进入相应的列表 项目进度图表&#xff1a;选择&#xff08;总体或单个&#xff09;项目显示…

在 Web3 里如何寻找靠谱的创新路径——Solv V3 阶段性复盘(上)

作者&#xff1a;Solv 核心团队Solv V3 是 3 月 21 日发布的&#xff0c;到这周一整整三个星期。三周时间产生的实际交易额超过 6,000 万美元&#xff0c;预计在本月内能够破亿。而且从我们手上的 pipeline 来看&#xff0c;这个增长的势头还将持续下去。在几个月内&#xff0c…