【新】7天用Go从零实现Web框架Gee教程-Day1
写在前面:
一年半前,这个系列就是学长推荐给我的,当时我还是初学 Go 的状态,不过学到一半没有继续下去,最近出于工作原因需要用到 Golang,翻找学习资料的时候忽然发现这个很久之前学的东西,很是喜欢,一年半时间过去,再次回望这个 Gee 系列,心中有了许多新的感悟,之前学的时候糊里糊涂,没有意识到研究这个具有的极大价值,算是朝花夕拾了。
Day 1
Gee 是一个 类 Gin 的 Web 框架
首先认识到出于开发一个 Web 框架这个目的来说,我们需要开发的是什么
我们是基于标准库 “net/http” 进行进一步开发,net/http 包已提供的能力有监听端口,路由映射(静态),解析/构建 HTTP 包(解析 HTTP Req,构建 HTTP Resp)
那么我们可以进一步开发的功能有很多,比如:动态路由映射(在原有的静态路由升级而来),鉴权,处理 cookie、headers 等,插件功能
那么我们在开发 Web 框架之前,需要熟悉 net/http 的能力边界
今天使用 net/http 实现了最基本的 Web 功能:接收请求并返回。
不过即使是这个最基本的功能,也用到了 net/http 的三种能力:端口监听、路由映射(静态)、解析/构建 HTTP 包
我们在对于 net/http 包的抽象基本能力的了解基础上,进一步熟悉实现这些能力具体的函数(包括它的定义、输入参数、输出等)
端口监听:http.ListenAndServe(addr string, handler Handler)
路由映射(静态):http.HandleFunc(pattern string, handler Func(http.ResponseWriter, *http.Request))
解析/构建 HTTP 包:http.Request、http.ResponseWriter 具备这俩功能的东西——“请求处理器” Handler,它本身是一个接口(接口就是用于描述一种东西)
type Handler interface { |
实现接受请求并返回
整合这些相关函数,我们得到了最简单的 Web 服务
package main |
通过终端执行命令 curl localhost:8000 来进行测试验证,控制台会打印 index
实现统一 Handler
看接下来的教程,发现就是用到了 Handler,不过原来 Handler 可以直接放到 http.ListenAndServe() 里面作为第二个参数
我们在 Handler 里可以统一管理路由映射,用 switch 就可以(之后在此基础上我们可以进一步实现动态路由)
func main() { |
Gee 框架雏形
在这步才算是有了一个框架的雏形,把文件结构重构了一下,但是我对于 go.mod 的作用还不是很熟悉,所以一看到重构后的文件结构有点懵
gee/ |
根目录的 go.mod 这里用到了替换依赖,但是为什么要使用呢?
replace gee => ./gee |
替换依赖了之后,在遇到 import gee 时就不会去找远程的库,而是直接找本地的库
我之前的写法踩了一个坑:Cannot use the unexported type 'engine' in the current package
engine 这个类首字母小写是 private 的,不能从包中导出,需要改成大写
在搭建 Gee 框架雏形的时候
大概需要增加的四个功能是:
1、路由表
2、Restful 风格的注册路由方法
3、引擎的创建方法
4、引擎的启动方法(封装 ListenAndServe)
路由表
需要给 Engine 配一个路由表,这里就联系了前面的小知识:对象相比函数而言可以存储数据,这点就很适合用来存储路由表
type Engine struct { |
Restful 风格的注册路由方法
// 注册 GET 路由方法 |
改造 ServeHTTP 方法
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
引擎的创建方法
func New() *Engine { |
引擎的启动方法(封装 ListenAndServe)
启动方法就绑定到 Engine 上好了,封装一下 http.ListenAndServe()
func (e *Engine) Run(addr string) { |
在 main 中使用 Gee 框架
func main() { |
成功实现
测试 GET 请求
curl localhost:8000 |
测试 POST 请求
curl -X POST localhost:8000 -d "name=Roxy&age=14" |
Questions
能不能导入 net 包,写成 net.ListenAndServe ?
import ( |
不行,虽然从文件结构上来说 net 文件夹包含了 http 文件夹,但是从包管理来看,net 包 和 net/http 包毫无关系,相互独立。
使用浏览器请求测试,打印两次 index
func main() { |
使用 Ghrome 浏览器 发起一次 localhost:8000 请求进行测试,打印两次 index
浏览器发送请求时会自动发送一个 localhost:8000/favicon.ico,打开开发者工具可以看到该请求
并且因为 “/“ 是通配符,所以两种路径的请求都会触发 indexHandler(通配符另外讨论)
可以通过 curl 命令发送纯净的请求进行测试
通配符
(待写)
.idea 文件夹中哪些文件需要提交,哪些不需要?
(待写)
为什么 http.HandleFunc() 的第二个参数不用 Handler 接口而用了一个类似的函数?
为什么 http.HandleFunc(pattern string, handler Func(http.ResponseWriter, *http.Request)) 的第二个参数不用 Handler 接口而用了一个类似的 函数?
其实没什么深意,或者说有深意,就是偷懒,是一种“语法糖” 标准做法是这样的:func Handle(pattern string, handler Handler) 简化做法:func HandleFunc(pattern string, func(http.ResponseWriter, *http.Request)) (其实关于这点在函数名 Handle 和 HandleFunc 里就可以窥见一二,Go 的发明者对于函数起名还是很讲究的)
因为如果我们要创建一个实现 Handler 接口的对象相对比较繁琐,需要这样写:
func main() { |
不过 函数 和 对象 还有其他区别,函数无法存储数据,只能输入输出;而对象可以存储数据,像图中的 name。所以说用对象这种方式的可操作空间更大
在这里创建 engine 的时候需要采用哪种方式?
一般有两种情况:一种是创建这个结构体的实例,一种是创建结构体的指针,对于 http.ListenAndServe() 来说两种情况都可以其实
需要注意的是,engine 和 *engine 是两种不同的类型,两者的方法集是独立的
结构体的示例
http.ListenAndServe(":8000", engine{name: "first"}) |
结构体的指针
e := new(engine) |
发现 http.ListenAndServe() 中的第二个参数既可以接收结构体实例,也可以接收结构体指针
但是需要注意一点——给结构体绑定方法时用的是 值接收者 还是 指针接收者。有一种情况下会出现错误,在 http.ListenAndServe() 中使用实例但给结构体绑定 ServeHTTP 方法时只用了指针接收者。
这里引出了 Golang 中很重要的概念:值接收者 和 指针接收者
我写的绑定方法是值接收者的形式
func (e engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
还有一种是指针接收者的形式
func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
给结构体绑定值接收者的方法,该结构体的实例和指针都可以使用这个方法,原因是指针调用值接收者的方法时会自动转为值进行操作;但是给结构体绑定指针接收者的方法,只有该结构体的指针可以使用这个方法,实例则不行,会编译错误
实际上,给结构体绑定指针接收者的方法,该结构体的实例的方法集中就不存在该方法,而给结构体绑定值接收者的方法的时候,结构体实例的方法集中自然有该方法,而结构体指针的方法集中也会自动加载该方法
那么值接收者的写法和指针接收者的写法有什么区别?
值接收者的形式
这里的 e 是一个拷贝值,在每次调用这个方法都会拷贝一份使用,不会影响原值
func (e engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
还有一种是指针接收者的形式
这里的 e 就是原值,可以修改
func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) { |
显然指针的可操作权限更大,值接收者能做到的指针接收者都能做到,反过来则不行,这也是为什么绑定值接收者时会自动给指针也绑定一份,而反过来不行
包的下载流程
(待写)
make 的用法
(待写)
Goland 不识别 main 函数(没有显示绿色小三角播放键)
Goland 只识别 main 包中的 main 函数,文件首行要写 package main
为什么 map 前一个参数放在 [] 里,而第二个不放?
第一个参数是索引,参考数组索引的写法。这样一想就很形象了。
RESTfulRegister(method string, pattern string, handlerFunc HandlerFunc) 的 第三个参数传入一个 func(w http.ResponseWriter, r *http.Request) 为什么可以,是自动转换类型了吗?
是的。在函数参数传递时对于作为参数的函数,只要函数签名是一样的,Go 就可以自动转换类型
写成 type HandlerFunc = func(w http.ResponseWriter, r *http.Request) 行吗?
对于目前的功能来说可行,有一点不同是这种写法 HandlerFunc 和 func(w http.ResponseWriter, r *http.Request) 完全等价,而 func 这个函数是在包外的(标准库的包),不能绑定新方法







