hello,elf-web Go语言内置了 net/http
库,封装了HTTP网络编程的基础的接口,本项目实现的 Web 框架便是基于net/http
的。当前 Go 代码创建了一个简单的 Web 服务器,监听端口 8080 并处理两种 HTTP 请求:
1. 根路径请求 (“/“) ,调用indexHandler
函数。
2. “/hello” 路径请求 ,调用helloHandler
函数。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "fmt" "net/http" "time" ) func main () { http.HandleFunc("/" , indexHandler) http.HandleFunc("/hello" , helloHandler) http.ListenAndServe(":8080" , nil ) } func indexHandler (w http.ResponseWriter, req *http.Request) { w.Write([]byte ("url.path: " + req.RequestURI)) } func helloHandler (w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello,elf-web" , time.Now()) }
http.Response
和 http.ResponseWriter
都是 Go 语言 net/http
包中处理 HTTP 请求的关键类型,但它们的角色和功能不同:
http.Response
http.ResponseWriter
是一个接口(指向底层具体类型值的指针) ,表示 HTTP 响应的编写器。 服务器使用它来构建发送给客户端的响应。
作为参数传递给 HTTP 处理程序函数。 处理程序函数可以使用 http.ResponseWriter
的方法来设置响应的状态码、头信息和写入响应体数据。
不会直接创建 http.Response
对象。 相反,它提供方法来设置响应的各个部分,最终由 Go 的 net/http
包使用这些信息来构建 http.Response
并发送给客户端。
http.Response
是服务器发送给客户端的完整响应,你可以读取它来获取响应信息。
http.ResponseWriter
是一个接口,服务器用它来构建发送给客户端的响应,你可以使用它来设置响应信息。
http.Handler http.Handler
是 Go 语言 net/http
包中的一个核心接口,它定义了处理 HTTP 请求的标准方法。 任何实现了 http.Handler
接口的类型都可以用于处理 HTTP 请求。当你创建一个 HTTP 服务器时,需要将 Handler
注册到特定的路由上,以便服务器知道如何处理对应路径的请求。
接口定义:
1 2 3 type Handler interface { ServeHTTP(ResponseWriter, *Request) }
ServeHTTP(ResponseWriter, *Request)
: 这是 Handler
接口的唯一方法。它接收两个参数:
ResponseWriter
: 用于写入 HTTP 响应的对象。你可以使用它设置响应头、状态码和响应体。
*Request
: 表示传入 HTTP 请求的对象,包含请求方法、URL、头部信息和请求体等数据。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type Engine struct {}func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { url := req.URL.Path switch url { case "/" : w.Write([]byte ("url.path: " + url)) case "/hello" : fmt.Fprintln(w, "hello,elf-web" , time.Now()) } } func main () { http.ListenAndServe(":8080" , new (Engine)) }
elf 封装http,搭建出框架雏形,代码结构:
1 2 3 4 5 elf/ |--elf.go |--go.mod main.go go.mod
查看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package elfimport ( "fmt" "log" "net/http" "strings" ) const ( patternPrefix = "/" routeSeparator = "-" ) type HandlerFunc func (w http.ResponseWriter, req *http.Request) type Engine struct { router map [string ]HandlerFunc } func New () *Engine { return &Engine{router: make (map [string ]HandlerFunc)} } func (engine *Engine) addRouter(method string , pattern string , handler HandlerFunc) { if !strings.HasPrefix(pattern, patternPrefix) { pattern = patternPrefix + pattern } key := method + routeSeparator + pattern log.Printf("[elf] Route %4s - %s" , method, pattern) engine.router[key] = handler } func (engine *Engine) GET(pattern string , handler HandlerFunc) { engine.addRouter("GET" , pattern, handler) } func (engine *Engine) POST(pattern string , handler HandlerFunc) { engine.addRouter("POST" , pattern, handler) } func (engine *Engine) Run(addr string ) error { return http.ListenAndServe(addr, engine) } func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { url := req.URL.Path method := req.Method key := method + routeSeparator + url if handler, ok := engine.router[key]; ok { handler(w, req) } else { w.WriteHeader(http.StatusNotFound) fmt.Fprint(w, "404 Page Not Found: " , req.URL) } }
1 2 3 // ./elf/go.mod module elf go 1.22.2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport ( "elf" "fmt" "net/http" ) func main () { r := elf.New() r.GET("/" , func (w http.ResponseWriter, req *http.Request) { w.Write([]byte ("url.path: " + req.RequestURI)) }) r.GET("hello" , func (w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, "hello,elf" ) }) r.POST("hello" , func (w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, "hello,elf" ) }) r.Run(":8080" ) }
1 2 3 4 5 // ./go.mod module elf-web go 1.22.2 require elf v0.0.0 replace elf => ./elf
context 对Web服务来说,构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复(如以下代码所示),繁杂的代码,而且容易出错。
1 2 3 4 5 6 7 8 9 10 obj = map [string ]interface {}{ "name" : "tom" , "password" : "1234" , } w.Header().Set("Content-Type" , "application/json" ) w.WriteHeader(http.StatusOK) encoder := json.NewEncoder(w) if err := encoder.Encode(obj); err != nil { http.Error(w, err.Error(), 500 ) }
封装*http.Request
和http.ResponseWriter
使得 web 框架的使用和扩展更加方便,是现代 web 开发中的一种最佳实践。
简化代码 :将 HTTP 请求和响应的处理逻辑封装到 Context 中,使得业务逻辑代码更简洁。
提高代码可读性 :通过封装,代码变得更具结构化,开发者可以更直观地理解每个方法的功能。
代码重用性 :封装后的方法可以在项目中的不同地方重复使用,减少代码冗余。
方便扩展 :如果需要添加新的功能或改进现有功能,只需修改 Context 及其方法即可,不需要改动其他业务逻辑代码。
一致性 :通过统一的接口处理不同类型的响应(如 JSON、HTML、纯文本等),确保响应的一致性和可维护性。
封装Context
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package elfimport ( "encoding/json" "fmt" "net/http" ) type H map [string ]interface {}type Context struct { Writer http.ResponseWriter Req *http.Request Path string Method string StatusCode int } func newContext (w http.ResponseWriter, req *http.Request) *Context { return &Context{ Writer: w, Req: req, Path: req.URL.Path, Method: req.Method, } } func (context *Context) SetHeader(key string , value string ) { context.Writer.Header().Add(key, value) } func (context *Context) SetStatus(code int ) { context.StatusCode = code context.Writer.WriteHeader(code) } func (context *Context) Form(key string ) string { return context.Req.FormValue(key) } func (context *Context) Query(key string ) string { return context.Req.URL.Query().Get(key) } func (context *Context) String(code int , format string , values ...interface {}) { context.SetHeader("Context-Type" , "text/plain" ) context.SetStatus(code) context.Writer.Write([]byte (fmt.Sprintf(format, values...))) } func (c *Context) Json(code int , obj interface {}) { c.SetHeader("Context-Type" , "application/json" ) c.SetStatus(code) encoding := json.NewEncoder(c.Writer) if err := encoding.Encode(obj); err != nil { http.Error(c.Writer, err.Error(), http.StatusInternalServerError) } } func (c *Context) Data(code int , data []byte ) { c.SetStatus(code) c.Writer.Write(data) } func (c *Context) Html(code int , html string ) { c.SetHeader("Context-Type" , "text/html" ) c.SetStatus(code) c.Writer.Write([]byte (html)) }
router 封装路由相关的方法和结构,router.go
,方便对 router
的功能进行增强。
封装Router
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package elfimport ( "log" "net/http" "strings" ) const ( patternPrefix = "/" routeSeparator = "-" ) type router struct { handlers map [string ]HandlerFunc } func newRouter () *router { return &router{ handlers: make (map [string ]HandlerFunc), } } func (r *router) addRouter(method string , pattern string , handler HandlerFunc) { if !strings.HasPrefix(pattern, patternPrefix) { pattern = patternPrefix + pattern } key := method + routeSeparator + pattern log.Printf("[elf] Route %4s - %s" , method, pattern) r.handlers[key] = handler } func (r *router) handle(c *Context) { key := c.Method + routeSeparator + c.Path if handler, ok := r.handlers[key]; ok { handler(c) } else { c.String(http.StatusNotFound, "404 NOT FOUND: %s\n" , c.Path) } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package elfimport ( "net/http" ) type HandlerFunc func (c *Context) type Engine struct { router *router } func New () *Engine { return &Engine{router: newRouter()} } func (engine *Engine) addRouter(method string , pattern string , handler HandlerFunc) { engine.router.addRouter(method, pattern, handler) } func (engine *Engine) GET(pattern string , handler HandlerFunc) { engine.addRouter("GET" , pattern, handler) } func (engine *Engine) POST(pattern string , handler HandlerFunc) { engine.addRouter("POST" , pattern, handler) } func (engine *Engine) Run(addr string ) error { return http.ListenAndServe(addr, engine) } func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := newContext(w, req) engine.router.handle(c) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func main () { r := elf.New() r.GET("/html" , func (c *elf.Context) { c.Html(http.StatusOK, "<h1>hello,elf</h1>" ) }) r.GET("json" , func (c *elf.Context) { c.Json(http.StatusOK, elf.H{ "msg" : "ok" , }) }) r.GET("/data" , func (c *elf.Context) { c.Data(http.StatusOK, []byte ("hello,elf" )) }) r.GET("/query" , func (c *elf.Context) { name := c.Query("name" ) age := c.Query("age" ) c.String(http.StatusOK, "user is %s, age is %s" , name, age) }) r.POST("/save" , func (c *elf.Context) { name := c.Form("name" ) age := c.Form("age" ) c.Json(http.StatusOK, elf.H{ "msg" : "ok" , "name" : name, "age" : age, }) }) r.Run(":8080" ) }
trie tree 前缀树用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 type Node struct { next map [rune ]*Node isWord bool } type Trie struct { size int root *Node } func NewTrie () *Trie { return &Trie{ size: 0 , root: &Node{ isWord: false , next: make (map [rune ]*Node), }, } } func (t *Trie) Insert(word string ) { node := t.root for _, ch := range word { if _, ok := node.next[ch]; !ok { node.next[ch] = &Node{ isWord: false , next: map [rune ]*Node{}, } } node = node.next[ch] } if !node.isWord { node.isWord = true t.size += 1 } } func (t *Trie) SearchWord(word string ) bool { node := t.root for _, ch := range word { if _, ok := node.next[ch]; !ok { return false } node = node.next[ch] } return node.isWord } func (t *Trie) StartWith(prefix string ) bool { node := t.root for _, ch := range prefix { if _, ok := node.next[ch]; !ok { return false } node = node.next[ch] } return true } func (t *Trie) WordQuantity() int { return t.size }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 trie := elf.NewTrie() words := []string {"hello" , "golang" , "gin" , "elf" , "gorm" , "java" , "spring" , "mysql" , "redis" , "kafka" } for _, word := range words { trie.Insert(word) } fmt.Printf("trie.size: %v\n" , trie.WordQuantity()) fmt.Println("==================search word========================" ) fmt.Printf("search word for 'hello' %v\n" , trie.SearchWord("hello" )) fmt.Printf("search word for 'world' %v\n" , trie.SearchWord("world" )) fmt.Printf("search word for 'golang' %v\n" , trie.SearchWord("golang" )) fmt.Println("==================search prefix======================" ) fmt.Printf("search prefix for 'go' %v\n" , trie.StartWith("go" )) fmt.Printf("search prefix for 'ha' %v\n" , trie.StartWith("ha" ))
HTTP请求的路径是由/
分隔的多段构成的,因此,每一段可以作为前缀树的一个节点。可通过树结构优化动态路由匹配。
Trie实现
1 2 3 4 5 6 type node struct { pattern string part string children []*node isWild bool }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 func (n *node) matchFirst(part string ) *node { for _, child := range n.children { if child.part == part || child.isWild { return child } } return nil } func (n *node) insert(pattern string , parts []string , height int ) { if len (parts) == height { n.pattern = pattern return } part := parts[height] child := n.matchFirst(part) if child == nil { child = &node{ part: part, isWild: part[0 ] == ':' || part[0 ] == '*' , } n.children = append (n.children, child) } child.insert(pattern, parts, height+1 ) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func (n *node) matchChildren(part string ) []*node { nodes := make ([]*node, 0 ) for _, child := range n.children { if child.part == part || child.isWild { nodes = append (nodes, child) } } return nodes } func (n *node) search(parts []string , height int ) *node { if len (parts) == height || strings.HasPrefix(n.part, "*" ) { if n.pattern == "" { return nil } return n } part := parts[height] children := n.matchChildren(part) for _, child := range children { rst := child.search(parts, height+1 ) if rst != nil { return rst } } return nil }
插入节点: 递归查找每层节点,如果没有匹配当前part的节点,则新建一个。需注意的是,对于路径/p/:lang/doc,只有在第三层doc节点时,pattern才会设置为/p/:lang/doc,p和:lang节点的pattern属性为空。因此,匹配结束时可以通过n.pattern == ““来判断路由规则是否匹配成功。例如,/p/python虽匹配到:lang,但由于:lang的pattern为空,因此匹配失败。
查询节点: 同样递归查询每层节点。退出条件为:匹配到*,匹配失败,或匹配到第len(parts)层节点。
将Trie加入路由中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 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), } } func parsePattern (pattern string ) []string { split := strings.Split(pattern, patternPrefix) parts := make ([]string , 0 ) for _, item := range split { if item != "" { parts = append (parts, item) if item[0 ] == '*' { break } } } return parts } func (r *router) addRouter(method string , pattern string , handler HandlerFunc) { if !strings.HasPrefix(pattern, patternPrefix) { pattern = patternPrefix + pattern } log.Printf("[elf] Route %s - %s" , method, pattern) if _, ok := r.roots[method]; !ok { r.roots[method] = &node{} } parts := parsePattern(pattern) r.roots[method].insert(pattern, parts, 0 ) key := method + routeSeparator + pattern r.handlers[key] = handler } func (r *router) getRouter(method string , path string ) (*node, map [string ]string ) { searchParts := parsePattern(path) root, ok := r.roots[method] if !ok { return nil , nil } params := make (map [string ]string ) node := root.search(searchParts, 0 ) if node != nil { parts := parsePattern(node.pattern) for index, part := range parts { if part[0 ] == ':' { params[part[1 :]] = searchParts[index] } if part[0 ] == '*' && len (part) > 1 { params[part[1 :]] = strings.Join(searchParts[index:], "/" ) break } } } return node, params } func (r *router) handle(c *Context) { node, params := r.getRouter(c.Method, c.Path) if node != nil { c.Params = params key := c.Method + routeSeparator + node.pattern r.handlers[key](c) } else { c.String(http.StatusNotFound, "404 NOT FOUND: %s\n" , c.Path) } }
使用 roots
存储每种请求方式的 Trie 树根节点,使用 handlers
存储每种请求方式的 HandlerFunc。在 getRoute
函数中,解析 :
和 *
两种匹配符,并返回一个包含参数的 map。例如,/p/go/doc
匹配到 /p/:lang/doc
,解析结果为 {lang: "go"}
;/static/css/geektutu.css
匹配到 /static/*filepath
,解析结果为 {filepath: "css/geektutu.css"}
。
group 使用 RouterGroup
提供了一种有效的机制来组织和管理 Web 应用程序的路由,使代码更加模块化、易于维护和扩展。
查看代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 type RouterGroup struct { prefix string middleware []HandlerFunc parent *RouterGroup engine *Engine } type Engine struct { *RouterGroup router *router groups []*RouterGroup } func New () *Engine { engine := &Engine{router: newRouter()} engine.RouterGroup = &RouterGroup{engine: engine} engine.groups = []*RouterGroup{engine.RouterGroup} return engine } func (group *RouterGroup) Group(prefix string ) *RouterGroup { newGroup := &RouterGroup{ prefix: prefix, parent: group, engine: group.engine, } group.engine.groups = append (group.engine.groups, newGroup) return newGroup } func (group *RouterGroup) addRouter(method string , pattern string , handler HandlerFunc) { group.engine.router.addRouter(method, group.prefix+pattern, handler) } func (group *RouterGroup) GET(pattern string , handler HandlerFunc) { group.addRouter("GET" , pattern, handler) } func (group *RouterGroup) POST(pattern string , handler HandlerFunc) { group.addRouter("POST" , pattern, handler) }
GitHub仓库: https://github.com/HlkL/golang-learn/tree/elf-web