首页
学习
活动
专区
圈层
工具
发布
16 篇文章
1
Go 语言 Web 编程系列(一)—— 快速入门:创建第一个 Web 应用
2
Go 语言 Web 编程系列(二)—— HTTP 请求处理的底层运行机制
3
Go 语言 Web 编程系列(三)—— 基于官方标准库自定义路由处理器
4
Go 语言 Web 编程系列(四)—— 基于 gorilla/mux 包实现路由定义:基本使用篇
5
Go 语言 Web 编程系列(五)—— 基于 gorilla/mux 包实现路由匹配:进阶使用篇
6
Go 语言 Web 编程系列(六)—— 基于 gorilla/mux 包实现路由匹配:路由中间件
7
Go 语言 Web 编程系列(七)—— 基于 gorilla/mux 包实现路由匹配:处理静态资源响应
8
Go 语言 Web 编程系列(八)—— 基于 gorilla/mux 包实现路由匹配:服务 SPA 应用
9
Go 语言 Web 编程系列(九)—— 基于 gorilla/mux 包实现路由匹配:通过 CORS 处理跨域请求
10
Go 语言 Web 编程系列(十)—— 基于 gorilla/mux 包实现路由匹配:健康检查与接口测试
11
Go 语言 Web 编程系列(十一)—— 仿照 Laravel 框架对 Go 路由代码进行拆分
12
Go 语言 Web 编程系列(十二)—— 通过 Request 读取 HTTP 请求报文
13
Go 语言 Web 编程系列(十三)—— 获取用户请求数据(上)
14
Go 语言 Web 编程系列(十四)—— 获取用户请求数据(下)
15
Go 语言 Web 编程系列(十五)—— 通过 ResponseWriter 接口创建 HTTP 响应
16
Go 语言 Web 编程系列(十六)—— 设置、读取和删除 Cookie

Go 语言 Web 编程系列(十二)—— 通过 Request 读取 HTTP 请求报文

今天开始,我们将继续开发 Go 语言 Web 开发之旅。

在前面的教程中,学院君给大家介绍了 Go 语言中 HTTP 服务器的实现和内置的路由分发实现,以及第三方的路由器解决方案 —— gorilla/mux,接下来,我们将注意力转移到路由分发之后的业务逻辑,比如 HTTP 请求处理,包括请求参数的解析、表单验证、文件上传等,以及 HTTP 响应发送,包括响应头设置、文件下载、视图模板等。

1、HTTP 请求报文结构

首先来看 HTTP 请求。了解 HTTP 协议的同学应该都知道,一个完整的 HTTP GET 请求报文结构如下:

HTTP 请求报文结构

包含请求行、请求头(首部字段)和请求实体(请求主体)三部分,请求行中包含了请求方法、URL 和 HTTP 协议版本,请求头中包含了 HTTP 请求首部字段,对于 GET 请求来说,没有提交表单数据,所以请求实体为空,对于 POST 请求来说,会包含包括表单数据的请求实体,对这块不够了解的同学可以网上看下 HTTP 协议或者阅读程序员内功修炼部分的 HTTP 报文简介及组成结构深入探索 HTTP 协议底层原理。

2、Request 结构体

Go 通过一个 Request 结构体来表示 HTTP 请求报文,这一点,我们在前面的处理器编写时已经看到了,这个结构体位于内置的 net/http 包中,其中包含了 HTTP 请求的所有信息,包括请求 URL、请求头、请求实体、表单信息等,平时常用的、比较重要的一些字段如下所示:

  • URL:请求 URL
  • Method:请求方法
  • Proto:HTTP 协议版本
  • Header:请求头(字典类型的键值对集合)
  • Body:请求实体(实现了 io.ReadCloser 接口的只读类型)
  • Form、PostForm、MultipartForm:请求表单相关字段,可用于存储表单请求信息

另外还有很多其他字段,比如 Host、From、ContentLength 等,这里就不一一列举了,感兴趣的同学可以自行去查看。

3、请求 URL

对于一个客户端 HTTP 请求来说,请求行中的最重要的当属 URL 信息,否则无法对服务器发起请求,比如我们访问 Google 首页进行搜索,需要现在浏览器地址栏输入 Google 首页的 URL:

这里的 https://www.google.com 就是请求 URL。

在 Go 语言的 http.Request 对象中,用于表示请求 URL 的 URL 字段是一个 url.URL 类型的指针:

我们来简单介绍下其中常见的字段:

  • Scheme 表示 HTTP 协议是 HTTPS 还是 HTTP,在上面的例子中是 https
  • 对于一些需要认证才能访问的应用,需要提供 User 信息;
  • Host 字段表示域名/主机信息,如果服务器监听端口不是默认的 80 端口的话,还需要通过 :端口号 的方式补充端口信息,在上面的例子中是 www.google.com
  • Path 表示 HTTP 请求路径,一般应用首页是空字符串,或者 /
  • Query 相关字段表示 URL 中的查询字符串,也就是 URL 中 ? 之后的部分;
  • Fragment 表示 URL 中的锚点信息,也就是 URL 中 # 之后的部分。

因此,常见的 URL 完整格式如下:

代码语言:javascript
复制
scheme://[user@]host/path[?query][#fragment]

如果不包含 / 的话,URL 解析后的结果如下:

代码语言:javascript
复制
scheme:opaque[?query][#fragment]

例如,对 https://xueyuanjun.com/books/golang-tutorials?page=2#comments 而言,Scheme 值是 httpsHost 值是 xueyuanjun.comPath 值是 /books/golang-tutorialsRawQuery 值是 page=2,我们后面还会演示如何通过 Form 来解析并获取查询字符串中的参数值,Fragment 值是 comments

有趣的是,如果请求是从浏览器发送的话,我们无法获取 URL 中的 Fragment 信息,这不是 Go 的问题,而是浏览器根本没有将其发送到服务端。那为什么还要提供这个字段呢?因为不是所有的请求都是从浏览器发送的,而且 Request 也可以在客户端库中使用。

我们可以编写一段测试代码进行演示,还是以 github.com/xueyuanjun/goblog 为例,在 routes/router.go 中,新增如下测试代码:

代码语言:go
复制
package routes

import (
    "encoding/json"
    "github.com/gorilla/mux"
    "log"
    "net/http"
)

// 返回一个 mux.Router 类型指针,从而可以当作处理器使用
func NewRouter() *mux.Router {

    // 创建 mux.Router 路由器示例
    router := mux.NewRouter().StrictSlash(true)

    // 应用请求日志中间件
    router.Use(loggingRequestInfo)

    // 遍历 web.go 中定义的所有 webRoutes
    for _, route := range webRoutes {
        // 将每个 web 路由应用到路由器
        router.Methods(route.Method).
            Path(route.Pattern).
            Name(route.Name).
            Handler(route.HandlerFunc)
    }

    return router
}

// 记录请求日志信息中间件
func loggingRequestInfo(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 打印请求 URL 明细
        url, _ := json.Marshal(r.URL)
        log.Println(string(url))
        next.ServeHTTP(w, r)
    })
}

我们通过新增一个 loggingRequestInfo 中间件记录所有请求 URL 的明细,这里我们通过 JSON 对 URL 对象进行编码,以便可读性更好。

goblog 目录下通过 go run main.go 启动 HTTP 服务器:

然后新开一个 Terminal 窗口,通过 curl 运行几组测试请求:

然后就可以在运行 HTTP 服务器的窗口看到请求日志了:

可以看到,Scheme、Host、Fragment 信息都是空的。Fragment 为空的原因上面已经提到,Scheme 需要根据是否启用 HTTPS 进行设置,Host 为空的原因是没有通过代理访问 HTTP 服务器,并且在本地开发环境中,Host 始终为空。

4、请求头

请求头和响应头都通过 http.Request.Header 类型表示,Header 是一个键值对字典,键是字符串,值是字符串切片。Header 提供了增删改查方法用于对请求头进行读取和设置。

读取/打印请求头

要获取某个请求头的值很简单,通过 Header 对象提供的 Get 方法,传入对应的字段名即可,比如要获取请求头中 User-Agent 字段,可以这么做:

代码语言:go
复制
r.Header.Get("User-Agent")

要打印完整的请求头,传入整个 r.Header 对象到打印函数即可。

我们修改 routes/router.go 中的中间件函数 loggingRequestInfo,新增打印请求头代码,并且将原来打印 URL 结构体代码调整为打印 URL 字符串:

代码语言:go
复制
// 记录请求日志信息中间件
func loggingRequestInfo(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("Request URL: %s\n", r.URL)
    fmt.Printf("User Agent: %s\n", r.Header.Get("User-Agent"))
    fmt.Printf("Request Header: %v\n", r.Header)
    next.ServeHTTP(w, r)
})

重新启动 HTTP 服务器,分别通过命令行 curl 和浏览器请求应用首页,可以看到日志信息如下:

由于 curl 访问没有设置额外请求头,所以信息很少,而浏览器会加上很多请求头,所以信息更丰富。

新增/修改/删除请求头

此外,我们还可以通过 Header 提供的 Add 方法新增请求头:

代码语言:go
复制
r.Header.Add("test", "value1")

通过 Header 提供的 Set 方法修改请求头:

代码语言:go
复制
r.Header.Set("test", "value2")

以及通过 Header 提供的 Del 方法删除请求头:

代码语言:go
复制
r.Header.Del("test")

5、请求实体

请求实体和响应实体都通过 Body 字段表示,该字段是 io.ReadCloser 接口类型。顾名思义,这个类型实现了 io.Readerio.Closer 接口。

io.Reader 提供了 Read 方法,用于读取传入的字节切片并返回读取的字节数以及错误信息,io.Closer 提供了 Close 方法,因此,你可以在 Body 上调用 Read 方法读取请求实体的内容,调用 Close 方法释放资源。

对于请求实体来说,对应的 Body 访问路径是 http.Request.Body,下面我们编写一段测试代码来演示请求实体的读取,在 goblog/handlers/post.go 中新增一个 AddPost 处理器方法:

代码语言:go
复制
func AddPost(w http.ResponseWriter, r *http.Request)  {
    len := r.ContentLength   // 获取请求实体长度
    body := make([]byte, len)  // 创建存放请求实体的字节切片
    r.Body.Read(body)        // 调用 Read 方法读取请求实体并将返回内容存放到上面创建的字节切片
    io.WriteString(w, string(body))  // 将请求实体作为响应实体返回
}

由于 GET 请求没有请求实体,所以需要通过 POST/PUT/DELETE 之类的请求进行测试,我们在 routes/web.go 中新增一个 Web 路由:

代码语言:go
复制
WebRoute{
    "NewPost",
    "POST",
    "/post/add",
    handlers.AddPost,
},

重启 HTTP 服务器,要测试这段代码,需要发起 POST 请求:

-id 是两个选项的组合,-i 表示输出 HTTP 响应的详细报文,-d 表示传递的表单数据。HTTP 响应报文与响应头通过空行进行分隔,可以看到,在响应实体中打印的正是传递的请求实体信息。

通常,我们不会一次性获取所有的请求实体信息,而是通过类似 FormValue 之类的方法获取每个请求参数,我们将在下一篇教程中详细介绍如何获取 HTTP 表单请求数据。

下一篇
举报
领券