前缀树路由Router
感谢Geek 兔的教程
示例代码,解释摘抄 https://geektutu.com/ + 结合自己的理解
前缀树设计与实现
之前,我们用了一个非常简单的
map
结构存储了路由表,使用map
存储键值对,索引非常高效,但是有一个弊端,键值对的存储的方式,只能用来索引静态路由。
那如果我们想支持类似于
/hello/:name
这样的动态路由怎么办呢?所谓动态路由,即一条路由规则可以匹配某一类型而非某一条固定的路由。例如/hello/:name
,可以匹配/hello/geektutu
、hello/jack
等。
实现动态路由最常用的数据结构,被称为前缀树(Trie树)。Trie 维基百科
看到名字你大概也能知道前缀树长啥样了:每一个节点的所有的子节点都拥有相同的前缀。这种结构非常适用于路由匹配,比如我们定义了如下路由规则:
- /:lang/doc
- /:lang/tutorial
- /:lang/intro
- /about
- /p/blog
- /p/related
HTTP请求的路径恰好是由/
分隔的多段构成的,因此,每一段可以作为前缀树的一个节点。
我们通过树结构查询,如果中间某一层的节点都不满足条件,那么就说明没有匹配到的路由,查询结束。
接下来我们实现的动态路由具备以下两个功能。
- 参数匹配
:
。例如/p/:lang/doc
,可以匹配/p/c/doc
和/p/go/doc
。 - 通配
*
。例如/static/*filepath
,可以匹配/static/fav.ico
,也可以匹配/static/js/jQuery.js
,这种模式常用于静态服务器,能够递归地匹配子路径。
前缀树设计
首先我们需要设计树节点上应该存储那些信息。
type node struct {
pattern string // 待匹配路由,例如 /p/:lang
part string // 路由中的一部分,例如 :lang
children []*node // 子节点,例如 [doc, tutorial, intro]
isWild bool // 是否精确匹配,part 含有 : 或 * 时为true
}
这里定义了一个名为 node
的结构体,它包含了以下字段:
pattern
:待匹配的路由字符串,例如/p/:lang
,表示一个路由模式,其中:lang
是一个占位符,后续会被实际的值替代。part
:路由中的一部分,例如:lang
,表示路由模式中的占位符部分。children
:一个指向子节点的切片,表示当前节点可能的子节点,例如[doc, tutorial, intro]
。isWild
:一个布尔值,表示当前节点是否用于精确匹配。当part
中包含:
或*
时,此字段为true
,表示该节点是一个通配符节点,用于匹配一类路由。
插入节点实现
比如我们想要插入一个/p/:lang
,按照之前的分析我们的前缀树结构是按照分层存储的!
假设我们第一层都空节点,后面插入的节点全部放入空节点的children中!
p->lang
所以一般先去判断 p 是不是存在于 第一层级的children~
- 如果不在,则需要添加对应的 p 节点到第一层的children中,然后调用插入递归去处理
lang
- 如果存在的话,那么直接就递归插入
lang
就好
那么我们的插入函数就需要传入一个参数来表示当前插入操作执行的深度height!默认从0开始!
所以我们需要把传入的pattern切割为数组,这样我们可以直接得到插入的层级,也可以直接通过索引访问层级!
比如
p->lang
很直观的我们就知道是2层,索引0的位置代表第一层,索引1的位置代表第二层!
// insert 方法用于向路由树中插入新的路由模式。
func (n *node) insert(pattern string, Parts []string, height int) {
// 1⃣️ 如果递归到达路由模式的末尾,将当前节点的 pattern 字段设置为传入的 pattern。
if len(Parts) == height {
n.pattern = pattern
return
}
// 获取当前深度的路由部分。
Part := Parts[height]
// 2⃣️ 查找与当前路由部分匹配的子节点。
child := n.matchChild(Part)
// 3⃣️ 如果没有找到匹配的子节点,创建一个新的子节点,并将其添加到当前节点的子节点列表中。
if child == nil {
child = &node{Part: Part, IsWild: Part[0] == ':' || Part[0] == '*'}
n.Children = append(n.Children, child)
}
// 递归调用 insert 方法,处理下一个路由部分。
child.insert(pattern, Parts, height+1)
}
代码注释解释:
1⃣️:如果递归到达路由模式的末尾,将当前节点的 pattern 字段设置为传入的 pattern。是因为后面我们判断不光是每一级的路径进行匹配,而且也要判断那一级节点的pattern
是否为空,单纯靠这路径去匹配有可能,我们插入的路径为 /p/:lang/doc
但是客户端请求的路径为 /p/python
,这样可能就不够准确!所以这里采用了一个只给最后一层添加pattern
的方式,不单单路径需要准确,而且需要当前节点pattern
不为空,代表最后一层!
2⃣️:查找与当前路由部分匹配的子节点。默认为空节点作为一级节点,依次只需要查询子节点中是否含part的节点
// 第一个匹配成功的节点,用于插入
func (n *node) matchChild(part string) *node {
// 遍历当前节点的子节点列表。
for _, child := range n.children {
// 如果子节点的部分(part)与给定部分相等,或者子节点是通配符节点(isWild=true),
// 则表示匹配成功,返回该子节点。
if child.part == part || child.isWild {
return child
}
}
// 如果没有找到匹配的子节点,返回 nil 表示匹配失败。
return nil
}
3⃣️:如果没有查到对应part的子节点,那么就去创建一个~ 如果当前part的第一个字符串为 :
或者 *
比如 /:lang
表示为通配符 part
~ isWild
的值设置为true
完整代码:
package gee
type node struct {
Pattern string `json: Pattern` // 待匹配路由,例如 /p/:lang
Part string `json: Part` // 路由中的一部分,例如 :lang
Children []*node `json: Children` // 子节点,例如 [doc, tutorial, intro]
IsWild bool `json: IsWild` // 是否精确匹配,Part 含有 : 或 * 时为true
}
// 第一个匹配成功的节点,用于插入
func (n *node) matchChild(Part string) *node {
for _, child := range n.Children {
if child.Part == Part || child.IsWild {
return child
}
}
return nil
}
// insert 方法用于向路由树中插入新的路由模式。
func (n *node) insert(Pattern string, Parts []string, height int) {
// 1⃣️ 如果递归到达路由模式的末尾,将当前节点的 Pattern 字段设置为传入的 Pattern。
if len(Parts) == height {
n.Pattern = Pattern
return
}
// 获取当前深度的路由部分。
Part := Parts[height]
// 2⃣️ 查找与当前路由部分匹配的子节点。
child := n.matchChild(Part)
// 3⃣️ 如果没有找到匹配的子节点,创建一个新的子节点,并将其添加到当前节点的子节点列表中。
if child == nil {
child = &node{Part: Part, IsWild: Part[0] == ':' || Part[0] == '*'}
n.Children = append(n.Children, child)
}
// 递归调用 insert 方法,处理下一个路由部分。
child.insert(Pattern, Parts, height+1)
}
插入测试
package gee
import (
"encoding/json"
"fmt"
"testing"
)
func Test_node_matchChild(t *testing.T) {
root := &node{}
// 将根节点转换为 JSON
// 插入路由模式 "/p/:lang/doc"
parts1 := []string{"p", ":lang", "doc"}
root.insert("/p/:lang/doc", parts1, 0)
// 插入路由模式 "/p/:lang/tutorial"
parts2 := []string{"p", ":lang", "tutorial"}
root.insert("/p/:lang/tutorial", parts2, 0)
// 插入路由模式 "/p/:lang/intro"
parts3 := []string{"p", ":lang", "intro"}
root.insert("/p/:lang/intro", parts3, 0)
// 插入路由模式 "/users/:id"
parts4 := []string{"users", ":id"}
root.insert("/users/:id", parts4, 0)
jsonData, err := json.MarshalIndent(root, "", " ")
if err != nil {
fmt.Println("JSON marshaling error:", err)
return
}
// 打印 JSON 数据
fmt.Println(string(jsonData))
}
插入最终结果JSON数据!
{
"Pattern": "",
"Part": "",
"Children": [
{
"Pattern": "",
"Part": "p",
"Children": [
{
"Pattern": "",
"Part": ":lang",
"Children": [
{
"Pattern": "/p/:lang/doc",
"Part": "doc",
"Children": null,
"IsWild": false
},
{
"Pattern": "/p/:lang/tutorial",
"Part": "tutorial",
"Children": null,
"IsWild": false
},
{
"Pattern": "/p/:lang/intro",
"Part": "intro",
"Children": null,
"IsWild": false
}
],
"IsWild": true
}
],
"IsWild": false
},
{
"Pattern": "",
"Part": "users",
"Children": [
{
"Pattern": "/users/:id",
"Part": ":id",
"Children": null,
"IsWild": true
}
],
"IsWild": false
}
],
"IsWild": false
}
查询节点实现
按照上面的 假如我们客户端请求了一个 /p/go/intro
,因为我们存储的时候是按照一层层的去存,那么查询的时候还是要按照一层层的去查!
首先把请求的 pattern
转化为切片,分每一个part去匹配,依然从第一层开始匹配(height:0
):
-
如果匹配到了那么就说明,当前树中有对应该层的节点~ 然后拿到全部的子节点,然后
height+1
,去下一层级的part去children里面去匹配 -
如果没有匹配到 说明树中没有对应的节点直接退出查询,返回
nil
-
另外如果height到了最后一层,没有
pattern
的值,或者是当前节点为 * 通配符开头,也没
pattern
的值(因为正常 * 也是最后一层了【不考虑/files/*filepath/js
这种情况】)那就表明没有匹配到,直接返回
nil
// search 方法用于在路由树中搜索与给定路由部分匹配的节点。
func (n *node) search(Parts []string, height int) *node {
// 如果递归到达路由部分的末尾或当前节点是通配符节点(以 * 开头 /files/*filepath),
// 并且当前节点的 pattern 字段不为空,则返回当前节点。
if len(Parts) == height || strings.HasPrefix(n.Part, "*") {
if n.pattern == "" {
return nil
}
return n
}
// 获取当前深度的路由部分。
Part := Parts[height]
//1⃣️ 查找与当前路由部分匹配的子节点。
Children := n.matchChildren(Part)
// 遍历匹配的子节点,递归调用 search 方法,处理下一个路由部分。
for _, child := range Children {
result := child.search(Parts, height+1)
if result != nil {
return result
}
}
// 如果没有找到匹配的节点,返回 nil 表示匹配失败。
return nil
}
1⃣️ 查找与当前路由部分匹配的子节点。
// matchChildren 方法用于查找与给定路由部分(Part)匹配的所有子节点。
func (n *node) matchChildren(Part string) []*node {
// 创建一个空的节点切片,用于存储匹配成功的子节点。
nodes := make([]*node, 0)
// 遍历当前节点的所有子节点。
for _, child := range n.Children {
// 如果子节点的部分(Part)与给定部分相等,或者子节点是通配符节点(IsWild=true),
// 则将该子节点添加到 nodes 中,表示匹配成功。
if child.Part == Part || child.IsWild {
nodes = append(nodes, child)
}
}
// 返回包含匹配成功的子节点的切片。
return nodes
}
查询测试
package gee
import (
"encoding/json"
"fmt"
"testing"
)
func Test_node_matchChild(t *testing.T) {
root := &node{}
// 将根节点转换为 JSON
// 插入路由模式 "/p/:lang/doc"
parts1 := []string{"p", ":lang", "doc"}
root.insert("/p/:lang/doc", parts1, 0)
// 插入路由模式 "/p/:lang/tutorial"
parts2 := []string{"p", ":lang", "tutorial"}
root.insert("/p/:lang/tutorial", parts2, 0)
// 插入路由模式 "/p/:lang/intro"
parts3 := []string{"p", ":lang", "intro"}
root.insert("/p/:lang/intro", parts3, 0)
// 插入路由模式 "/users/:id"
parts4 := []string{"users", ":id"}
root.insert("/users/:id", parts4, 0)
// 插入路由模式 "/users/:id"
parts5 := []string{"files", "*filepath"}
root.insert("/files/*filepath", parts5, 0)
s1 := root.search([]string{"files", "cuwang.png"}, 0)
s2 := root.search([]string{"users", "1"}, 0)
s3 := root.search([]string{"p", "go", "intro"}, 0)
s4 := root.search([]string{"p", "java", "tutorial"}, 0)
s5 := root.search([]string{"ptt", "java", "tutorial"}, 0)
jsonData1, _ := json.MarshalIndent(s1, "", " ")
jsonData2, _ := json.MarshalIndent(s2, "", " ")
jsonData3, _ := json.MarshalIndent(s3, "", " ")
jsonData4, _ := json.MarshalIndent(s4, "", " ")
jsonData5, _ := json.MarshalIndent(s5, "", " ")
// 打印 JSON 数据
fmt.Println(string(jsonData1))
fmt.Println(string(jsonData2))
fmt.Println(string(jsonData3))
fmt.Println(string(jsonData4))
fmt.Println(string(jsonData5))
}
测试结果
=== RUN Test_node_matchChild
{
"Part": "*filepath",
"Children": null,
"IsWild": true
}
{
"Part": ":id",
"Children": null,
"IsWild": true
}
{
"Part": "intro",
"Children": null,
"IsWild": false
}
{
"Part": "tutorial",
"Children": null,
"IsWild": false
}
null
--- PASS: Test_node_matchChild (0.00s)
完整代码
package gee
import "strings"
type node struct {
pattern string `json: pattern` // 待匹配路由,例如 /p/:lang
Part string `json: Part` // 路由中的一部分,例如 :lang
Children []*node `json: Children` // 子节点,例如 [doc, tutorial, intro]
IsWild bool `json: IsWild` // 是否精确匹配,Part 含有 : 或IsWild * 时为true
}
// 第一个匹配成功的节点,用于插入
func (n *node) matchChild(Part string) *node {
for _, child := range n.Children {
if child.Part == Part || child.IsWild {
return child
}
}
return nil
}
// matchChildren 方法用于查找与给定路由部分(Part)匹配的所有子节点。
func (n *node) matchChildren(Part string) []*node {
// 创建一个空的节点切片,用于存储匹配成功的子节点。
nodes := make([]*node, 0)
// 遍历当前节点的所有子节点。
for _, child := range n.Children {
// 如果子节点的部分(Part)与给定部分相等,或者子节点是通配符节点(IsWild=true),
// 则将该子节点添加到 nodes 中,表示匹配成功。
if child.Part == Part || child.IsWild {
nodes = append(nodes, child)
}
}
// 返回包含匹配成功的子节点的切片。
return nodes
}
// insert 方法用于向路由树中插入新的路由模式。
func (n *node) insert(pattern string, Parts []string, height int) {
// 1⃣️ 如果递归到达路由模式的末尾,将当前节点的 pattern 字段设置为传入的 pattern。
if len(Parts) == height {
n.pattern = pattern
return
}
// 获取当前深度的路由部分。
Part := Parts[height]
// 2⃣️ 查找与当前路由部分匹配的子节点。
child := n.matchChild(Part)
// 3⃣️ 如果没有找到匹配的子节点,创建一个新的子节点,并将其添加到当前节点的子节点列表中。
if child == nil {
child = &node{Part: Part, IsWild: Part[0] == ':' || Part[0] == '*'}
n.Children = append(n.Children, child)
}
// 递归调用 insert 方法,处理下一个路由部分。
child.insert(pattern, Parts, height+1)
}
// search 方法用于在路由树中搜索与给定路由部分匹配的节点。
func (n *node) search(Parts []string, height int) *node {
// 如果递归到达路由部分的末尾或当前节点是通配符节点(以 * 开头 /files/*filepath),
// 并且当前节点的 pattern 字段不为空,则返回当前节点。
if len(Parts) == height || strings.HasPrefix(n.Part, "*") {
if n.pattern == "" {
return nil
}
return n
}
// 获取当前深度的路由部分。
Part := Parts[height]
//1⃣️ 查找与当前路由部分匹配的子节点。
Children := n.matchChildren(Part)
// 遍历匹配的子节点,递归调用 search 方法,处理下一个路由部分。
for _, child := range Children {
result := child.search(Parts, height+1)
if result != nil {
return result
}
}
// 如果没有找到匹配的节点,返回 nil 表示匹配失败。
return nil
}
将 Trie 树应用到路由中
Trie
树的插入与查找都成功实现了,接下来我们将 Trie
树应用到路由中去吧。
-
我们使用 roots 来存储每种请求方式的Trie 树根。``roots
:一个映射(map),用于存储不同路由模式的根节点(
node` 结构体的实例)。这里使用路由模式的方法名称(HTTP 方法,例如 GET、POST)作为键,对应的根节点作为值。每个方法都有一个独立的前缀树来处理相应的路由。 -
使用
handlers
存储每种请求方式的HandlerFunc
。一个映射,用于存储不同路由模式对应的处理函数(HandlerFunc)。这里使用路由模式的名称作为键,对应的处理函数作为值handlers key eg, handlers['GET-/p/:lang/doc'], handlers['POST-/p/book']
type router struct {
roots map[string]*node
handlers map[string]HandlerFunc
}
func newRouter() *router {
return &router{
roots: make(map[string]*node),
handlers: make(map[string]HandlerFunc),
}
}
其他的函数
parsePattern
解析函数
切割pattern 为 字符串切片,且通配符
*
只允许出现一次!且在末尾!
"/user/521" => ["user","521"]
"/files/xxxxxx" => [files,xxxxxx]
// parsePattern 函数用于解析路由模式字符串,返回一个字符串切片,表示路由模式的各个部分。
func parsePattern(pattern string) []string {
// 使用 "/" 字符分割路由模式字符串,得到一个切片。
vs := strings.Split(pattern, "/")
// 创建一个空的字符串切片,用于存储路由模式的各个部分。
parts := make([]string, 0)
// 遍历切片中的每个元素。
for _, item := range vs {
// 如果元素不为空,将其添加到 parts 切片中。
if item != "" {
parts = append(parts, item)
// 如果元素以 "*" 字符开头,表示后面的部分都是通配符,可以停止解析。
if item[0] == '*' {
break
}
}
}
// 返回包含路由模式各个部分的切片。
return parts
}
例如,对于路由模式 /users/:id/info/*path
,调用 parsePattern
函数后,返回的字符串切片可能是 ["users", ":id", "info", "*path"]
,这表示路由模式的各个部分被正确地解析出来,以供后续路由匹配使用。
package gee
import (
"fmt"
"testing"
)
func Test_parsePattern(t *testing.T) {
fmt.Println(parsePattern("/user/:id")) // [user :id]
fmt.Println(parsePattern("/user/getAllUsers")) // [user getAllUsers]
fmt.Println(parsePattern("/files/*/js/*")) // [files *]
fmt.Println(parsePattern("/files/*filepath")) // [files *filepath]
}
addRoute
: 添加路由
这段代码定义了一个名为
addRoute
的方法,用于向路由器添加路由规则。它将路由方法(例如 GET、POST)、路由模式和处理函数关联起来,以便后续的路由匹配和处理。
// addRoute 方法用于向路由器添加路由规则。
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
// 解析路由模式字符串,获取各个部分。
parts := parsePattern(pattern)
// 构建唯一的键,用于存储处理函数。
key := method + "-" + pattern
// 检查是否已经存在对应路由方法的根节点,如果不存在则创建一个。
_, ok := r.roots[method]
if !ok {
r.roots[method] = &node{}
}
// 调用根节点的 insert 方法,将路由模式插入到路由树中。
r.roots[method].insert(pattern, parts, 0)
// 将处理函数与键关联,存储到 handlers 映射中。
r.handlers[key] = handler
}
getRoute
: 获取路由
用于根据请求的方法和路径查找匹配的路由规则。它会返回一个路由节点以及与路由模式中的参数相对应的参数映射。
// getRoute 方法用于根据请求的方法和路径查找匹配的路由规则。
func (r *router) getRoute(method string, path string) (*node, map[string]string) {
// 解析请求路径,获取各个部分。
//"/user/521" => ["user","521"]
// "/files/xxxxxx" => [files,xxxxxx]
searchParts := parsePattern(path)
// 创建一个空的参数映射,用于存储与路由模式中的动态参数相对应的值。例如/user/:id 传入 /user/521 => params = {id:521}
params := make(map[string]string)
// 查找对应请求方法的根节点。
root, ok := r.roots[method]
// 如果找不到对应的根节点,返回 nil 表示没有匹配的路由规则。
if !ok {
return nil, nil
}
n := root.search(searchParts, 0)
// 调用根节点的 search 方法,查找匹配的最后节点。
// "/user/:id"
// n = {
// "Pattern": "/users/:id",
// "Part": ":id",
// "Children": null,
// "IsWild": true
// }
// "/files/*filepath"
// n = {
// "Pattern": "/files/*filepath",
// "Part": "*filepath",
// "Children": null,
// "IsWild": true
// }
// 如果找到匹配的节点,解析路由模式中的参数。
if n != nil {
// 这里拿到的是树中存的pattern!
//"/user/:id" => parts = [user,:id]
//"/files/*filepath" => parts = [files,*filepath]
parts := parsePattern(n.pattern)
for index, part := range parts {
// 如果路由模式中的部分以 ":" 开头,表示是一个参数,将其与实际路径部分关联起来。
// [user,:id] [user,521]
// index 为 0 part 为 user 两个动态匹配条件都不满足
// index 为 1 part 为 521
if part[0] == ':' {
// 拼装 :id 取出 id字段 = searchParts[index]
params[part[1:]] = searchParts[index] // id:[user,521][1] => {id:521}
}
// 如果路由模式中的部分以 "*" 开头且长度大于 1,表示是一个通配符参数,
// 将后续路径部分合并为一个值,并与通配符参数关联。
// [files,*filepath] [user,xxx.png]
// index 为 0 part 为 files 两个动态匹配条件都不满足
// index 为 1 part 为 *filepath
if part[0] == '*' && len(part) > 1 {
// 拼装 *filepath 取出 filepath 字段 = searchParts[index] = filepath = [filepath,xxx.png][1] => {filepath:xxx.png}
// 这里的strings.Join(searchParts[index:], "/")是为了多级文件目录匹配的情况 比如 /filepath/js/xxx.png 这样就可以把后面通配符的 目录 /js/xxx.png 连起来!
params[part[1:]] = strings.Join(searchParts[index:], "/")
break
}
}
return n, params
}
// 如果没有找到匹配的节点,返回 nil 表示没有匹配的路由规则。
return nil, nil
}
测试一下
func Test_GetRoute(t *testing.T) {
r := newTrieRouter()
r.addRoute("GET", "/user/:id", nil)
r.addRoute("PUT", "/files/*filepath", nil)
n1, m1 := r.getRoute("GET", "/user/32")
n2, m2 := r.getRoute("PUT", "/files/js/xxx.png")
n3, m3 := r.getRoute("PUT", "/files/xxx.png")
n4, m4 := r.getRoute("PUT", "/user/userlist")
fmt.Println(n1)
fmt.Println(m1)
fmt.Println(n2)
fmt.Println(m2)
fmt.Println(n3)
fmt.Println(m3)
fmt.Println(n4)
fmt.Println(m4)
}
=== RUN Test_GetRoute
&{/user/:id :id [] true}
map[id:32]
&{/files/*filepath *filepath [] true}
map[filepath:js/xxx.png]
&{/files/*filepath *filepath [] true}
map[filepath:xxx.png]
<nil>
map[]
Context与handle的变化
在 HandlerFunc 中,希望能够访问到解析的参数,因此,需要对 Context 对象增加一个属性和方法,来提供对路由参数的访问。我们将解析后的参数存储到Params
中,通过c.Param("lang")
的方式获取到对应的值。
// Context 结构体用于封装 HTTP 请求和响应的相关信息。
type Context struct {
// 原始的 HTTP 响应写入器和请求对象
Writer http.ResponseWriter
Req *http.Request
// 请求信息
Path string
Method string
Params map[string]string
// 响应信息
StatusCode int
}
// Param 方法用于从 Params 映射中获取指定键的参数值。
func (c *Context) Param(key string) string {
value, _ := c.Params[key]
return value
}
// handle 用于统一调用路由处理函数的方法。它的主要作用是根据请求的方法和路径,找到匹配的路由规则,并执行对应的路由处理函数
func (r *TrieRouter) handle(c *Context) {
// 调用 getRoute 方法,获取匹配的路由节点和参数映射。
n, params := r.getRoute(c.Method, c.Path)
// 如果找到匹配的路由节点,将参数映射设置到上下文中,然后执行对应的路由处理函数。
if n != nil {
c.Params = params
key := c.Method + "-" + n.Pattern
r.handlers[key](c)
} else {
// 如果没有找到匹配的路由规则,返回 404 NOT FOUND 响应。
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
}
}
测试demo
package main
import (
"gee"
"net/http"
)
func main() {
r := gee.New()
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
r.GET("/hello", func(c *gee.Context) {
// expect /hello?name=geektutu
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
})
r.GET("/hello/:name", func(c *gee.Context) {
// expect /hello/geektutu
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})
r.GET("/assets/*filepath", func(c *gee.Context) {
c.JSON(http.StatusOK, gee.H{"filepath": c.Param("filepath")})
})
r.Run(":9999")
}
❯ curl "http://localhost:9999/hello/cunwanglaodi"
hello cunwanglaodi, you're at /hello/cunwanglaodi
❯ curl "http://localhost:9999/assets/css/cunwanglaodi.css"
{"filepath":"css/cunwanglaodi.css"}
评论区