写在前面:

一年半前,这个系列就是学长推荐给我的,当时我还是初学 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.Requesthttp.ResponseWriter 具备这俩功能的东西——“请求处理器” Handler,它本身是一个接口(接口就是用于描述一种东西)

type Handler interface {
ServeHTTP(http.ResponseWriter, *http.Request)
}

实现接受请求并返回

整合这些相关函数,我们得到了最简单的 Web 服务

package main

import (
"fmt"
"log"
"net/http"
)

func main() {
s := "gopher"
fmt.Printf("Hello and welcome, %s!\n", s)

http.HandleFunc("/", indexHandler)
log.Fatal(http.ListenAndServe(":8000", nil))
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("index")
}

通过终端执行命令 curl localhost:8000 来进行测试验证,控制台会打印 index

dccc302c7a206d8221eef8182bc05936

bf04e5830d503ee4744813be84ee6ac0

实现统一 Handler

看接下来的教程,发现就是用到了 Handler,不过原来 Handler 可以直接放到 http.ListenAndServe() 里面作为第二个参数

我们在 Handler 里可以统一管理路由映射,用 switch 就可以(之后在此基础上我们可以进一步实现动态路由)

func main() {
log.Fatal(http.ListenAndServe(":8000", engine{name:"first"}))
}

type engine struct {
name string
}

func (e engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Println("Engine:" + e.name)
path := r.URL.Path
switch path {
case "/":
fmt.Fprintf(w, "index")
case "/hello":
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%s]:%s\n", k, v)
}
default:
fmt.Fprintf(w, "Not Found:%s\n", path)
}
}

Gee 框架雏形

在这步才算是有了一个框架的雏形,把文件结构重构了一下,但是我对于 go.mod 的作用还不是很熟悉,所以一看到重构后的文件结构有点懵

gee/
|--gee.go
|--go.mod
main.go
go.mod

根目录的 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 {
Name string
router map[string]HandlerFunc // 路由表
}

Restful 风格的注册路由方法

// 注册 GET 路由方法
func (e *Engine) GET(pattern string, handlerFunc HandlerFunc) {
e.RESTfulRegister("GET", pattern, handlerFunc)
}

// 注册 POST 路由方法
func (e *Engine) POST(pattern string, handlerFunc HandlerFunc) {
e.RESTfulRegister("POST", pattern, handlerFunc)
}

// 统一路由方法
func (e *Engine) RESTfulRegister(method string, pattern string, handlerFunc HandlerFunc) {
rest_addr := method + "-" + pattern
e.router[rest_addr] = handlerFunc
}

改造 ServeHTTP 方法

func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
method := r.Method
handler := e.router[method+"-"+path]
// 解决路由表中查不到的情况
if handler == nil {
fmt.Println("404 Not Found")
} else {
handler(w, r)
}
}

引擎的创建方法

func New() *Engine {
return &Engine{
Name: "second",
router: make(map[string]HandlerFunc),
}
}

引擎的启动方法(封装 ListenAndServe)

启动方法就绑定到 Engine 上好了,封装一下 http.ListenAndServe()

func (e *Engine) Run(addr string) {
log.Println("Engine-" + e.Name + " runs")
log.Fatal(http.ListenAndServe(addr, e))
}

在 main 中使用 Gee 框架

func main() {
e := gee.New()
e.GET("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Println("GET request to /")
})

e.POST("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Println("POST request to /")
r.ParseForm()
for k, v := range r.PostForm {
fmt.Fprintf(w, "PostForm[%q]:%q\n", k, v)
}
})

e.Run(":8000")
}

成功实现

测试 GET 请求

curl localhost:8000

image-20260525043654102

测试 POST 请求

curl -X POST localhost:8000 -d "name=Roxy&age=14"

image-20260525043605976

Questions

能不能导入 net 包,写成 net.ListenAndServe ?

import (
"net"
)

func main() {
net.ListenAndServe(":8080", nil);
}

不行,虽然从文件结构上来说 net 文件夹包含了 http 文件夹,但是从包管理来看,net 包 和 net/http 包毫无关系,相互独立。

使用浏览器请求测试,打印两次 index

func main() {
s := "gopher"
fmt.Printf("Hello and welcome, %s!\n", s)

http.HandleFunc("/", indexHandler)
log.Fatal(http.ListenAndServe(":8000", nil))
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
fmt.Println("index")
}

使用 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() {
http.Handle("/", handler{name: "handler-1"})
log.Fatal(http.ListenAndServe(":8000", nil))
}

type handler struct {
name string
}

func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Println("Handle:" + h.name)
}

13b28369512f581df5a6f0222f2e87d0

不过 函数 和 对象 还有其他区别,函数无法存储数据,只能输入输出;而对象可以存储数据,像图中的 name。所以说用对象这种方式的可操作空间更大

在这里创建 engine 的时候需要采用哪种方式?

一般有两种情况:一种是创建这个结构体的实例,一种是创建结构体的指针,对于 http.ListenAndServe() 来说两种情况都可以其实

需要注意的是,engine 和 *engine 是两种不同的类型,两者的方法集是独立的

结构体的示例

http.ListenAndServe(":8000", engine{name: "first"})

结构体的指针

e := new(engine)
e.name = "first"
http.ListenAndServe(":8000", e)

// 等价形式(主流用法)
http.ListenAndServe(":8000", &engine{name: "first"})

发现 http.ListenAndServe() 中的第二个参数既可以接收结构体实例,也可以接收结构体指针

但是需要注意一点——给结构体绑定方法时用的是 值接收者 还是 指针接收者。有一种情况下会出现错误,在 http.ListenAndServe() 中使用实例但给结构体绑定 ServeHTTP 方法时只用了指针接收者

这里引出了 Golang 中很重要的概念:值接收者 和 指针接收者

我写的绑定方法是值接收者的形式

func (e engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Println("Engine:" + e.name)
path := r.URL.Path
switch path {
case "/":
fmt.Fprintf(w, "index")
case "/hello":
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%s]:%s\n", k, v)
}
default:
fmt.Fprintf(w, "Not Found:%s\n", path)
}
}

还有一种是指针接收者的形式

func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Println("Engine:" + e.name) // 注意到这里取 e 的字段的写法不变
path := r.URL.Path
switch path {
case "/":
fmt.Fprintf(w, "index")
case "/hello":
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%s]:%s\n", k, v)
}
default:
fmt.Fprintf(w, "Not Found:%s\n", path)
}
}

给结构体绑定值接收者的方法,该结构体的实例和指针都可以使用这个方法,原因是指针调用值接收者的方法时会自动转为值进行操作;但是给结构体绑定指针接收者的方法,只有该结构体的指针可以使用这个方法,实例则不行,会编译错误

实际上,给结构体绑定指针接收者的方法,该结构体的实例的方法集中就不存在该方法,而给结构体绑定值接收者的方法的时候,结构体实例的方法集中自然有该方法,而结构体指针的方法集中也会自动加载该方法

那么值接收者的写法和指针接收者的写法有什么区别?

值接收者的形式

这里的 e 是一个拷贝值,在每次调用这个方法都会拷贝一份使用,不会影响原值

func (e engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
switch path {
case "/":
e.name = "second"
fmt.Println("Engine:" + e.name)
fmt.Fprintf(w, "index")
case "/hello":
fmt.Println("Engine:" + e.name)
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%s]:%s\n", k, v)
}
default:
fmt.Fprintf(w, "Not Found:%s\n", path)
}
}

// 验证
// localhost:8000/hello
// localhost:8000
// localhost:8000/hello

image-20260524182821107

还有一种是指针接收者的形式

这里的 e 就是原值,可以修改

func (e *engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
switch path {
case "/":
e.name = "second"
fmt.Println("Engine:" + e.name)
fmt.Fprintf(w, "index")
case "/hello":
fmt.Println("Engine:" + e.name)
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%s]:%s\n", k, v)
}
default:
fmt.Fprintf(w, "Not Found:%s\n", path)
}
}

// 验证
// localhost:8000/hello
// localhost:8000
// localhost:8000/hello

image-20260524182712590

显然指针的可操作权限更大,值接收者能做到的指针接收者都能做到,反过来则不行,这也是为什么绑定值接收者时会自动给指针也绑定一份,而反过来不行

包的下载流程

(待写)

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 这个函数是在包外的(标准库的包),不能绑定新方法