通过学习项目 7_days_golang搭建各种自己的框架了解一些网络工作原理和知识 学习geektutu源代码:https://github.com/geektutu/7days-golang
跟着学习的项目代码已经放在: https://github.com/Whuichenggong/Study_Go
1.gee.go 关键代码
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 package geeimport ( "fmt" "net/http" ) type HandlerFunc func (http.ResponseWriter, *http.Request) type Engine struct { router map [string ]HandlerFunc } func New () *Engine { return &Engine{router: make (map [string ]HandlerFunc)} } func (engine *Engine) addRoute(method string , pattern string , handler HandlerFunc) { key := method + pattern engine.router[key] = handler } func (engine *Engine) GET(pattern string , handler HandlerFunc) { engine.addRoute("GET" , pattern, handler) } func (engine *Engine) POST(pattern string , handler HandlerFunc) { engine.addRoute("POST" , pattern, handler) } func (engine *Engine) Run(addr string ) (err error ) { return http.ListenAndServe(addr, engine) } func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { key := req.URL.Path if handler, ok := engine.router[key]; ok { handler(w, req) } else { fmt.Fprintf(w, "404 Not Found: %s\n" , req.URL) } }
2.go.mod代码 1 2 3 4 5 6 7 8 module github.com/Whuichenggong go 1.22.1 require gee v0.0.0 replace gee => ./gee
replace gee => ./gee
这是一个替换指令,它告诉 Go 工具链用本地相对路径 ./gee 中的 gee 包替换远程需要的 gee 包。 这意味着,尽管 require 指令可能指向一个特定的远程版本或分支, 这个 replace 指令实际上将使用当前目录下的 gee 文件夹中的代码。
2.1初始化 Go 模块: 如果你的项目还没有被初始化为 Go 模块,你需要先在项目的根目录下运行以下命令来初始化它:
go mod init <module-name>
替换 为你的模块名称。例如,如果你的项目名称是 example,你会运行:
go mod init example
3.main.go 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 package main import ( "fmt" "net/http" "gee" ) func main() { r := gee.New() r.GET("/", func(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path) }) r.GET("/hello", func(w http.ResponseWriter, req *http.Request) { for k, v := range req.Header { fmt.Fprintf(w, "Header[%q] = %q\n", k, v) } }) r.Run(":9999") }
新增: 测试 POST 请求 启动服务器后,测试 POST 请求可以使用以下工具:
方法 1: 使用 curl 执行以下命令发送 POST 请求:
curl -X POST http://localhost:8080/submit
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 package mainimport ("fmt" "io/ioutil" "net/http" ) type HandlerFunc func (http.ResponseWriter, *http.Request) type Engine struct {router map [string ]HandlerFunc } func New () *Engine {return &Engine{router: make (map [string ]HandlerFunc)}} func (engine *Engine) addRoute(method string , pattern string , handler HandlerFunc) {key := method + pattern engine.router[key] = handler } func (engine *Engine) POST(pattern string , handler HandlerFunc) {engine.addRoute("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) {key := req.Method + req.URL.Path if handler, ok := engine.router[key]; ok {handler(w, req) } else { http.NotFound(w, req) } }
func main() { engine := New()
// 注册一个 POST 路由
engine.POST("/submit", func(w http.ResponseWriter, req *http.Request) {
// 读取请求体数据
body, err := ioutil.ReadAll(req.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}
// 响应请求体内容
fmt.Fprintf(w, "Received: %s", string(body))
})
// 启动服务器
engine.Run(":8080")
} 测试: 启动程序后,用 curl 发送 POST 请求并附带数据:
b curl -X POST -d “data=HelloWorld” http://localhost:8080/submit 服务器返回:
kotlin
Received: data=HelloWorld
ServeHTTP好像有点问题 main.go 附带了对代码的理解
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 geeimport ( "fmt" "net/http" ) type HandlerFunc func (http.ResponseWriter, *http.Request) type Engine struct { router map [string ]HandlerFunc } func New () *Engine { return &Engine{router: make (map [string ]HandlerFunc)} } func (engine *Engine) addRoute(method string , pattern string , handler HandlerFunc) { key := method + pattern engine.router[key] = handler } func (engine *Engine) GET(pattern string , handler HandlerFunc) { engine.addRoute("GET" , pattern, handler) } func (engine *Engine) POST(pattern string , handler HandlerFunc) { engine.addRoute("POST" , pattern, handler) } func (engine *Engine) Run(addr string ) (err error ) { return http.ListenAndServe(addr, engine) } func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { key := req.URL.Path if handler, ok := engine.router[key]; ok { handler(w, req) } else { fmt.Fprintf(w, "404 Not Found: %s\n" , req.URL) } }
问题 在 ServeHTTP 中,当前只从 req.URL.Path 获取路径, 而没有结合 req.Method,会导致不同的 HTTP 方法(如 GET 和 POST)冲突或无法正确匹配。 addRoute 方法仅使用了路径(pattern)和方法(method)拼接为路由键,例如:GET/home。
1 2 3 4 func (engine *Engine) addRoute(method string , pattern string , handler HandlerFunc) { key := method + "-" + pattern engine.router[key] = handler }
修改的这段代码 原来只使用了路径(req.URL.Path)作为路由键。例如:
请求路径 /hello 的键为 /hello。 不区分 GET /hello 和 POST /hello,它们会共用同一个路由键 /hello。 建议的代码 使用 HTTP 方法和路径 拼接成路由键。例如:
GET /hello 的键为 GET-/hello。 POST /hello 的键为 POST-/hello。 这样可以区分不同方法对应的路由处理函数。
与gin框架启动很相似
对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。 但是这两个对象提供的接口粒度太细,比如我们要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode), 消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装, 那么框架的用户将需要写大量重复,繁杂的代码 且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。
代码要学会封装 否则代码整洁度看起来还是会差很多的 对于别人理解一会更方便
为什么要添加context 对于框架来说,还需要支撑额外的功能。例如,将来解析动态路由/hello/:name,参数:name的值放在哪呢? 再比如,框架需要支持中间件,那中间件产生的信息放在哪呢?
contxet保留了你想寻找的一些东西 拓展性和复杂性留在内部 对外简化了接口。
Context 的作用是为每个 HTTP 请求提供一个上下文对象, 方便操作请求和响应,并提供了一些简化开发的工具方法。 通过 Context 统一管理 HTTP 请求和响应的逻辑。
可以把 Context 看作是:
一个请求的容器: 它封装了与 HTTP 请求相关的所有信息,并提供了一些方法让你更轻松地操作这些信息。
开发者和 HTTP 请求的桥梁: 开发者通过 Context 与客户端通信,包括读取请求信息和发送响应。
1 2 3 4 5 6 7 8 9 10 11 12 func handler (c *Context) {name := c.Query("name" ) if name != "" { c.JSON(http.StatusOK, H{"message" : "Hello " + name}) } else { c.String(http.StatusBadRequest, "Name is required" ) } }
深入框架原理: 阅读 Gin、Echo 等框架的源码,了解它们如何设计和扩展 Context。
尝试扩展功能: 在 Context 上添加自定义方法,比如记录日志、追踪请求 ID 等。
http.ResponseWriter 和 *http.Request 的实际意义 http.ResponseWriter
作用: 代表服务端用来写入 HTTP 响应的接口。开发者通过它向客户端返回数据(如响应头、响应状态码、响应体等)。 实际应用: 在服务端,http.ResponseWriter 将生成的 HTTP 响应数据写入 TCP 连接的输出流,客户端会接收到这些数据并解析呈现。 *http.Request
作用: 表示客户端发来的 HTTP 请求,包含了所有请求相关的信息(如 URL、方法、头部、表单数据、Cookie、Body 等)。 实际应用: 服务端根据 *http.Request 的内容(路径、方法等),判断客户端的需求并生成相应的响应。
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" ) func handler (w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type" , "text/plain" ) w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Hello, %s!\n" , req.URL.Query().Get("name" )) } func main () { http.HandleFunc("/" , handler) http.ListenAndServe(":8080" , nil ) }
客户端请求示例:
浏览器访问 http://localhost:8080/?name=zhaozhonghe
服务端响应:
HTTP/1.1 200 OK //设置的状态码 200 Content-Type: text/plain //设置的请求头 响应过来了 并且返回到了 客户端页面 Content-Length: 12
Hello, zhaozhonghe! // 读取 HTTP 请求 将数据写入响应体,通过 w 发送给客户端。
第二天 1.添加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 74 75 76 77 78 79 80 package geeimport ( "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 (c *Context) PostForm(key string ) string { return c.Req.FormValue(key) } func (c *Context) Query(key string ) string { return c.Req.URL.Query().Get(key) } func (c *Context) Status(code int ) { c.StatusCode = code c.Writer.WriteHeader(code) } func (c *Context) SetHeader(key string , value string ) { c.Writer.Header().Set(key, value) } func (c *Context) String(code int , format string , values ...interface {}) { c.SetHeader("Content-Type" , "text/plain" ) c.Status(code) c.Writer.Write([]byte (fmt.Sprintf(format, values...))) } func (c *Context) JSON(code int , obj interface {}) { c.SetHeader("Content-Type" , "application/json" ) c.Status(code) encoder := json.NewEncoder(c.Writer) if err := encoder.Encode(obj); err != nil { http.Error(c.Writer, err.Error(), 500 ) } } func (c *Context) Data(code int , data []byte ) { c.Status(code) c.Writer.Write(data) } func (c *Context) HTML(code int , html string ) { c.SetHeader("Content-Type" , "text/html" ) c.Status(code) c.Writer.Write([]byte (html)) }
2.添加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 package geeimport ( "log" "net/http" ) type router struct { handlers map [string ]HandlerFunc } func newRouter () *router { return &router{handlers: make (map [string ]HandlerFunc)} } func (r *router) addRoute(method string , pattern string , handler HandlerFunc) { log.Printf("Route %4s - %s" , method, pattern) key := method + "-" + pattern r.handlers[key] = handler } func (r *router) handle(c *Context) { key := c.Method + "-" + c.Path if handler, ok := r.handlers[key]; ok { handler(c) } else { c.String(http.StatusNotFound, "404 NOT FOUND: %s\n" , c.Path) } }
r.handlers[key] = handler 这段代码将key也就是路径 和 处理函数关连到了一起
post用终端请求 1. Invoke-WebRequest -Uri “http://localhost:9999/login “ -Method POST -Body “username=zhaozhonghe&password=zzh123456”
curl.exe -X POST -d “username=zhaozhonghe&password=zzh123456” http://localhost:9999/login 返回结果1 {"password":"zzh123456","username":"zhaozhonghe"}
测试第二天的gee 第一种返回结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 StatusCode : 200 StatusDescription : OK Content : {"password":"zzh123456","username":"zhaozhonghe"} RawContent : HTTP/1.1 200 OK Content-Length: 50 Content-Type: application/json Date: Tue, 26 Nov 2024 13:43:37 GMT {"password":"zzh123456","username":"zhaozhonghe"} Forms : {} Headers : {[Content-Length, 50], [Content-Type, application/json], [Date, Tue, 26 Nov 2024 13:43:37 GMT]} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 50
gee.go 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 package geeimport "net/http" type HandlerFunc func (*Context) type Engine struct { router *router } func New () *Engine { return &Engine{router: newRouter()} } func (engine *Engine) addRoute(method string , pattern string , handler HandlerFunc) { engine.router.addRoute(method, pattern, handler) } func (engine *Engine) GET(pattern string , handler HandlerFunc) { engine.addRoute("GET" , pattern, handler) } func (engine *Engine) POST(pattern string , handler HandlerFunc) { engine.addRoute("POST" , pattern, handler) } func (engine *Engine) Run(addr string ) (err error ) { return http.ListenAndServe(addr, engine) } func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := newContext(w, req) engine.router.handle(c) }
第三天 我们用了一个非常简单的map结构存储了路由表,使用map存储键值对,索引非常高效,但是有一个弊端,键值对的存储的方式,只能用来索引静态路由。 那如果我们想支持类似于/hello/:name这样的动态路由怎么办呢? 所谓动态路由,即一条路由规则可以匹配某一类型而非某一条固定的路由。 例如/hello/:name,可以匹配/hello/geektutu、hello/jack等。 请等待~~~
11.21日看到了字节的课 是关于动态路由的设计 前缀匹配树
router.go 前缀树路由: 重点学习这个数据结构
bilibili: https://www.bilibili.com/video/BV1wT4y1x7xm?t=45.6
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 package geeimport ( "net/http" "strings" ) 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 { vs := strings.Split(pattern, "/" ) parts := make ([]string , 0 ) for _, item := range vs { if item != "" { parts = append (parts, item) if item[0 ] == '*' { break } } } return parts } 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{} } r.roots[method].insert(pattern, parts, 0 ) r.handlers[key] = handler } func (r *router) getRoute(method string , path string ) (*node, map [string ]string ) { searchParts := parsePattern(path) params := make (map [string ]string ) root, ok := r.roots[method] if !ok { return nil , nil } n := root.search(searchParts, 0 ) if n != nil { parts := parsePattern(n.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 n, params } return nil , nil } func (r *router) getRoutes(method string ) []*node { root, ok := r.roots[method] if !ok { return nil } nodes := make ([]*node, 0 ) root.travel(&nodes) return nodes } func (r *router) handle(c *Context) { n, params := r.getRoute(c.Method, c.Path) if n != nil { c.Params = params key := c.Method + "-" + n.pattern r.handlers[key](c) } else { c.String(http.StatusNotFound, "404 NOT FOUND: %s\n" , c.Path) } }
parsePattern 函数的作用是解析路由路径,将路径按 / 分隔成各个部分。比如 /user/:id 会被分解成 [“user”, “:id”]。 如果路径中出现了 *(通常用于匹配任意多的路径部分),解析会在遇到 * 时停止。比如 /files/*filepath 会解析成 [“files”, “*filepath”]。 parts 数组存储了路由路径的各个部分(如静态部分、动态部分、通配符部分)
tire.go 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 82 83 84 package geeimport ( "fmt" "strings" ) type node struct { pattern string part string children []*node isWild bool } func (n *node) String() string { return fmt.Sprintf("node{pattern=%s, part=%s, isWild=%t}" , n.pattern, n.part, n.isWild) } func (n *node) insert(pattern string , parts []string , height int ) { if len (parts) == height { n.pattern = pattern return } part := parts[height] children := n.matchChildren(part) var child *node if len (children) == 0 { child = &node{part: part, isWild: part[0 ] == ':' || part[0 ] == '*' } n.children = append (n.children, child) } else { child = children[0 ] } child.insert(pattern, parts, height+1 ) } 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 { result := child.search(parts, height+1 ) if result != nil { return result } } return nil } func (n *node) travel(list *([]*node)) { if n.pattern != "" { *list = append (*list, n) } for _, child := range n.children { child.travel(list) } } func (n *node) matchChild(part string ) *node { for _, child := range n.children { if child.part == part || child.isWild { return child } } return nil } 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 }
先学习一下前缀树
定义树结点结构体 1 2 3 4 5 type trieNode struct { nexts [26 ]*trieNode PassCnt int end bool }
树
1 2 3 type Trie struct { root *trieNode }
1 2 3 4 5 func Newtrie *Trie { return &Trie{ root: &trieNode{}, } }
查询 1 2 3 4 5 func (t *Trie) Search(word string ) bool { node := t.search(word) return node != nil && node.end }
Tire.search 方法源码 字符➖a 如果返回的单词是 前缀树中的别的单词的前缀判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func (t *Trie) search(target string )*trieNode{move :t.root /依次追历target中的每个字符 for_, ch:range target{ if move.nexts[ch-'a' ]==nil {return nil } movemove.nexts [ch-'a' ] } return move} }
前缀匹配 1 2 3 4 func (t *Trie) StartWith(prefix(string )) bool { return t.search(prefix) != nil }
前缀统计 1 2 3 4 5 6 7 8 9 func (t *Trie) PassCnt(prefix string ) int { node := t.search(prefix) if node == nil { return 0 } return node.PassCnt }
插入单词 例子: 要插入apple 树中app可以复用 则插入 l e
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func (t *Trie) Insert(word string ) { if t.Search(word){ return } move := t.root for _,ch := range word { if move.nexts[ch-'a' ] == nil { move.nexts[ch-'a' ] = &trieNode{} } move.nexts[ch-'a' ].passCnt++ move = move.nexts[ch-'a' ] move.end =true }
删除流程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func (t *Trie) Erase(word string ) bool { if !t.Search(word){ return false } move := t.root for _, ch := range word { move.nexts[ch-'a' ].passCnt -- if move.nexts[ch-'a' ].passCnt == 0 { move.nexts[ch-'a' ] = nil return true } move = move.nexts[ch-'a' ] } move.end = false return true }
整段代码下来还是有点看不懂啊呜呜
11.25日 拖了几天 感谢tutu
分组控制 分组控制是Web框架的基础功能之一,路由的分组,往往某一组路由需要相似的处理
以/post开头的路由匿名可访问。 以/admin开头的路由需要鉴权。 以/api开头的路由是 RESTful 接口,可以对接第三方平台,需要三方平台鉴权。
/post是一个分组 /post/a和/post/b可以是该分组下的子分组 作用在/post分组上的中间件(middleware),也都会作用在子分组,子分组还可以应用自己特有的中间件。
中间件可以给框架提供无限的扩展能力 用在分组上的效果也更明显 /admin的分组,可以应用鉴权中间件;/分组应用日志中间件, /是默认的最顶层的分组,也就意味着给所有的路由,即整个框架增加了记录日志的能力。
一个 Group 对象需要具备哪些属性呢?首先是前缀(prefix), 比如/,或者/api;要支持分组嵌套,那么需要知道当前分组的父亲(parent)是谁; 中间件是应用在分组上的,那还需要存储应用在该分组上的中间件(middlewares)。
1 2 3 4 5 r := gee.New() v1 := r.Group("/v1" ) v1.GET("/" , func (c *gee.Context) { c.HTML(http.StatusOK, "<h1>Hello Gee</h1>" ) })
好好看看仓库中的代码 梳理思路 感觉好有意思但是看不懂哈哈哈哈
11.26日 回看前三天的代码 增加一些自己的理解和修改 再继续向下学习!