文章目录
- net/Http基础
- 框架雏形
- 上下文
学习文档:
Go语言标准库文档中文版
7天用Go从零实现Web框架Gee教程 | 极客兔兔 (geektutu.com)
gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang).
net/Http基础
go语言的http包提供了HTTP客户端和HTTP服务端的实现。下面是一个简单的HTTP服务端:
package mainimport ("fmt""net/http"
)func main() {http.HandleFunc("/hello",func(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "Hello World")}http.ListenAndServe(":8080", nil)
}
浏览器访问127.0.0.1:8080/hello可以看到Hello World。
func ListenAndServe(addr string, handler Handler) error
在go标准库文档中得知ListenAndServe监听TCP地址addr,并且会使用handler参数调用Serve函数处理接收到的连接。当handler参数为nil时会使用DefaultServeMux。
而DefaultServeMux是一个ServeMux类型的实例,ServeMux拥有一个SeveHTTP方法。
var DefaultServeMux = &defaultServeMuxvar defaultServeMux ServeMux
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
ListenAndServe函数中handler参数是Handle接口实现的实例,其中只有ServeHTTP这一个方法。 ListenAndServe函数只要传入任何实现 ServerHTTP接口的实例,所有的HTTP请求,就会交给该实例处理。
type Handler interface {ServeHTTP(ResponseWriter, *Request)
}
框架雏形
事已至此,先创建个文件夹吧!给框架命名为kilon,目录结构如下:
myframe/├── kilon/│ ├── go.mod [1]│ ├── kilon.go├── go.mod [2]├── main.go
从gin框架的实现(gin/gin.go)中可以看到gin引擎主要有以下方法:
func New(opts ...OptionFunc) *Engine // 创建一个新的引擎对象
func (origin *Engine) addRoute(method, path string, handlers HandlersChain) // 向引擎对象添加路由信息
func (origin *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) // 实现ServeHttP接口
func (origin *Engine) Run(addr ...string) (err error) // 启动 HTTP 服务器并监听指定的地址和端口
在kilon.go中模仿gin.go添加下面代码:
package kilonimport ("fmt""net/http"
)
// 给函数起别名,方便作为参数调用
type HandlerFunc func(http.ResponseWriter, *http.Request)
// 引擎对象
type Origin struct {// 存储路由信息的map,key为路由信息,这里使用Method + "-" + Path的格式,value为路由绑定的方法router map[string]HandlerFunc
}
// 创建一个新的引擎对象
func New() *Origin {// make函数用于slice、map以及channel的内存分配和初始化return &Origin{router: make(map[string]HandlerFunc)}
}
// 向引擎对象添加路由信息,并绑定函数。
func (origin *Origin) addRoute(method string, pattern string, handler HandlerFunc) {// Method + "-" + Path 构造key值key := method + "-" + pattern// 插入需要绑定的函数origin.router[key] = handler
}
// 向引擎对象注册GET、POST方法的路由信息,进行封装,降低参数数量,并提高使用时的代码可读性。
func (origin *Origin) GET(pattern string, hander HandlerFunc) {origin.addRoute("GET", pattern, hander)
}
func (origin *Origin) POST(pattern string, hander HandlerFunc) {origin.addRoute("POST", pattern, hander)
}
// 实现ServeHttP接口
func (origin *Origin) ServeHTTP(w http.ResponseWriter, req *http.Request) {// 从请求中携带的参数构造pathkey := req.Method + "-" + req.URL.Path // 如果该路由信息已经注册,则调用绑定的函数if handler, ok := origin.router[key]; ok {// 调用绑定的函数handler(w, req)} else {// 路由没注册,返回 404fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)}
}
// 启动 HTTP 服务器并监听指定的地址和端口
func (origin *Origin) Run(addr string) (err error) {// 本质是调用http包的ListenAndServe启动。(实现了接口方法的 struct 可以自动转换为接口类型)return http.ListenAndServe(addr, origin)
}
至此,框架的雏形已经搭建好,实现了路由映射表、提供了用户注册方法以及包装了启动服务函数。下面在main.go中进行测试:
在go.mod [2] 中添加下面内容 (先使用指令go mod init
生成go.mod):
replace kilon => ./kilon
这行代码告诉go mod 在导包时不从网上寻找包,而是使用当前目录下的kilon包。
在main.go中添加代码:
package mainimport ("fmt""kilon""net/http"
)func main() {r := kilon.New()r.GET("/hello", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "Hello World")})r.Run(":8080") // 省略地址指监听所有网络的8080端口
}
运行指令:go mod tidy
,后可以在go.mod [2]看到:
module myframego 1.22.1replace kilon => ./kilonrequire kilon v0.0.0-00010101000000-000000000000 // 成功导入
运行代码后,访问127.0.0.1:8080/hello可以看到Hello World。
上下文
r.GET("/hello", func(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "Hello World")
}) // 框架雏形路由注册
框架雏形的使用还是有点麻烦,所有的请求与响应内容还有数据格式的处理都需要用户来实现。这时候需要引入上下文的概念来解决这个问题。上下文"(context)指的是 HTTP 请求的上下文环境,它包含了当前请求的各种信息,比如请求的 URL、HTTP 方法、请求头、请求体等等。
r.GET("/hello", func(c *gin.Context) {c.JSON(200, gin.H{"message": "Hello World",})
}) // Gin框架路由注册
在 Gin 框架中,上下文由 gin.Context
( gin/context.go)类型表示,它是对 HTTP 请求的抽象,提供了丰富的方法来处理请求和响应。每当收到一个 HTTP 请求,Gin 就会创建一个新的上下文对象,然后将该对象传递给对应的处理函数。开发者可以通过这个上下文对象,方便地获取请求的相关信息(如动态路由信息,中间件信息等),以及向客户端发送响应数据。
新建context.go文件设计上下文对象,当前目录结构为:
myframe/├── kilon/│ ├── context.go│ ├── go.mod [1]│ ├── kilon.go├── go.mod [2]├── main.go
具体实现:
创建一个名为Context的struct,写入需要便捷获取到的请求属性
type Context struct {Writer http.ResponseWriterReq *http.RequestPath string // 请求路径,从req中获取Method string // 请求方法,从req中获取StatusCode int // 响应状态码,由用户输入
}
写入构造方法,在构造方法中从req获取到对应属性:
func newContext(w http.ResponseWriter, req *http.Request) *Context {return &Context{Writer: w,Req: req,Path: req.URL.Path, // 从req中获取请求路径Method: req.Method, // 从req中获取请求方法}
}
包装方法,让用户可以通过Context对象获取到req的信息与设置响应内容,而无需关心Context内部信息。
// 从req的post表单中获取指定键的值。
func (c *Context) PostForm(key string) string {return c.Req.FormValue(key)
}
// 用于从req的URL查询参数中获取指定键的值。
func (c *Context) Query(key string) string {return c.Req.URL.Query().Get(key)
}
// 设置响应状态码
func (c *Context) Status(code int) {c.StatusCode = codec.Writer.WriteHeader(code)
}
// 发送响应数据
func (c *Context) Data(code int, data []byte) {c.Status(code)c.Writer.Write(data)
}
Content-Type
是 HTTP 头部中的一个字段,用于指示发送给客户端的实体正文的媒体类型。在标准的 HTTP 协议中,Content-Type
可以有很多种取值,常见的包括:
text/plain
: 表示纯文本内容。text/html
: 表示 HTML 格式的文档。application/json
: 表示 JSON 格式的数据。application/xml
: 表示 XML 格式的数据。application/octet-stream
: 表示二进制流数据。image/jpeg
: 表示 JPEG 格式的图片。image/png
: 表示 PNG 格式的图片。
在框架雏形中如果用户想返回这些类型的格式数据,需要自己设置响应头部信息并将数据编码成对应格式。接下来在框架中包装多个方法,让用户可以简单的调用对应方法,就可以将数据以需要的格式响应。
// 设置HTTP响应头部的指定字段和值
func (c *Context) SetHeader(key string, value string) {c.Writer.Header().Set(key, value)
}
// 纯文本内容,可变参数values用于实现fmt.Sprintf的包装
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...)))
}
// HTML格式内容
func (c *Context) HTML(code int, html string) {c.SetHeader("Content-Type", "text/html")c.Status(code)c.Writer.Write([]byte(html))
}
// Json格式内容
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)}
}
当用户想调用JSON方法时,每次都需要自己定义一个接口对象再写入数据,还是不够方便。这个时候可以给接口对象起一个别名:
type H map[string]interface{}
这样就可以在调用的时候直接像gin框架那样直接gin.H{}写入json数据了。
现在Context.go中内容如下:
package kilonimport ("encoding/json""fmt""net/http"
)type H map[string]interface{}type Context struct {Writer http.ResponseWriterReq *http.RequestPath stringMethod stringStatusCode 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 = codec.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))
}
现在Context.go已经写好了,但是框架引擎的函数调用还是:
type HandlerFunc func(http.ResponseWriter, *http.Request)
现在需要将其换成Context对象参数。
type HandlerFunc func(*Context)
并且ServeHTTP中调用逻辑需要改写:
func (origin *Origin) ServeHTTP(w http.ResponseWriter, req *http.Request) {c := newContext(w, req) // 创建一个Context实例key := req.Method + "-" + req.URL.Pathif handler, ok := origin.router[key]; ok {handler(c) // 现在路由注册的函数参数已经换成了Context对象} else {fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)}
}
至此上下文对象已经完成了,在main.go中测试一下:
package mainimport ("kilon""net/http"
)func main() {r := kilon.New()r.GET("/hello0", func(ctx *kilon.Context) {ctx.Data(http.StatusOK, []byte("Hello World"))})r.GET("/hello1", func(ctx *kilon.Context) {ctx.String(http.StatusOK, "Hello %s", "World")// ctx.String(http.StatusOK, "Hello World")})r.GET("/hello2", func(ctx *kilon.Context) {ctx.JSON(http.StatusOK, kilon.H{"message": "Hello World",})})r.GET("/hello3", func(ctx *kilon.Context) {ctx.HTML(http.StatusOK, "<h1>Hello World</h1>")})r.Run(":8080")
}
代码运行后访问对应地址可以看到不同结果。