Go 微服务框架 | 路由实现
文章目录
- 不用框架实现web接口
- 实现简单的路由
- 实现分组路由
- 支持不同的请求方式
- 支持同一个路径的不同请求方式
- 前缀树
- 应用前缀树
- 完善路由代码
不用框架实现web接口
// blog main.go 文件
package mainimport ("fmt""log""net/http"
)func main() {fmt.Println("Hello World!")// 注册 HTTP 路由 /hellohttp.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello Go!")})// 启动 HTTP 服务器err := http.ListenAndServe("8111", nil)if err != nil {log.Fatal(err)}
}
http.HandleFunc
注册一个路由/hello
并绑定处理函数。- 当用户访问
http://localhost:8111/hello
时,执行匿名函数:w http.ResponseWriter
:用于向客户端发送 HTTP 响应。r *http.Request
:包含客户端发来的请求信息(如 URL、Headers 等)。
fmt.Fprintf(w, "Hello Go!")
:- 向 HTTP 响应写入
"Hello Go!"
(相当于w.Write([]byte("Hello Go!"))
)。 - 默认状态码是
200 OK
。
- 向 HTTP 响应写入
http.ListenAndServe(":8111", nil)
:- 启动 HTTP 服务器,监听
8111
端口(: 表示监听所有网络接口)。 nil
表示使用默认的DefaultServeMux
路由器(即之前用http.HandleFunc
注册的路由)。
- 启动 HTTP 服务器,监听
if err != nil { log.Fatal(err) }
:如果服务器启动失败(如端口被占用),打印错误并终止程序。fmt.Fprintf
是 Go 语言fmt
包提供的一个格式化输出函数,用于将格式化后的字符串写入指定的io.Writer
接口(如文件、HTTP 响应、标准输出等)。它的作用类似于fmt.Printf
,但不是输出到终端,而是写入到任意实现了io.Writer
的对象。
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
w io.Writer
:目标写入器(如http.ResponseWriter
、文件、缓冲区等)。format string
:格式化字符串(包含占位符,如%s
,%d
,%v
等)。a ...interface{}
:可变参数,用于填充格式化字符串中的占位符。- 返回值:
n int
:成功写入的字节数。err error
:写入过程中遇到的错误(如写入失败)。
实现简单的路由
// zjgo ms.go 文件package zjgoimport ("log""net/http"
)type Engine struct {
}func New() *Engine {return &Engine{}
}func (e *Engine) Run() {err := http.ListenAndServe("8111", nil)if err != nil {log.Fatal(err)}
}
- 经过封装之后,原来的
main
函数可以简洁为如下:
package mainimport ("fmt""github.com/ErizJ/ZJGo/zjgo"
)func main() {fmt.Println("Hello World!")// // 注册 HTTP 路由 /hello// http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {// fmt.Fprintf(w, "Hello Go!")// })// // 启动 HTTP 服务器// err := http.ListenAndServe("8111", nil)// if err != nil {// log.Fatal(err)// }engine := zjgo.New()engine.Run()
}
- 注意这里服务启动后会
404 Not Found
,因为我们没有实现对应的响应函数Handler
。
package zjgoimport ("log""net/http"
)// 定义处理响应函数
type HandleFunc func(w http.ResponseWriter, r *http.Request)// 定义路由结构体
type router struct {handleFuncMap map[string]HandleFunc
}// 给路由结构体添加一个添加路由功能的函数
func (r *router) Add(name string, handleFunc HandleFunc) {r.handleFuncMap[name] = handleFunc
}// 定义一个引擎结构体
type Engine struct {router
}// 引擎结构体的初始化方法
func New() *Engine {return &Engine{router: router{handleFuncMap: make(map[string]HandleFunc),},}
}// 引擎的启动方法
func (e *Engine) Run() {for key, value := range e.handleFuncMap {http.HandleFunc(key, value)}err := http.ListenAndServe("8111", nil)if err != nil {log.Fatal(err)}
}
- 在 Go 语言中,当你将一个类型(如
router
)嵌入到另一个结构体(如Engine
)中时,这被称为类型嵌入(Embedded Type
)。这是一种组合的方式,它允许Engine
直接访问router
的字段和方法,而不需要显式地通过一个字段名来访问。
package mainimport ("fmt""net/http""github.com/ErizJ/ZJGo/zjgo"
)func main() {fmt.Println("Hello World!")// // 注册 HTTP 路由 /hello// http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {// fmt.Fprintf(w, "Hello Go!")// })// // 启动 HTTP 服务器// err := http.ListenAndServe("8111", nil)// if err != nil {// log.Fatal(err)// }engine := zjgo.New()engine.Add("/hello", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello Go!")})engine.Run()
}
- 这样我们就实现了一个简单的路由功能,下面进行进一步完善。
实现分组路由
- 大多数情况下我们希望写的接口归属于某一个模块,这样便于管理以及维护,代码也会更为清晰。
- 例如:
/user/getUser
和/user/createUser
都同属于user
模块。
// zj.gopackage zjgoimport ("log""net/http"
)// 定义处理响应函数
type HandleFunc func(w http.ResponseWriter, r *http.Request)// 抽象出路由的概念
type routerGroup struct {name string // 组名handleFuncMap map[string]HandleFunc // 映射关系应该由每个路由组去维护
}// 定义路由结构体
type router struct {routerGroups []*routerGroup // 路由下面应该维护着不同的组
}// 添加路由组
func (r *router) Group(name string) *routerGroup {routerGroup := &routerGroup{name: name,handleFuncMap: make(map[string]HandleFunc),}r.routerGroups = append(r.routerGroups, routerGroup)return routerGroup
}// 给路由结构体添加一个添加路由功能的函数
func (routerGroup *routerGroup) Add(name string, handleFunc HandleFunc) {routerGroup.handleFuncMap[name] = handleFunc
}// 定义一个引擎结构体
type Engine struct {router
}// 引擎结构体的初始化方法
func New() *Engine {return &Engine{router: router{},}
}// 引擎的启动方法
func (e *Engine) Run() {for _, group := range e.routerGroups {for name, value := range group.handleFuncMap {http.HandleFunc("/"+group.name+name, value)}}err := http.ListenAndServe(":3986", nil)if err != nil {log.Fatal(err)}
}
// main.gopackage mainimport ("fmt""net/http""github.com/ErizJ/ZJGo/zjgo"
)func main() {fmt.Println("Hello World!")// // 注册 HTTP 路由 /hello// http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {// fmt.Fprintf(w, "Hello Go!")// })// // 启动 HTTP 服务器// err := http.ListenAndServe("8111", nil)// if err != nil {// log.Fatal(err)// }engine := zjgo.New()g1 := engine.Group("user")g1.Add("/hello", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello Go!——user/hello")})g1.Add("/info", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello Go!——user/info")})g2 := engine.Group("order")g2.Add("/hello", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello Go!——order/hello")})g2.Add("/info", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, "Hello Go!——order/info")})fmt.Println("Starting...")engine.Run()}
支持不同的请求方式
net / http
下的路由,只要路径匹配,就可以进入处理方法。- 但是在我们实际应用之中,比如我们使用 Restful 风格的接口在同一路径下,会使用
GET
、POST
、DELETE
、PUT
来代替增删改查,所以我们要对不同的请求方式做相应的支持。
// zj.gopackage zjgoimport ("fmt""log""net/http"
)// 定义处理响应函数
type HandleFunc func(w http.ResponseWriter, r *http.Request)// 抽象出路由的概念
type routerGroup struct {name string // 组名handleFuncMap map[string]HandleFunc // 映射关系应该由每个路由组去维护handlerMethodMap map[string][]string // 记录GET,POST等请求方式所记录的路由,实现对同一路由不同请求方式的支持
}// 定义路由结构体
type router struct {routerGroups []*routerGroup // 路由下面应该维护着不同的组
}// 添加路由组
func (r *router) Group(name string) *routerGroup {routerGroup := &routerGroup{name: name,handleFuncMap: make(map[string]HandleFunc),handlerMethodMap: make(map[string][]string),}r.routerGroups = append(r.routerGroups, routerGroup)return routerGroup
}// 给路由结构体添加一个添加路由功能的函数
// func (routerGroup *routerGroup) Add(name string, handleFunc HandleFunc) {
// routerGroup.handleFuncMap[name] = handleFunc
// }// Any代表支持任意的请求方式
func (routerGroup *routerGroup) Any(name string, handleFunc HandleFunc) {routerGroup.handleFuncMap[name] = handleFuncrouterGroup.handlerMethodMap["ANY"] = append(routerGroup.handlerMethodMap["ANY"], name)
}// POST代表支持POST请求方式
func (routerGroup *routerGroup) Post(name string, handleFunc HandleFunc) {routerGroup.handleFuncMap[name] = handleFuncrouterGroup.handlerMethodMap[http.MethodPost] = append(routerGroup.handlerMethodMap[http.MethodPost], name)
}// GET代表支持GET请求方式
func (routerGroup *routerGroup) Get(name string, handleFunc HandleFunc) {routerGroup.handleFuncMap[name] = handleFuncrouterGroup.handlerMethodMap[http.MethodGet] = append(routerGroup.handlerMethodMap[http.MethodGet], name)
}// 只要实现 ServeHTTP 这个方法,就相当于实现了对应的 HTTP 处理器
// 结构体 Engine 实现了 ServeHTTP(w http.ResponseWriter, r *http.Request) 方法
// 所以它就自动实现了 http.Handler 接口,因此可以直接被用于 http.ListenAndServe
// Engine 实现了 ServeHTTP,它就是一个合法的 http.Handler,可以用 http.Handle 来绑定它到某个具体的路由路径上!
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {// 在 Go 的 net/http 包中,r *http.Request 代表了客户端发来的 HTTP 请求对象。// 可以通过 r.Method 来获取这次请求使用的是什么方法(Method),例如:GET、POST、PUT、DELETE 等。if r.Method == http.MethodGet {fmt.Fprintf(w, "这是一个 GET 请求")} else if r.Method == http.MethodPost {fmt.Fprintf(w, "这是一个 POST 请求")} else {fmt.Fprintf(w, "这是一个其他类型的请求:%s", r.Method)}for _, group := range e.routerGroups {for name, methodHandle := range group.handleFuncMap {url := "/" + group.name + name//判断一下当前的url是否等于请求的url,即路由匹配if r.RequestURI == url {// 先判断当前请求路由是否在支持任意请求方式的 Any map里面if routers, exist := group.handlerMethodMap["ANY"]; exist {for _, routerName := range routers {// 确实支持 Any 请求方式if routerName == name {methodHandle(w, r)return}}}// 不支持 Any,去该请求所对应的请求方式对应的 Map 里面去找是否有对应的路由if routers, exist := group.handlerMethodMap[r.Method]; exist {for _, routerName := range routers {// 确实支持对应请求方式if routerName == name {methodHandle(w, r)return}}}// 没找到对应的路由,说明该请求方式不允许w.WriteHeader(http.StatusMethodNotAllowed)fmt.Fprintf(w, "%s %s not allowed!!!\n", r.Method, r.RequestURI)return}}}w.WriteHeader(http.StatusNotFound)fmt.Fprintf(w, "%s %s not found!!!\n", r.Method, r.RequestURI)return
}// 定义一个引擎结构体
type Engine struct {router
}// 引擎结构体的初始化方法
func New() *Engine {return &Engine{router: router{},}
}// 引擎的启动方法
func (e *Engine) Run() {// for _, group := range e.routerGroups {// for name, value := range group.handleFuncMap {// http.HandleFunc("/"+group.name+name, value)// }// }// 把 e 这个http处理器绑定到对应路由下http.Handle("/", e)err := http.ListenAndServe(":3986", nil)if err != nil {log.Fatal(err)}
}
// main.gopackage mainimport ("fmt""net/http""github.com/ErizJ/ZJGo/zjgo"
)func main() {fmt.Println("Hello World!")// // 注册 HTTP 路由 /hello// http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {// fmt.Fprintf(w, "Hello Go!")// })// // 启动 HTTP 服务器// err := http.ListenAndServe("8111", nil)// if err != nil {// log.Fatal(err)// }engine := zjgo.New()g1 := engine.Group("user")g1.Get("/hello", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, http.MethodGet+" Hello Go!——user/hello")})// 浏览器地址栏输入的都是 GET 请求// 需要用 curl 或 Postman 来发一个真正的 POST 请求,才会命中 Post handlerg1.Post("/info", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, http.MethodPost+" Hello Go!——user/info")})// 只要路由匹配,就会执行对应的处理函数g1.Any("/any", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintf(w, " Hello Go!——user/any")})// g2 := engine.Group("order")// g2.Add("/hello", func(w http.ResponseWriter, r *http.Request) {// fmt.Fprintf(w, "Hello Go!——order/hello")// })// g2.Add("/info", func(w http.ResponseWriter, r *http.Request) {// fmt.Fprintf(w, "Hello Go!——order/info")// })fmt.Println("Starting...")engine.Run()
}
- 但是目前还是存在一些问题的,目前不支持同一个路由进行
GET
和POST
,因为在 map 里面会被覆盖。 routerGroup.handleFuncMap[name] = handleFunc
,在Post
和Get
方法下都有这段代码,这就会造成方法的覆盖。
支持同一个路径的不同请求方式
- 标准库 net/http 本身只提供了最基础的路由匹配机制,也就是通过:
http.HandleFunc("/path", handler)
,它的匹配机制非常简单:只根据请求路径匹配,不区分请求方法(GET/POST
)。 - 如果在方法中这样写,是手动在
handler
里面区分请求方法,net/http
本身并不会替你区分。
http.HandleFunc("/info", func(w http.ResponseWriter, r *http.Request) {if r.Method == http.MethodPost {// 处理 POST} else if r.Method == http.MethodGet {// 处理 GET} else {http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)}
})
- 要限制不同请求方法(
GET
、POST
分别调用不同的handler
),就得框架(或者你自己)在ServeHTTP
里手动实现。 - 像 Gin、Echo、Fiber 等这些 Web 框架,都是在它们内部封装了:
- 请求方法和路径的双重匹配机制
- 路由注册表,支持多种请求方式绑定不同
handler
- 匹配失败时返回
405 Method Not Allowed
或404 Not Found
- 考虑在每个路由组
routerGroup
的handleFuncMap
上做文章,原先是map[name]HandleFunc
,现在可以加入map
中map
,也即map[name]map[method]HandleFunc
。
// context.gopackage zjgoimport "net/http"// context 用于保存上下文的信息,用于传递信息
type Context struct {W http.ResponseWriterR *http.Request
}
// zj.gopackage zjgoimport ("fmt""log""net/http"
)const ANY = "ANY"// 定义处理响应函数
type HandleFunc func(ctx *Context)// 抽象出路由的概念
type routerGroup struct {name string // 组名handleFuncMap map[string]map[string]HandleFunc // 映射关系应该由每个路由组去维护handlerMethodMap map[string][]string // 记录GET,POST等请求方式所记录的路由,实现对同一路由不同请求方式的支持
}// 定义路由结构体
type router struct {routerGroups []*routerGroup // 路由下面应该维护着不同的组
}// 添加路由组
func (r *router) Group(name string) *routerGroup {routerGroup := &routerGroup{name: name,handleFuncMap: make(map[string]map[string]HandleFunc),handlerMethodMap: make(map[string][]string),}r.routerGroups = append(r.routerGroups, routerGroup)return routerGroup
}// 给路由结构体添加一个添加路由功能的函数
// func (routerGroup *routerGroup) Add(name string, handleFunc HandleFunc) {
// routerGroup.handleFuncMap[name] = handleFunc
// }// 由于 ANY POST GET 都需要重复相同的逻辑代码,所以做一个提取操作
func (routerGroup *routerGroup) handleRequest(name string, method string, handleFunc HandleFunc) {if _, exist := routerGroup.handleFuncMap[name]; !exist {routerGroup.handleFuncMap[name] = make(map[string]HandleFunc)}if _, exist := routerGroup.handleFuncMap[name][method]; !exist {routerGroup.handleFuncMap[name][method] = handleFuncrouterGroup.handlerMethodMap[method] = append(routerGroup.handlerMethodMap[method], name)} else {panic("Under the same route, duplication is not allowed!!!")}
}// Any代表支持任意的请求方式
func (routerGroup *routerGroup) Any(name string, handleFunc HandleFunc) {routerGroup.handleRequest(name, ANY, handleFunc)
}// POST代表支持POST请求方式
func (routerGroup *routerGroup) Post(name string, handleFunc HandleFunc) {routerGroup.handleRequest(name, http.MethodPost, handleFunc)
}// GET代表支持GET请求方式
func (routerGroup *routerGroup) Get(name string, handleFunc HandleFunc) {routerGroup.handleRequest(name, http.MethodGet, handleFunc)
}// 只要实现 ServeHTTP 这个方法,就相当于实现了对应的 HTTP 处理器
// 结构体 Engine 实现了 ServeHTTP(w http.ResponseWriter, r *http.Request) 方法
// 所以它就自动实现了 http.Handler 接口,因此可以直接被用于 http.ListenAndServe
// Engine 实现了 ServeHTTP,它就是一个合法的 http.Handler,可以用 http.Handle 来绑定它到某个具体的路由路径上!
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {// 在 Go 的 net/http 包中,r *http.Request 代表了客户端发来的 HTTP 请求对象。// 可以通过 r.Method 来获取这次请求使用的是什么方法(Method),例如:GET、POST、PUT、DELETE 等。if r.Method == http.MethodGet {fmt.Fprintf(w, "这是一个 GET 请求!!!")} else if r.Method == http.MethodPost {fmt.Fprintf(w, "这是一个 POST 请求!!!")} else {fmt.Fprintf(w, "这是一个其他类型的请求:%s", r.Method)}for _, group := range e.routerGroups {for name, methodHandleMap := range group.handleFuncMap {url := "/" + group.name + name//判断一下当前的url是否等于请求的url,即路由匹配if r.RequestURI == url {ctx := &Context{W: w, R: r}// 先判断当前请求路由是否支持任意请求方式的Anyif handle, exist := methodHandleMap[ANY]; exist {handle(ctx)return}// 不支持 Any,去该请求所对应的请求方式对应的 Map 里面去找是否有对应的路由if handle, exist := methodHandleMap[r.Method]; exist {handle(ctx)return}// 没找到对应的路由,说明该请求方式不允许w.WriteHeader(http.StatusMethodNotAllowed)fmt.Fprintf(w, "%s %s not allowed!!!\n", r.Method, r.RequestURI)return}}}w.WriteHeader(http.StatusNotFound)fmt.Fprintf(w, "%s %s not found!!!\n", r.Method, r.RequestURI)return
}// 定义一个引擎结构体
type Engine struct {router
}// 引擎结构体的初始化方法
func New() *Engine {return &Engine{router: router{},}
}// 引擎的启动方法
func (e *Engine) Run() {// for _, group := range e.routerGroups {// for name, value := range group.handleFuncMap {// http.HandleFunc("/"+group.name+name, value)// }// }// 把 e 这个http处理器绑定到对应路由下http.Handle("/", e)err := http.ListenAndServe(":3986", nil)if err != nil {log.Fatal(err)}
}
// main.gopackage mainimport ("fmt""net/http""github.com/ErizJ/ZJGo/zjgo"
)func main() {fmt.Println("Hello World!")// // 注册 HTTP 路由 /hello// http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {// fmt.Fprintf(w, "Hello Go!")// })// // 启动 HTTP 服务器// err := http.ListenAndServe("8111", nil)// if err != nil {// log.Fatal(err)// }engine := zjgo.New()g1 := engine.Group("user")g1.Get("/hello", func(ctx *zjgo.Context) {fmt.Fprintf(ctx.W, http.MethodGet+" Hello Go!——user/hello")})// 浏览器地址栏输入的都是 GET 请求// 需要用 curl 或 Postman 来发一个真正的 POST 请求,才会命中 Post handlerg1.Post("/info", func(ctx *zjgo.Context) {fmt.Fprintf(ctx.W, http.MethodPost+" Hello Go!——user/info——POST")})g1.Get("/info", func(ctx *zjgo.Context) {fmt.Fprintf(ctx.W, http.MethodGet+" Hello Go!——user/info——GET")})// 只要路由匹配,就会执行对应的处理函数g1.Any("/any", func(ctx *zjgo.Context) {fmt.Fprintf(ctx.W, " Hello Go!——user/any")})// g2 := engine.Group("order")// g2.Add("/hello", func(w http.ResponseWriter, r *http.Request) {// fmt.Fprintf(w, "Hello Go!——order/hello")// })// g2.Add("/info", func(w http.ResponseWriter, r *http.Request) {// fmt.Fprintf(w, "Hello Go!——order/info")// })fmt.Println("Starting...")engine.Run()}
前缀树
- 前面的实现,我们只是实现了静态路由,不能实现更为复杂的需求,比如
/user/get/:id
这种才有参数的。 - 带有参数的路由路径,成为动态路由。
- 除了带有参数的,一般情况下我们还希望有更多支持,比如希望支持通配符
**
,比如/static/**
,可以匹配/static/vue.js
,/static/css/index.css
这些。
// 简单的前缀树实现代码思路type Trie struct {next [26]*Trieend bool
}func Constructor() Trie {myTrie := Trie{}return myTrie
}func (this *Trie) Insert(word string) {if this.Search(word) {return}node := thisfor _, ch := range word {if node.next[ch-'a'] == nil {node.next[ch-'a'] = &Trie{}}node = node.next[ch-'a']}node.end = true
}func (this *Trie) Search(word string) bool {node := this.search(word)return node != nil && (*node).end
}func (this *Trie) search(word string) *Trie {node := thisfor _, ch := range word {if node.next[ch-'a'] == nil {return nil}node = node.next[ch-'a']}return node
}func (this *Trie) StartsWith(prefix string) bool {node := this.search(prefix)return node != nil
}
// tree.gopackage zjgoimport "strings"type TreeNode struct {name string // 用 '/' 分割路径后的每个路由名称children []*TreeNode // 子节点
}// Put path --> /user/get/:id
// PUT 一般用于“更新”
// :id 是一种路由参数(Path Parameter)占位符
// /user/get/123 会匹配 /user/get/:id,并提取出 id = 123
// /user/get/abc 会匹配 /user/get/:id,提取出 id = abc
func (t *TreeNode) Put(path string) {root := tstrs := strings.Split(path, "/")for index, name := range strs {// 忽略第一个斜杠前的那个空格if index == 0 {continue}isMatch := falsefor _, node := range t.children {if node.name == name {isMatch = truet = nodebreak}}if !isMatch {node := &TreeNode{name: name,children: make([]*TreeNode, 0),}t.children = append(t.children, node)t = node}}t = root
}// Get path --> /user/get/1
// GET 一般用于“查”
func (t *TreeNode) Get(path string) *TreeNode {strs := strings.Split(path, "/")for index, name := range strs {// 忽略第一个斜杠前的那个空格if index == 0 {continue}isMatch := falsefor _, node := range t.children {if node.name == name || node.name == "*" || strings.Contains(node.name, ":") {isMatch = truet = nodeif index == len(strs)-1 {return node}break}}if !isMatch {for _, node := range t.children {// /user/**// /user/get/userInfo// /user/aa/bbif node.name == "**" {// only onereturn node}}}}return nil
}// test_tree.go
package zjgoimport ("fmt""testing"
)func TestTreeNode(t *testing.T) {root := &TreeNode{name: "/", children: make([]*TreeNode, 0)}root.Put("/user/get/:id")root.Put("/user/info/hello")root.Put("/user/create/aaa")root.Put("/order/get/aaa")node := root.Get("/user/get/1")fmt.Println(node)node = root.Get("/user/info/hello")fmt.Println(node)node = root.Get("/user/create/aaa")fmt.Println(node)node = root.Get("/order/get/aaa")fmt.Println(node)
}=== RUN TestTreeNode
&{:id []}
&{hello []}
&{aaa []}
&{aaa []}
--- PASS: TestTreeNode (0.00s)
PASS
ok github.com/ErizJ/ZJGo/zjgo (cached)
应用前缀树
// utils.gopackage zjgoimport "strings"// 分割字符串,只留下去掉了路由组名字后的字路由的路径
func SubStringLast(name string, groupName string) string {if index := strings.Index(name, groupName); index < 0 {return ""} else {return name[index+len(groupName):]}
}
// tree.gopackage zjgoimport "strings"type TreeNode struct {name string // 用 '/' 分割路径后的每个路由名称children []*TreeNode // 子节点routerName string // 前缀树到当前节点所走过的路径isEnd bool // 对bug的修正,防止 /user/hello/xx 这样的路由 /user/hello 访问这个路径也一样能从前缀树查找出来,并不会报404
}// Put path --> /user/get/:id
// PUT 一般用于“更新”
// :id 是一种路由参数(Path Parameter)占位符
// /user/get/123 会匹配 /user/get/:id,并提取出 id = 123
// /user/get/abc 会匹配 /user/get/:id,提取出 id = abc
func (t *TreeNode) Put(path string) {strs := strings.Split(path, "/")for index, name := range strs {// 忽略第一个斜杠前的那个空格if index == 0 {continue}isMatch := falsefor _, node := range t.children {if node.name == name {isMatch = truet = nodebreak}}if !isMatch {node := &TreeNode{name: name,children: make([]*TreeNode, 0),isEnd: index == len(strs)-1,}t.children = append(t.children, node)t = node}}t.isEnd = true
}// Get path --> /user/get/1
// GET 一般用于“查”
func (t *TreeNode) Get(path string) *TreeNode {strs := strings.Split(path, "/")routerName := ""for index, name := range strs {// 忽略第一个斜杠前的那个空格if index == 0 {continue}isMatch := falsefor _, node := range t.children {if node.name == name || node.name == "*" || strings.Contains(node.name, ":") {isMatch = truerouterName += "/" + node.namenode.routerName = routerNamet = nodeif index == len(strs)-1 {return node}break}}if !isMatch {for _, node := range t.children {// /user/**// /user/get/userInfo// /user/aa/bbif node.name == "**" {// only onerouterName += "/" + node.namenode.routerName = routerNamereturn node}}}}return nil
}
// zj.gopackage zjgoimport ("fmt""log""net/http"
)const ANY = "ANY"// 定义处理响应函数
type HandleFunc func(ctx *Context)// 抽象出路由的概念
type routerGroup struct {name string // 组名handleFuncMap map[string]map[string]HandleFunc // 映射关系应该由每个路由组去维护handlerMethodMap map[string][]string // 记录GET,POST等请求方式所记录的路由,实现对同一路由不同请求方式的支持TreeNode *TreeNode // 记录该路由组下的路由前缀树
}// 定义路由结构体
type router struct {routerGroups []*routerGroup // 路由下面应该维护着不同的组
}// 添加路由组
func (r *router) Group(name string) *routerGroup {routerGroup := &routerGroup{name: name,handleFuncMap: make(map[string]map[string]HandleFunc),handlerMethodMap: make(map[string][]string),TreeNode: &TreeNode{name: "/", children: make([]*TreeNode, 0)},}r.routerGroups = append(r.routerGroups, routerGroup)return routerGroup
}// 给路由结构体添加一个添加路由功能的函数
// func (routerGroup *routerGroup) Add(name string, handleFunc HandleFunc) {
// routerGroup.handleFuncMap[name] = handleFunc
// }// 由于 ANY POST GET 都需要重复相同的逻辑代码,所以做一个提取操作
func (routerGroup *routerGroup) handleRequest(name string, method string, handleFunc HandleFunc) {if _, exist := routerGroup.handleFuncMap[name]; !exist {routerGroup.handleFuncMap[name] = make(map[string]HandleFunc)}if _, exist := routerGroup.handleFuncMap[name][method]; !exist {routerGroup.handleFuncMap[name][method] = handleFuncrouterGroup.handlerMethodMap[method] = append(routerGroup.handlerMethodMap[method], name)} else {panic("Under the same route, duplication is not allowed!!!")}routerGroup.TreeNode.Put(name)
}// Any代表支持任意的请求方式
func (routerGroup *routerGroup) Any(name string, handleFunc HandleFunc) {routerGroup.handleRequest(name, ANY, handleFunc)
}// POST代表支持POST请求方式
func (routerGroup *routerGroup) Post(name string, handleFunc HandleFunc) {routerGroup.handleRequest(name, http.MethodPost, handleFunc)
}// GET代表支持GET请求方式
func (routerGroup *routerGroup) Get(name string, handleFunc HandleFunc) {routerGroup.handleRequest(name, http.MethodGet, handleFunc)
}// 只要实现 ServeHTTP 这个方法,就相当于实现了对应的 HTTP 处理器
// 结构体 Engine 实现了 ServeHTTP(w http.ResponseWriter, r *http.Request) 方法
// 所以它就自动实现了 http.Handler 接口,因此可以直接被用于 http.ListenAndServe
// Engine 实现了 ServeHTTP,它就是一个合法的 http.Handler,可以用 http.Handle 来绑定它到某个具体的路由路径上!
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {// 在 Go 的 net/http 包中,r *http.Request 代表了客户端发来的 HTTP 请求对象。// 可以通过 r.Method 来获取这次请求使用的是什么方法(Method),例如:GET、POST、PUT、DELETE 等。if r.Method == http.MethodGet {fmt.Fprintf(w, "这是一个 GET 请求!!! ")} else if r.Method == http.MethodPost {fmt.Fprintf(w, "这是一个 POST 请求!!! ")} else {fmt.Fprintf(w, "这是一个其他类型的请求:%s!!! ", r.Method)}for _, group := range e.routerGroups {routerName := SubStringLast(r.RequestURI, "/"+group.name)if node := group.TreeNode.Get(routerName); node != nil && node.isEnd {// 路由匹配上了ctx := &Context{W: w, R: r}// 先判断当前请求路由是否支持任意请求方式的Anyif handle, exist := group.handleFuncMap[node.routerName][ANY]; exist {handle(ctx)return}// 不支持 Any,去该请求所对应的请求方式对应的 Map 里面去找是否有对应的路由if handle, exist := group.handleFuncMap[node.routerName][r.Method]; exist {handle(ctx)return}// 没找到对应的路由,说明该请求方式不允许w.WriteHeader(http.StatusMethodNotAllowed)fmt.Fprintf(w, "%s %s not allowed!!!\n", r.Method, r.RequestURI)return}// for name, methodHandleMap := range group.handleFuncMap {// url := "/" + group.name + name// //判断一下当前的url是否等于请求的url,即路由匹配// if r.RequestURI == url {// }// }}w.WriteHeader(http.StatusNotFound)fmt.Fprintf(w, "%s %s not found!!!\n", r.Method, r.RequestURI)return
}// 定义一个引擎结构体
type Engine struct {router
}// 引擎结构体的初始化方法
func New() *Engine {return &Engine{router: router{},}
}// 引擎的启动方法
func (e *Engine) Run() {// for _, group := range e.routerGroups {// for name, value := range group.handleFuncMap {// http.HandleFunc("/"+group.name+name, value)// }// }// 把 e 这个http处理器绑定到对应路由下http.Handle("/", e)err := http.ListenAndServe(":3986", nil)if err != nil {log.Fatal(err)}
}
// main.gopackage mainimport ("fmt""net/http""github.com/ErizJ/ZJGo/zjgo"
)func main() {fmt.Println("Hello World!")// // 注册 HTTP 路由 /hello// http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {// fmt.Fprintf(w, "Hello Go!")// })// // 启动 HTTP 服务器// err := http.ListenAndServe("8111", nil)// if err != nil {// log.Fatal(err)// }engine := zjgo.New()g1 := engine.Group("user")g1.Get("/hello", func(ctx *zjgo.Context) {fmt.Fprintf(ctx.W, http.MethodGet+" Hello Go!——user/hello")})// 浏览器地址栏输入的都是 GET 请求// 需要用 curl 或 Postman 来发一个真正的 POST 请求,才会命中 Post handlerg1.Post("/info", func(ctx *zjgo.Context) {fmt.Fprintf(ctx.W, http.MethodPost+" Hello Go!——user/info——POST")})g1.Get("/info", func(ctx *zjgo.Context) {fmt.Fprintf(ctx.W, http.MethodGet+" Hello Go!——user/info——GET")})g1.Get("/get/:id", func(ctx *zjgo.Context) {fmt.Fprintf(ctx.W, http.MethodGet+" Hello Go!——user/get/:id——GET")})g1.Get("/isEnd/get", func(ctx *zjgo.Context) {fmt.Fprintf(ctx.W, http.MethodGet+" Hello Go!——user/isEnd/get——GET")})// 只要路由匹配,就会执行对应的处理函数g1.Any("/any", func(ctx *zjgo.Context) {fmt.Fprintf(ctx.W, " Hello Go!——user/any")})// g2 := engine.Group("order")// g2.Add("/hello", func(w http.ResponseWriter, r *http.Request) {// fmt.Fprintf(w, "Hello Go!——order/hello")// })// g2.Add("/info", func(w http.ResponseWriter, r *http.Request) {// fmt.Fprintf(w, "Hello Go!——order/info")// })fmt.Println("Starting...")engine.Run()}
完善路由代码
// zj.gopackage zjgoimport ("fmt""log""net/http"
)const ANY = "ANY"// 定义处理响应函数
type HandleFunc func(ctx *Context)// 抽象出路由的概念
type routerGroup struct {name string // 组名handleFuncMap map[string]map[string]HandleFunc // 映射关系应该由每个路由组去维护handlerMethodMap map[string][]string // 记录GET,POST等请求方式所记录的路由,实现对同一路由不同请求方式的支持TreeNode *TreeNode // 记录该路由组下的路由前缀树
}// 定义路由结构体
type router struct {routerGroups []*routerGroup // 路由下面应该维护着不同的组
}// 添加路由组
func (r *router) Group(name string) *routerGroup {routerGroup := &routerGroup{name: name,handleFuncMap: make(map[string]map[string]HandleFunc),handlerMethodMap: make(map[string][]string),TreeNode: &TreeNode{name: "/", children: make([]*TreeNode, 0)},}r.routerGroups = append(r.routerGroups, routerGroup)return routerGroup
}// 给路由结构体添加一个添加路由功能的函数
// func (routerGroup *routerGroup) Add(name string, handleFunc HandleFunc) {
// routerGroup.handleFuncMap[name] = handleFunc
// }// 由于 ANY POST GET 都需要重复相同的逻辑代码,所以做一个提取操作
func (routerGroup *routerGroup) handleRequest(name string, method string, handleFunc HandleFunc) {if _, exist := routerGroup.handleFuncMap[name]; !exist {routerGroup.handleFuncMap[name] = make(map[string]HandleFunc)}if _, exist := routerGroup.handleFuncMap[name][method]; !exist {routerGroup.handleFuncMap[name][method] = handleFuncrouterGroup.handlerMethodMap[method] = append(routerGroup.handlerMethodMap[method], name)} else {panic("Under the same route, duplication is not allowed!!!")}routerGroup.TreeNode.Put(name)
}// Any代表支持任意的请求方式
func (routerGroup *routerGroup) Any(name string, handleFunc HandleFunc) {routerGroup.handleRequest(name, ANY, handleFunc)
}// POST代表支持POST请求方式
func (routerGroup *routerGroup) Post(name string, handleFunc HandleFunc) {routerGroup.handleRequest(name, http.MethodPost, handleFunc)
}// GET代表支持GET请求方式
func (routerGroup *routerGroup) Get(name string, handleFunc HandleFunc) {routerGroup.handleRequest(name, http.MethodGet, handleFunc)
}// DELETE代表支持DELETE请求方式
func (routerGroup *routerGroup) Delete(name string, handleFunc HandleFunc) {routerGroup.handleRequest(name, http.MethodDelete, handleFunc)
}// PUT代表支持PUT请求方式
func (routerGroup *routerGroup) Put(name string, handleFunc HandleFunc) {routerGroup.handleRequest(name, http.MethodPut, handleFunc)
}// PATCH代表支持PATCH请求方式
func (routerGroup *routerGroup) Patch(name string, handleFunc HandleFunc) {routerGroup.handleRequest(name, http.MethodPatch, handleFunc)
}// 只要实现 ServeHTTP 这个方法,就相当于实现了对应的 HTTP 处理器
// 结构体 Engine 实现了 ServeHTTP(w http.ResponseWriter, r *http.Request) 方法
// 所以它就自动实现了 http.Handler 接口,因此可以直接被用于 http.ListenAndServe
// Engine 实现了 ServeHTTP,它就是一个合法的 http.Handler,可以用 http.Handle 来绑定它到某个具体的路由路径上!
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {// 在 Go 的 net/http 包中,r *http.Request 代表了客户端发来的 HTTP 请求对象。// 可以通过 r.Method 来获取这次请求使用的是什么方法(Method),例如:GET、POST、PUT、DELETE 等。if r.Method == http.MethodGet {fmt.Fprintf(w, "这是一个 GET 请求!!! ")} else if r.Method == http.MethodPost {fmt.Fprintf(w, "这是一个 POST 请求!!! ")} else {fmt.Fprintf(w, "这是一个其他类型的请求:%s!!! ", r.Method)}for _, group := range e.routerGroups {routerName := SubStringLast(r.RequestURI, "/"+group.name)if node := group.TreeNode.Get(routerName); node != nil && node.isEnd {// 路由匹配上了ctx := &Context{W: w, R: r}// 先判断当前请求路由是否支持任意请求方式的Anyif handle, exist := group.handleFuncMap[node.routerName][ANY]; exist {handle(ctx)return}// 不支持 Any,去该请求所对应的请求方式对应的 Map 里面去找是否有对应的路由if handle, exist := group.handleFuncMap[node.routerName][r.Method]; exist {handle(ctx)return}// 没找到对应的路由,说明该请求方式不允许w.WriteHeader(http.StatusMethodNotAllowed)fmt.Fprintf(w, "%s %s not allowed!!!\n", r.Method, r.RequestURI)return}// for name, methodHandleMap := range group.handleFuncMap {// url := "/" + group.name + name// //判断一下当前的url是否等于请求的url,即路由匹配// if r.RequestURI == url {// }// }}w.WriteHeader(http.StatusNotFound)fmt.Fprintf(w, "%s %s not found!!!\n", r.Method, r.RequestURI)return
}// 定义一个引擎结构体
type Engine struct {router
}// 引擎结构体的初始化方法
func New() *Engine {return &Engine{router: router{},}
}// 引擎的启动方法
func (e *Engine) Run() {// for _, group := range e.routerGroups {// for name, value := range group.handleFuncMap {// http.HandleFunc("/"+group.name+name, value)// }// }// 把 e 这个http处理器绑定到对应路由下http.Handle("/", e)err := http.ListenAndServe(":3986", nil)if err != nil {log.Fatal(err)}
}
相关文章:
Go 微服务框架 | 路由实现
文章目录 不用框架实现web接口实现简单的路由实现分组路由支持不同的请求方式支持同一个路径的不同请求方式前缀树应用前缀树完善路由代码 不用框架实现web接口 // blog main.go 文件 package mainimport ("fmt""log""net/http" )func main() {…...
2025年AI开发学习路线
目录 一、基础阶段(2-3个月) 1. 数学与编程基础 2. 机器学习入门 二、核心技能(3-4个月) 1. 深度学习与框架 2. 大模型开发(重点) 三、进阶方向(3-6个月) 1. 多模态与智能体…...
TimescaleDB 2.19.2 发布
TimescaleDB 2.19.2 已于 2025 年 4 月 7 日发布2。此次发布是基于 PostgreSQL 的开源时序数据库 TimescaleDB 的一次更新。 从 GitHub 上的 Pull Request 信息可知,此次发布主要是将相关更改合并到 2.19.x 分支,涉及到一系列的测试和构建配置,包括不同版本 PostgreSQL(如 …...
「Unity3D」TextMeshPro中的TMP_InputField,用来实现输入框的几个小问题
第一,正确设置Scrollbar。 设置Scrollbar之后,不能设置Text Component的Font Size为Auto Size,否则Scrollbar无法正确计算显示。 那么,要想自动适配字体大小,可以让Placeholder中的Font Size设置为Auto,这…...
HTML 是什么?网页创建的核心标记语言
原文:HTML 是什么?网页创建的核心标记语言 | w3cschool笔记 HTML 是什么? HTML 是一种标记语言,用于创建网页。简单来说,HTML 就像一本魔法书,它告诉电脑如何展示网页上的内容,比如文字、图片…...
考研单词笔记 2025.04.09
act v表现,行动,做事,扮演,充当,担任,起作用n行为,行动,法案,法令 action n行为,行动 behave v表现,行事,守规矩,举止端…...
map/multimap
1.概念 map中所有元素都是pair<key,value>,key 是map的键,value 是map的值 所有元素都会根据key自动排序 map/multimap属于关联式容器,底层结构是用二叉树实现。 map和multimap区别: map不允许容器中有重复key值元素 m…...
CSS 定位属性的生动比喻:以排队为例理解 relative 与 absolute
目录 一、理解标准流与队伍的类比 二、relative 定位:队伍中 “小范围活动” 的人 三、absolute 定位:队伍中 “彻底离队” 的人 在学习 CSS 的过程中,定位属性relative和absolute常常让初学者感到困惑。它们的行为方式和对页面布局的影响较…...
基于二叉堆实现的 PriorityQueue
基于二叉堆实现的 PriorityQueue 是一种常见的数据结构,广泛用于任务调度、路径搜索、事件模拟等场景。下面我将用 Java 语言实现一个简单的基于最小堆的 PriorityQueue,即优先级最小的元素先出队。 ✅ 实现目标 使用数组实现二叉最小堆(即父…...
大模型分布式推理和量化部署
一、小常识 1、计算大模型占用多少显存 对于一个7B(70亿)参数的模型,每个参数使用16位浮点数(等于 2个 Byte)表示,则模型的权重大小约为: 7010^9 parameters2 Bytes/parameter14GB 70亿个参数…...
循环神经网络 - 长程依赖问题及改进方案
循环神经网络在学习过程中的主要问题是由于梯度消失或爆炸问题,很难建模长时间间隔(Long Range)的状态之间的依赖关系。 本文我们来学习长程依赖问题及其对应的改进方案,在这部分知识的学习过程中,我建议大家着重理解,对于数学公…...
点击抽奖功能总结
首先用户打开网页,映入眼帘的是一个输入框和一个提交按钮。当用户在输入框中输入自己的年龄并点击提交后,系统会根据输入的年龄给出相应提示。若年龄达到 60 岁,页面将显示一个新的抽奖区域,用户可以点击 “抽奖” 按钮开始抽奖。…...
AWS Bedrock生成视频详解:AI视频创作新时代已来临
💡 TL;DR: AWS Bedrock现已支持AI视频生成功能,让企业无需深厚AI专业知识即可创建高质量视频内容。本文详解Bedrock视频生成能力的工作原理、应用场景和实操指南,助你快速掌握这一革命性技术。 🎬 AWS Bedrock视频生成:改变内容创作的游戏规则 还记得几年前,制作一个专…...
理解 TOGAF®标准中的架构原则
原则是帮助组织实现其使命的基本规则和指南。它们旨在长期稳定且很少修改,在各个领域中充当决策和行动的指南针。在企业架构(EA)的背景下,原则在指导架构框架的开发和应用方面发挥着至关重要的作用。本文将探讨企业原则和架构原则…...
基于视觉密码的加密二值图像可逆数据隐藏
接下来,分享一篇论文,标题为《Multi-Party Reversible Data Hiding in Ciphertext Binary Images Based on Visual Cryptography》,由Bing Chen等人发表在《IEEE Signal Processing Letters》上。该论文提出了一种基于视觉密码学的多方可逆数…...
ubuntu22.04 中 No module named ‘_bz2‘问题解决方案
前言 本篇是介绍ubuntu22.04中 No module named ‘_bz2‘问题解决方案 网上版本很多,比如安装libbz库什么的,可能别人有用,但是我自己这边出了一堆问题 一、流程 1.1 查看bz2.xx.so文件 看自己的python版本,我新安装了个pyth…...
什么是声波,声波的传播距离受哪些因素影响?
一、声波的定义: 声波是一种机械波,它是通过介质(如空气、水、固体等)传播的振动。以下是关于声波的详细介绍: 1、声波的产生 声波是由物体的振动产生的。例如,人说话时,声带振动产生声波&…...
用PHPExcel 封装的导出方法,支持导出无限列
用PHPExcel 封装的导出方法,支持导出无限列 避免PHPExcel_Exception Invalid cell coordinate [1 异常错误 /*** EXCEL导出* param [string] $file_name 保存的文件名及表格工作区名,不加excel后缀名* param [array] $fields 二维数组* param [array] $…...
STL-stack栈和queue队列
stack栈和queue队列 在STL中 stack 和 queue 设计为容器适配器,容器适配器是使用特定容器类的封装对象作为其基础容器的类,提供一组特定的成员函数来访问其元素。 在我的STL系列中之前的容器 vector、list、deque 都是从底层类型一步步封装而来的,但是 stack 和 queue 没有…...
AI 提示词不会写?试试 PromptIDE
这段时间,AI 技术大爆炸 已经改变了我们的工作方式,而 会不会用 AI,已经成为区分工作能力的关键! 💡 在这个AI重构工作方式的时代,会用和不会用AI的人正在拉开巨大差距: √ 高手用AI——效率飙…...
【python读取并显示遥感影像】
在Python中读取并显示遥感影像,可以使用rasterio库读取影像数据,并结合matplotlib进行可视化。以下是一个完整的示例代码: import rasterio import matplotlib.pyplot as plt import numpy as np# 打开遥感影像文件 with rasterio.open(path…...
代码随想录算法训练营第十三天
LeetCode题目: 110. 平衡二叉树257. 二叉树的所有路径404. 左叶子之和222. 完全二叉树的节点个数3375. 使数组的值全部为 K 的最少操作次数(每日一题) 其他: 今日总结 往期打卡 110. 平衡二叉树 跳转: 110. 平衡二叉树 学习: 代码随想录公开讲解 问题: 给定一个二叉树&#…...
TQTT_KU5P开发板教程---高速收发器之XDMA实现PCIE
文档功能介绍 本文档主要实现了通过一个叫做XDMA的IP,实现PCIE的测试例子。工程新建方法请参考文档《流水灯》。 Vivado创建项目 起始页(或 file-->Project-->New 创建新工程(Create New Project) 向导起始页面 点击 Next--> Project Name(…...
蓝桥杯速成刷题清单(上)
一、1.排序 - 蓝桥云课 (快速排序)算法代码: #include <bits/stdc.h> using namespace std; const int N 5e5 10; int a[N];int main() {int n;cin >> n;for (int i 0; i < n; i) {cin >> a[i];}sort(a, a n);for …...
【FreeRTOS】二值信号量 是 消息队列 吗
在读FreeRTOS内核实现与应用开发实战指南的时候,书中第16章有这么一句话:可以将二值信号量看作只有一个消息的队列,incident这个队列只能为空或满(因此称为二值),在运用时只需要之傲队列中是否由消息即可&a…...
BOTA六维力矩传感器在三层AI架构中的集成实践:从数据采集到力控闭环
随着机器人技术的迅猛发展,Bota六维力矩传感器成为三层AI架构中的核心组件。它通过高精度的力与力矩感知能力,为感知层提供实时数据支持,优化了决策层的判断效率,并确保执行层操作的精确性和安全性。 Bota贯通式力矩传感器PixOne&…...
UE5 matcap学习笔记
没难度节点,但是要记住这种思维,移动端常用: 原视频:(美学阿姨)MatCap材质原理讲解与UE5中的实现方法_哔哩哔哩_bilibili...
神经网络 - 关于简单的激活函数的思考总结
最近一直在学习神经网络,有一些收获,也有一些迷惑,所以驻足思考:为什么简单的激活函数如sigmoid函数、ReLU函数,当应用在神经网络的模型中,却可以实现对现实世界复杂的非线性关系的模拟呢?本文我…...
pig 权限管理开源项目学习
pig 源码 https://github.com/pig-mesh/pig 文档在其中,前端在文档中,官方视频教学也在文档中有。 第一次搭建,建议直接去看单体视频,照着做即可。 文章目录 项目结构Maven 多模块项目pig-boot 启动核心模块pig-auth 实现认证和…...
excel中的VBA指令示例(二)
。。。接上篇。 Range("D1").Select ’选择D1单元格 ActiveCell.FormulaR1C1 "装配数量" ‘单元格内容为装配数量 Range("D1").Select Selection.AutoFilter …...
基于vue3与supabase系统认证机制
1. 认证框架概述 系统采用 Supabase 作为认证和数据服务提供商,实现了完整的用户身份验证流程。系统使用基于 JWT (JSON Web Token) 的认证方式,提供了安全可靠的用户身份管理机制。 1.1 技术栈 前端: Vue 3 TypeScript状态管理: Pinia认证服务: Sup…...
【算法笔记】并查集详解
🚀 并查集(Union-Find)详解:原理、实现与优化 并查集(Union-Find)是一种非常高效的数据结构,用于处理动态连通性问题,即判断若干个元素是否属于同一个集合,并支持集合合…...
基于Redis实现短信防轰炸的Java解决方案
基于Redis实现短信防轰炸的Java解决方案 前言 在当今互联网应用中,短信验证码已成为身份验证的重要手段。然而,这也带来了"短信轰炸"的安全风险 - 恶意用户利用程序自动化发送大量短信请求,导致用户被骚扰和企业短信成本激增。本…...
编程中,!! 双感叹号的理解
在编程中,!! 双感叹号的含义取决于上下文。通常情况下,!! 是逻辑非操作符的双重使用,用来将一个值强制转换为布尔类型。 1. 逻辑非操作符 在 JavaScript 中,! 是逻辑非操作符,它会将一个值转换为布尔类型:…...
ARM内核与寄存器
ARM内核与寄存器详解 目录 ARM架构概述ARM处理器模式 Cortex-M3内核的处理器模式Cortex-A系列处理器模式 ARM寄存器集 通用寄存器程序计数器(PC)链接寄存器(LR)堆栈指针(SP)状态寄存器(CPSR/SPSR) 协处理器寄存器NEON和VFP寄存器寄存器使用规范常见ARM指令与寄存器操作 ARM架…...
【C++进阶】关联容器:set类型
目录 一、set 基本概念 1.1 定义与特点 1.2 头文件与声明 1.3 核心特性解析 二、set 底层实现 2.1 红黑树简介 2.2 红黑树在 set 中的应用 三、set 常用操作 3.1 插入元素 3.2 删除元素 3.3 查找元素 3.4 遍历元素 3.5 性能特征 四、set 高级应用 4.1 自定义比较…...
Linux内核——X86分页机制
X86分页机制 x86的分页单元支持两种分页模式:常规分页与扩展分页。 常规分页采用两级结构,固定页大小为4KB。线性地址被划分为三个字段: 页目录索引(最高10位)页表索引(中间10位)页内偏移&am…...
重温Java - Java基础二
工作中常见的6中OOM 问题 堆内存OOM 堆内存OOM 是最常见的OOM了。出现堆内存OOM 问题的异常信息如下 java.lang.OutOfMemoryError: Java heap space此OOM是由于Java中的heap的最大值,已经不能满足需求了。 举个例子 Test public void test01(){List<OOMTest…...
回溯算法+对称剪枝——从八皇后问题到数独问题(二)
引入: 本节我们进一步完善八皇后问题,学习剪枝、八皇后残局问题 进一步领会逻辑编程的概念,深入体会回溯算法,回顾上一节提到的启发搜索策略。 回顾: 八皇后问题:我们需要在一个空棋盘上放置 n 个皇后&a…...
基于 Spring Boot 瑞吉外卖系统开发(三)
基于 Spring Boot 瑞吉外卖系统开发(三) 分类列表 静态页面 实现功能所需要的接口 定义Mapper接口 Mapper public interface CategoryMapper extends BaseMapper<Category> {}定义Service接口 public interface CategoryService extends ISe…...
Pascal VOC 2012 数据集格式与文件结构
Pascal VOC 2012 1 Pascal VOC 2012 数据集1.1 数据集概述1.2 文件结构1.3 关键文件和内容格式(1) Annotations/ 目录(2) ImageSets/ 目录(3) JPEGImages/ 目录(4) SegmentationClass/ 和 SegmentationObject/ 目录 1.4 标注格式说明(1) 目标检测标注(2) 语义分割标注(3)实例分…...
11:00开始面试,11:08就出来了,问的问题有点变态。。。
从小厂出来,没想到在另一家公司又寄了。 到这家公司开始上班,加班是每天必不可少的,看在钱给的比较多的份上,就不太计较了。没想到8月一纸通知,所有人不准加班,加班费不仅没有了,薪资还要降40%…...
MySQL与Oracle字段类型对比及迁移指南
文章目录 MySQL与Oracle字段类型对比及迁移指南1. 核心字段类型对照表2. 常见函数对照表2.1 字符串函数2.2 日期函数2.3 数值函数2.4 类型转换函数2.5 空值处理函数2.6 其他常用函数3. 迁移配置细则4. 迁移工具推荐5. 常见问题解决方案6. 性能优化建议MySQL与Oracle字段类型对比…...
Spring MVC 处理 HTTP 状态码、响应头和异常的完整示例
Spring MVC 处理 HTTP 状态码、响应头和异常的完整示例 1. 正常响应处理 通过 ResponseEntity 可以灵活控制 HTTP 状态码、响应头和响应体。 代码示例:创建资源返回 201 并设置 Location 头 import org.springframework.http.HttpHeaders; import org.springfram…...
http页面的加载过程
HTTP/2 核心概念 1.1 流(Stream) • 定义:HTTP/2 连接中的逻辑通道,用于传输数据,每个流有唯一标识符(Stream ID)。 • 特点: ◦ 支持多路复用(多个流并行传输&#…...
动手人形机器人(RL)
1 PPO的讲解 核心步骤,如策略网络和价值网络的定义、优势估计、策略更新、价值更新等基础功能的实现 2 代码构成 可能涉及 初始化,Behavior Clone 3 动手强化学习 import pytorch as torch class actorcritic ##等待补充 4 PD Gains 在机器人学中&…...
使用RabbitMQ实现异步秒杀
搭建RabbitMQ 在虚拟机上用docker搭建RabbitMQ,首先拉取镜像 docker run --privilegedtrue -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management mkdir -p /usr/local/docker/rabbitmq再创建rabbitmq容器,下面的命令已经能够创建之后…...
基于PyQt5的企业级生日提醒系统设计与实现
在企业人力资源管理场景中,员工生日提醒是一项重要的关怀功能。本文将以一个基于PyQt5开发的生日提醒系统为例,深入解析桌面应用程序开发中的关键技术实现,涵盖GUI设计、数据持久化、系统集成、动画效果等核心模块。 一、技术选型分析 1.1 PyQt5框架优势 跨平台特性:支持W…...
蓝桥杯嵌入式第16届——ADC模数转化部分
将外部的模拟信号转换为数字信号 ( ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁 ) STM32 - ADC 笔记_stm32 adc电容-CSDN博客 引脚状况 STM32cubemx配置 ADC1配置 ADC2配置 代码部分 …...
拜特科技签约天津城投集团,携手共建司库管理系统
近日,拜特科技成功签约天津城市基础设施建设投资集团有限公司(以下简称“天津城投集团”),携手共建司库管理系统。 自2015年结缘以来,拜特科技与天津城投集团已携手并进十年,构建了稳固且高效的合作桥梁。…...