【Golang学习笔记】从零开始搭建一个Web框架(一)

学习文档:

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 main

import (
	"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 = &defaultServeMux

var 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 kilon

import (
	"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) {
    // 从请求中携带的参数构造path
	key := req.Method + "-" + req.URL.Path 
    // 如果该路由信息已经注册,则调用绑定的函数
	if handler, ok := origin.router[key]; ok {
        // 调用绑定的函数
		handler(w, req)
	} else {
        // 路由没注册,返回 404
		fmt.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 main

import (
	"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 myframe

go 1.22.1

replace kilon => ./kilon

require 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.Contextgin/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.ResponseWriter
	Req        *http.Request
	Path       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 = code
	c.Writer.WriteHeader(code)
}
// 发送响应数据
func (c *Context) Data(code int, data []byte) {
	c.Status(code)
	c.Writer.Write(data)
}

Content-Type 是 HTTP 头部中的一个字段,用于指示发送给客户端的实体正文的媒体类型。在标准的 HTTP 协议中,Content-Type 可以有很多种取值,常见的包括:

  1. text/plain: 表示纯文本内容。
  2. text/html: 表示 HTML 格式的文档。
  3. application/json: 表示 JSON 格式的数据。
  4. application/xml: 表示 XML 格式的数据。
  5. application/octet-stream: 表示二进制流数据。
  6. image/jpeg: 表示 JPEG 格式的图片。
  7. 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 kilon

import (
	"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))
}

现在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.Path
	if handler, ok := origin.router[key]; ok {
		handler(c) // 现在路由注册的函数参数已经换成了Context对象
	} else {
		fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
	}
}

至此上下文对象已经完成了,在main.go中测试一下:

package main

import (
	"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")
}

代码运行后访问对应地址可以看到不同结果。

  • 39
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值