一、环境搭建
以Goland举例
1. 创建项目
【Q】 创建时go和go(gopath)的区别?
这两者的区别主要在于Go的模块管理方式。
- Go Modules(Go)
- 简介:Go Modules是Go语言的现代依赖管理系统,从Go 1.11开始引入,并在Go 1.13中成为默认的依赖管理方式。
- 特点:
- 模块化管理:使用go.mod文件来管理项目的依赖关系。
- 版本控制:支持版本化的依赖管理,可以指定依赖的版本。
- 不依赖GOPATH:项目可以在任何目录下创建,不需要放在GOPATH中。
- 更灵活:适合现代软件开发的需求,特别是对于多模块项目。
- GOPATH(Go (GOPATH))
- 简介:GOPATH是Go语言的传统工作区模式,在Go Modules出现之前是唯一的依赖管理方式。
- 特点:
- 固定目录结构:所有的Go代码必须放在GOPATH/src目录下。
- 全局依赖管理:所有项目共享同一个GOPATH,依赖管理不够灵活。
- 不支持版本控制:没有内置的版本管理机制,依赖管理较为原始。
- 逐渐被淘汰:随着Go Modules的普及,GOPATH模式逐渐被淘汰。
选择哪个?
Go Modules(Go):如果是Go 1.11及以后,推荐使用Go Modules,因为它提供了更现代化的依赖管理方式,支持版本控制,并且不依赖于GOPATH的固定目录结构。
GOPATH(Go (GOPATH)):仅在需要维护旧项目或特定情况下使用。
2. 按天创建包
- 给包取名:day1-http-base
- 第一天有三个示例需要展示
- 分别取名
base1
,base2
,base3
如下
- 分别取名
二、标准库net/http
以及http.Handler
接口
1. 开敲代码,看看效果
Go语言内置了 net/http
库,封装了HTTP网络编程的基础的接口,Gee Web 框架便是基于net/http
的。我们接下来通过一个例子,简单介绍下这个库的使用。
1.1 代码
文件结构:
day1-http-base/base1/main.go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", indexHandler) // 注册根路径的处理函数
http.HandleFunc("/hello", helloHandler) // 注册/hello路径的处理函数
log.Fatal(http.ListenAndServe(":9999", nil)) // 启动HTTP服务器,监听9999端口
}
func indexHandler(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path) // 输出请求的URL路径
}
func helloHandler(w http.ResponseWriter, req *http.Request) {
for k, v := range req.Header { // 遍历请求头
fmt.Fprintf(w, "Header[%q] = %q\n", k, v) // 输出每个请求头的键值对
}
}
1.2 测试及运行结果
1.2.1 测试工具
采用curl这个工具,进行测试。Curl 是一个命令行工具和库,用于通过URL传输数据。简单来说, Curl 就是一个可以通过命令行发送GET,POST 等多种协议请求的工具。curl 是 http 调试必备的一个命令行工具
Windows 10 中内置了 curl 工具。若没有,需要手动安装。以windows为例
- 去官网下载
.zip
文件
链接: curl官网
- 解压
- 配置环境变量
4. 验证
Win + R
输入cmd
打开命令行
输入curl --version
,得到版本信息即安装成功
1.2.2 运行
在Terminal中输入命令,得到下边的运行结果,即运行成功
$ curl http://localhost:9999/
URL.Path = "/"
$ curl http://localhost:9999/hello
Header["Accept"] = ["*/*"]
Header["User-Agent"] = ["curl/7.54.0"]
2. 对着代码解释一下
2.1 整体解释一下
main
函数:
- 使用
http.HandleFunc
为根路径/
和/hello
路径注册了两个处理函数indexHandler
和helloHandler
。 - 使用
http.ListenAndServe
启动HTTP
服务器,监听本地的9999
端口。如果服务器启动失败,log.Fatal
会记录错误并终止程序。
indexHandler
函数:
- 处理根路径/的请求。
- 使用
fmt.Fprintf
将请求的URL
路径写入响应中。
helloHandler
函数:
- 处理
/hello
路径的请求。 - 遍历请求的头部信息,并将每个头部的键值对写入响应中。
这个程序的功能是启动一个简单的HTTP服务器,能够响应两个不同路径的请求,并在响应中输出请求的相关信息。
2.2 请求头是什么,具体什么格式?
在HTTP协议中,请求头(Request Headers)是客户端发送给服务器的附加信息,用于提供关于客户端环境、请求的细节以及客户端期望的响应格式等信息。请求头以键值对的形式存在,每个请求头占一行,键和值之间用冒号分隔。
- 请求头的格式
Header-Name: Header-Value
- 常见的请求头
- Host: 指定请求的主机名和端口号。
示例:Host: www.example.com - User-Agent: 提供关于客户端软件的信息。
示例:User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3 - Accept: 指定客户端能够处理的内容类型。
示例:Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8 - Accept-Language: 指定客户端能够理解的自然语言。
示例:Accept-Language: en-US,en;q=0.5 - Accept-Encoding: 指定客户端能够理解的内容编码。
示例:Accept-Encoding: gzip, deflate, br - Connection: 控制连接的选项。
示例:Connection: keep-alive - Cookie: 发送存储在客户端的cookie。
示例:Cookie: sessionId=abc123
- 以下是一个完整的HTTP请求头示例:
GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: sessionId=abc123
2.3 w http.ResponseWriter
, req *http.Request
这两个是什么类型
这两个参数是Go语言中处理HTTP请求的核心部分,ResponseWriter
用于构建和发送响应,而Request
用于读取和解析请求。
http请求就记住:!!根据请求,构建相应!!
http.ResponseWriter
:
- 这是一个接口,提供了构建HTTP响应的方法。
- 通过
ResponseWriter
,你可以设置HTTP响应的头部信息、状态码,并将响应数据写入客户端。 - 常用的方法包括:
Write([]byte) (int, error)
: 将数据写入响应体。WriteHeader(statusCode int)
: 设置HTTP状态码。Header() http.Header
: 返回一个Header对象,用于设置响应头。
*http.Request
:
- 这是一个结构体指针,表示HTTP请求的详细信息。
- 包含了请求的各种信息,如请求方法、URL、头部、主体等。
- 常用的字段和方法包括:
Method string
: 请求的方法(如GET, POST)。URL *url.URL
: 请求的URL。Header Header
: 请求的头部信息。Body io.ReadCloser
: 请求的主体。FormValue(key string) string
: 获取表单参数的值。
2.4 %q
是什么
在Go语言中,%q是fmt包中格式化字符串的一种动词,用于格式化输出带引号的字符串或字符。具体来说:
- 字符串:当用于字符串时,%q会将字符串用双引号括起来,并对其中的特殊字符进行转义。
- 字符:当用于字符时,%q会将字符用单引号括起来,并对其中的特殊字符进行转义。
package main
import (
"fmt"
)
func main() {
str := "Hello, World!"
char := 'A'
fmt.Printf("String: %q\n", str) // 输出: String: "Hello, World!"
fmt.Printf("Character: %q\n", char) // 输出: Character: 'A'
}
三、在二的基础上抽象
main 函数的最后一行,是用来启动 Web 服务的,第一个参数是地址,:9999
表示在 9999 端口监听。而第二个参数则代表处理所有的HTTP请求的实例,nil 代表使用标准库中的实例处理。第二个参数,是基于net/http
标准库实现Web框架的入口。
第二个参数的类型是什么呢?通过查看net/http
的源码(如下)可以发现,Handler
是一个接口,需要实现方法 ServeHTTP
,也就是说,只要传入任何实现了 ServerHTTP
接口的实例,所有的HTTP请求,就都交给了该实例处理了。
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
1. 开敲代码,瞅瞅结果
文件结构:day1-http-base/base2/main.go
记得创建新包base2,该包下创建main.go
因为一个包只能有一个main.go
1.1 代码
package main
import (
"fmt"
"log"
"net/http"
)
// 定义一个空的结构体类型 Engine
type Engine struct{}
// 实现 http.Handler 接口的 ServeHTTP 方法
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/":
// 如果请求路径是根路径 "/", 返回请求的 URL 路径
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
case "/hello":
// 如果请求路径是 "/hello",遍历请求头部信息并写入响应
for k, v := range req.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
default:
// 对于其他路径,返回 404 错误信息
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
}
}
func main() {
// 创建一个 Engine 实例
engine := new(Engine)
// 启动 HTTP 服务器,监听 9999 端口,并使用 engine 处理请求
log.Fatal(http.ListenAndServe(":9999", engine))
}
1.2 瞅瞅结果
先运行main, 再curl请求,得到同样结果
$ curl http://localhost:9999/
URL.Path = "/"
$ curl http://localhost:9999/hello
Header["Accept"] = ["*/*"]
Header["User-Agent"] = ["curl/7.54.0"]
2. 详细谈谈
在这段代码中,我们定义了一个名为Engine
的空结构体,并为它实现了一个名为ServeHTTP
的方法。这个方法有两个参数:
ResponseWriter
:这是第一个参数,用于构建和发送HTTP响应。你可以通过它来设置响应的内容、状态码等。Request
:这是第二个参数,包含了HTTP请求的所有信息,比如请求的URL、头部信息(Header)、请求体(Body)等。
在main
函数中,我们使用http.ListenAndServe
启动了一个HTTP服务器,并将我们创建的engine
实例作为第二个参数传递给它。这意味着所有的HTTP请求都会被转发到我们的Engine实例进行处理。
2.1 为什么这样做?
在没有实现Engine
之前,我们通常使用http.HandleFunc
来为每个具体的路由(比如/hello)定义处理逻辑。==这种方式虽然简单,但每个路由都需要单独定义处理函数,缺乏统一的控制。通过实现Engine,我们创建了一个统一的入口来处理所有的HTTP请求。==这就像在门口设置了一个总控人员,所有的请求都要先经过他。这样,我们可以在一个地方定义路由规则,也可以在这里添加一些通用的处理逻辑,比如日志记录、错误处理等。
四、Gee框架的雏形
重新组织二、三的代码,搭建出整个框架的雏形。
最终的代码目录结构是这样的。
├─base1
├─base2
└─base3
└─gee
|--gee.go
|--go.mod
main.go
go.mod
1. 开敲代码,瞅瞅结果
1.1 文件结构
- 在base3中新建包gee(我们的Gee框架)
- 初始化gee模块:打开Terminal ,进入day1-http-base/base3/gee文件夹,初始化gee模块
go mod init gee
初始化成功后,文件结构变成
此时gee是一个模块,模块名为gee(!!记得,接下来还要创建其他模块,不要搞混!!模块名和文件夹名是没有联系的,可以不一样) - 初始化example模块: Terminal ,进入day1-http-base/base3文件夹,初始化example模块
go mod init example
结果
此时框架的模块已经建好。
1.2 代码
1.2.1 day1-http-base/base3/go.mod(看好是哪个mod文件,不要搞错文件路径)
module example
go 1.13
require gee v0.0.0 //添加
replace gee => ./gee //添加
- 在 go.mod 中使用 replace 将 gee 指向 ./gee
从 go 1.11 版本开始,引用相对路径的 package 需要使用上述方式。
【Q】若是显示依赖失败,可以看下Goland是否打开模块化
File–>Settings–>Go -->Go Modules
1.2.2 day1-http-base/base3/gee/gee.go
package gee
import (
"fmt"
"net/http"
)
// 定义一个处理函数类型,接收http.ResponseWriter和*http.Request作为参数
type HandlerFunc func(http.ResponseWriter, *http.Request)
// Engine结构体,包含一个路由映射表
type Engine struct {
router map[string]HandlerFunc
}
// New函数,创建并返回一个新的Engine实例
func New() *Engine {
return &Engine{router: make(map[string]HandlerFunc)}
}
// addRoute方法,向路由映射表中添加路由
func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
key := method + "-" + pattern // 生成路由的唯一键
engine.router[key] = handler // 将处理函数与路由键关联
}
// GET方法,注册GET请求的路由
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
engine.addRoute("GET", pattern, handler)
}
// POST方法,注册POST请求的路由
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
engine.addRoute("POST", pattern, handler)
}
// Run方法,启动HTTP服务器,监听指定的地址
func (engine *Engine) Run(addr string) (err error) {
return http.ListenAndServe(addr, engine)
}
// ServeHTTP方法,实现http.Handler接口,处理所有的HTTP请求
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 {
fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL) // 否则返回404错误
}
}
这就是Gee框架的核心代码。
- 定义处理函数类型
HandlerFunc
:这是一个函数类型,用于定义如何处理HTTP请求。开发者可以使用这个类型来编写自己的请求处理逻辑。
- 路由映射表
router
:在Engine
结构体中,我们创建了一个路由映射表。这个表的作用是将请求的路径和方法(如GET或POST)与相应的处理函数关联起来。- 键的构成:键由请求方法和路径组成,比如
GET-/
、GET-/hello
、POST-/hello
。这样,即使路径相同,只要请求方法不同,也可以映射到不同的处理函数。
- 注册路由
GET
和POST
方法:这些方法允许开发者将特定的路径和处理函数注册到路由映射表中。比如,当你调用engine.GET("/hello", handler)
时,框架会将/hello
路径与handler
函数关联起来。
- 启动服务器
Run
方法:这个方法是对http.ListenAndServe
的简单包装,用于启动HTTP服务器并监听指定的地址。
- 处理请求
- ServeHTTP方法:这是框架的核心功能。每当服务器接收到请求时,这个方法会被调用。
- 路径解析:首先,它会解析请求的路径和方法,生成一个键。
- 查找路由:然后,它会在路由映射表中查找这个键。
- 执行处理函数:如果找到对应的处理函数,就执行它。
- 返回404:如果找不到,就返回一个简单的404错误信息。
1.2.3 day1-http-base/base3/main.go
package main
import (
"fmt"
"gee" // 导入自定义的 gee 包
"net/http" // 导入 net/http 包用于处理 HTTP 请求和响应
)
func main() {
r := gee.New() // 创建一个新的 gee.Engine 实例
// 注册一个处理函数,处理根路径 "/" 的 GET 请求
r.GET("/", func(w http.ResponseWriter, req *http.Request) {
// 将请求的 URL 路径写入响应
fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
})
// 注册一个处理函数,处理 "/hello" 路径的 GET 请求
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)
}
})
// 启动 HTTP 服务器,监听 9999 端口
r.Run(":9999")
}
这就是Gee框架的使用。
如果你使用过gin
框架的话,肯定会觉得无比的亲切。gee
框架的设计以及API均参考了gin
。使用New()
创建 gee
的实例,使用 GET()
方法添加路由,最后使用Run()
启动Web
服务。这里的路由,只是静态路由,不支持/hello/:name
这样的动态路由,动态路由我们将在下一次实现。
1.3 运行
执行go run main.go,再用 curl 工具访问,结果与最开始的一致。
$ curl http://localhost:9999/
URL.Path = "/"
$ curl http://localhost:9999/hello
Header["Accept"] = ["*/*"]
Header["User-Agent"] = ["curl/7.54.0"]
curl http://localhost:9999/world
404 NOT FOUND: /world
至此,整个Gee框架的原型已经出来了。实现了路由映射表,提供了用户注册静态路由的方法,包装了启动服务的函数。当然,到目前为止,我们还没有实现比net/http标准库更强大的能力,不用担心,很快就可以将动态路由、中间件等功能添加上去了。