GET /card/:id
POST /card/:id
DELTE /card/:id
GET /card/:id/name
...
GET /card/:id/relations
1、Router框架
2、MVC类框架
net/http
包提供的就是这样的基础功能,写一个简单的http echo server
只需要30s。package main
import (...)
func echo(wr http.ResponseWriter, r *http.Request) {
msg, err := ioutil.ReadAll(r.Body)
if err != nil {
wr.Write([]byte("echo error"))
return
}
writeLen, err := wr.Write(msg)
if err != nil || writeLen != len(msg) {
log.Println(err, "write len:", writeLen)
}
}
func main() {
http.HandleFunc("/", echo)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err)
}
}
net/http
库就显得不太合适了。//Burrow: http_server.go
func NewHttpServer(app *ApplicationContext) (*HttpServer, error) {
...
server.mux.HandleFunc("/", handleDefault)
server.mux.HandleFunc("/burrow/admin", handleAdmin)
server.mux.Handle("/v2/kafka", appHandler{server.app, handleClusterList})
server.mux.Handle("/v2/kafka/", appHandler{server.app, handleKafka})
server.mux.Handle("/v2/zookeeper", appHandler{server.app, handleClusterList})
...
}
net/http
。只看上面这段代码似乎非常优雅,我们的项目里大概只有这五个简单的URI,所以我们提供的服务就是下面这个样子:/
/burrow/admin
/v2/kafka
/v2/kafka/
/v2/zookeeper
handleKafka()
这个函数一探究竟:func handleKafka(app *ApplicationContext, w http.ResponseWriter, r *http.Request) (int, string) {
pathParts := strings.Split(r.URL.Path[1:], "/")
if _, ok := app.Config.Kafka[pathParts[2]]; !ok {
return makeErrorResponse(http.StatusNotFound, "cluster not found", w, r)
}
if pathParts[2] == "" {
// Allow a trailing / on requests
return handleClusterList(app, w, r)
}
if (len(pathParts) == 3) || (pathParts[3] == "") {
return handleClusterDetail(app, w, r, pathParts[2])
}
switch pathParts[3] {
case "consumer":
switch {
case r.Method == "DELETE":
switch {
case (len(pathParts) == 5) || (pathParts[5] == ""):
return handleConsumerDrop(app, w, r, pathParts[2], pathParts[4])
default:
return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
}
case r.Method == "GET":
switch {
case (len(pathParts) == 4) || (pathParts[4] == ""):
return handleConsumerList(app, w, r, pathParts[2])
case (len(pathParts) == 5) || (pathParts[5] == ""):
// Consumer detail - list of consumer streams/hosts? Can be config info later
return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
case pathParts[5] == "topic":
switch {
case (len(pathParts) == 6) || (pathParts[6] == ""):
return handleConsumerTopicList(app, w, r, pathParts[2], pathParts[4])
case (len(pathParts) == 7) || (pathParts[7] == ""):
return handleConsumerTopicDetail(app, w, r, pathParts[2], pathParts[4], pathParts[6])
}
case pathParts[5] == "status":
return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], false)
case pathParts[5] == "lag":
return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], true)
}
default:
return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
}
case "topic":
switch {
case r.Method != "GET":
return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
case (len(pathParts) == 4) || (pathParts[4] == ""):
return handleBrokerTopicList(app, w, r, pathParts[2])
case (len(pathParts) == 5) || (pathParts[5] == ""):
return handleBrokerTopicDetail(app, w, r, pathParts[2], pathParts[4])
}
case "offsets":
// Reserving this endpoint to implement later
return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
}
// If we fell through, return a 404
return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
}
net/http
中默认的路由。在Go开源界应用最广泛的router是httpRouter
,很多开源的router框架都是基于httpRouter进行一定程度的改造的成果。关于httpRouter路由的原理,会在本章节的router一节中进行详细的阐释。http
的multiplexer。在上一节中我们通过对Burrow代码的简单学习,已经知道如何用http
标准库中内置的mux来完成简单的路由功能了。如果开发Web系统对路径中带参数没什么兴趣的话,用http
标准库中的mux
就可以。RESTful
是几年前刮起的API设计风潮,在RESTful
中除了GET和POST之外,还使用了HTTP协议定义的几种其它的标准化语义。具体包括:const (
MethodGet = "GET"
MethodHead = "HEAD"
MethodPost = "POST"
MethodPut = "PUT"
MethodPatch = "PATCH" // RFC 5789
MethodDelete = "DELETE"
MethodConnect = "CONNECT"
MethodOptions = "OPTIONS"
MethodTrace = "TRACE"
)
GET /repos/:owner/:repo/comments/:id/reactions
POST /projects/:project_id/columns
PUT /user/starred/:owner/:repo
DELETE /user/starred/:owner/:repo
mux
显然就力不从心了。httprouter
,或是基于httprouter
的变种对路由进行支持。前面提到的github的参数式路由在httprouter
中都是可以支持的。httprouter
中使用的是显式匹配,所以在设计路由的时候需要规避一些会导致路由冲突的情况,例如:冲突:
GET /user/info/:name
GET /user/:id
不冲突:
GET /user/info/:name
POST /user/:id
如果两个路由拥有一致的http方法(指 GET/POST/PUT/DELETE)和请求路径前缀,且在某个位置出现了A路由是wildcard(指:id这种形式)参数,B路由则是普通字符串,那么就会发生路由冲突
。路由冲突会在初始化阶段直接panic:panic: wildcard route ‘:id‘ conflicts with existing children in path ‘/user/:id‘
goroutine 1 [running]:
github.com/cch123/httprouter.(*node).insertChild(0xc4200801e0, 0xc42004fc01, 0x126b177, 0x3, 0x126b171, 0x9, 0x127b668)
/Users/caochunhui/go_work/src/github.com/cch123/httprouter/tree.go:256 +0x841
github.com/cch123/httprouter.(*node).addRoute(0xc4200801e0, 0x126b171, 0x9, 0x127b668)
/Users/caochunhui/go_work/src/github.com/cch123/httprouter/tree.go:221 +0x22a
github.com/cch123/httprouter.(*Router).Handle(0xc42004ff38, 0x126a39b, 0x3, 0x126b171, 0x9, 0x127b668)
/Users/caochunhui/go_work/src/github.com/cch123/httprouter/router.go:262 +0xc3
github.com/cch123/httprouter.(*Router).GET(0xc42004ff38, 0x126b171, 0x9, 0x127b668)
/Users/caochunhui/go_work/src/github.com/cch123/httprouter/router.go:193 +0x5e
main.main()
/Users/caochunhui/test/go_web/httprouter_learn2.go:18 +0xaf
exit status 2
httprouter
考虑到字典树的深度,在初始化时会对参数的数量进行限制,所以在路由中的参数数目不能超过255
,否则会导致httprouter无法识别后续的参数
。不过这一点上也不用考虑太多,毕竟URI是人设计且给人来看的,相信没有长得夸张的URI能在一条路径中带有200个以上的参数。*
号来进行通配,不过*
号开头的参数只能放在路由的结尾,例如下面这样:Pattern: /src/*filepath
/src/ filepath = ""
/src/somefile.go filepath = "somefile.go"
/src/subdir/somefile.go filepath = "subdir/somefile.go"
httprouter
来做简单的HTTP静态文件服务器。httprouter也支持对一些特殊情况下的回调函数进行定制
,例如404的时候:r := httprouter.New()
// 404
r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("oh no, not found"))
})
r.PanicHandler = func(w http.ResponseWriter, r *http.Request, c interface{}) {
log.Printf("Recovering from panic, Reason: %#v", c.(error))
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(c.(error).Error()))
}
httprouter
和众多衍生router使用的数据结构被称为压缩字典树(Radix Tree)
。读者可能没有接触过压缩字典树,但对字典树(Trie Tree)应该有所耳闻。下图是一个典型的字典树
结构:O(n)
,n可以认为是目标字符串的长度。为什么要这样做?字符串本身不像数值类型可以进行数值比较,两个字符串对比的时间复杂度取决于字符串长度。如果不用字典树来完成上述功能,要对历史字符串进行排序,再利用二分查找之类的算法去搜索,时间复杂度只高不低。可认为字典树是一种空间换时间的典型做法。每个字母都需要建立一个孩子节点
,这样会导致字典树的层数比较深,压缩字典树相对好地平衡了字典树的优点和缺点。是典型的压缩字典树
结构:PUT /user/installations/:installation_id/repositories/:repository_id
GET /marketplace_listing/plans/
GET /marketplace_listing/plans/:id/accounts
GET /search
GET /status
GET /support
补充路由:
GET /marketplace_listing/plans/ohyes
// 略去了其它部分的 Router struct
type Router struct {
// ...
trees map[string]*node
// ...
}
trees
中的key
即为HTTP 1.1的RFC中定义的各种方法,具体有:GET
HEAD
OPTIONS
POST
PUT
PATCH
DELETE
path: 当前节点对应的路径中的字符串
wildChild: 子节点是否为参数节点,即 wildcard node,或者说 :id 这种类型的节点
nType: 当前节点类型,有四个枚举值: 分别为 static/root/param/catchAll。
static // 非根节点的普通字符串节点
root // 根节点
param // 参数节点,例如 :id
catchAll // 通配符节点,例如 *anyway
indices:子节点索引,当子节点为非参数类型,即本节点的wildChild为false时,会将每个子节点的首字母放在该索引数组。说是数组,实际上是个string。
每一种方法对应的都是一棵独立的压缩字典树
,这些树彼此之间不共享数据。具体到我们上面用到的路由,PUT和GET是两棵树而非一棵。PUT
:r := httprouter.New()
r.PUT("/user/installations/:installation_id/repositories/:reposit", Hello)
PUT
对应的根节点就会被创建出来。把这棵PUT的树画出来:GET /marketplace_listing/plans
时,类似前面PUT的过程,GET树的结构如图GET /marketplace_listing/plans/:id/accounts
,新的路径与之前的路径有共同的前缀,且可以直接在之前叶子节点后进行插入,那么结果也很简单,插入后的树结构见图:id
这个节点只有一个字符串的普通子节点,所以indices
还依然不需要处理。GET /search
,这时会导致树的边分裂,GET /status和GET /support
也插入到树中。这时候会导致在search节点上再次发生分裂,最终结果见图1、在插入wildcard节点时,父节点的children数组非空且wildChild被设置为false。例如:GET /user/getAll和GET /user/:id/getAddr,或者GET /user/*aaa和GET /user/:id。
2、在插入wildcard节点时,父节点的children数组非空且wildChild被设置为true,但该父节点的wildcard子节点要插入的wildcard名字不一样。例如:GET /user/:id/info和GET /user/:name/info。
3、在插入catchAll节点时,父节点的children非空。例如:GET /src/abc和GET /src/*filename,或者GET /src/:id和GET /src/*filename。
4、在插入static节点时,父节点的wildChild字段被设置为true。
5、在插入static节点时,父节点的children非空,且子节点nType为catchAll。
GET /marketplace_listing/plans/ohyes
时,出现第4种冲突情况:它的父节点marketplace_listing/plans/
的wildChild字段为true。package main
import (
"fmt"
"github.com/julienschmidt/httprouter"
"net/http"
)
func main() {
// 生成httprouter对象
r := httprouter.New()
r.GET("/", newRouter)
// 服务启动将httprouter对象传入listenAndServer第二个参数
http.ListenAndServe(":8080", r)
}
func newRouter(wr http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprintln(wr, "hello")
}
原文:https://www.cnblogs.com/binHome/p/13068796.html