Go 语言通过 ResponseWriter 对象发送 HTTP 响应

HTTP 响应报文结构

前面几篇教程我们了解了如何在 Go 语言中解析用户请求信息,包括表单字段和文件上传,接下来,我们来看看处理完请求后,如何将响应发送给客户端。HTTP 响应的报文结构如下所示:

和 HTTP 请求报文结构类似,响应报文也可以分为三部分:状态行、响应头(首部字段)和响应主体。

首先是状态行,在状态行中包含了 HTTP 协议版本和响应状态码,200 OK 表示响应成功,更多状态码信息(常见的有 404、403、500、301 等)可以网上查看下 HTTP 协议或者阅读 HTTP 响应状态码这篇文章了解。

然后是响应头,其中包含了 HTTP 响应的首部字段,比如内容类型/编码、缓存控制、Cookie 信息等。

最后是响应实体,对于 API 接口来说,通常就是返回的 XML/JSON 格式数据,对与 HTML 视图响应,就是一个标准的 HTML 文档,如上图所示。响应头和响应报文之间通过两个换行符分隔。

ResponseWriter 接口

在 Go 语言中,客户端请求信息都封装到了 Request 对象,但是发送给客户端的响应并不是 Response 对象,而是 ResponseWriter

func Home(w http.ResponseWriter, r *http.Request)  {
    io.WriteString(w, "Welcome to my blog site")
}

ResponseWriter 是处理器用来创建 HTTP 响应的接口,其源码结构如下所示:

type ResponseWriter interface {
   // 用于设置/获取所有响应头信息
	Header() Header
   // 用于写入数据到响应实体
	Write([]byte) (int, error)
   // 用于设置响应状态码
	WriteHeader(statusCode int)
}

实际上,在底层支撑 ResponseWriter 的结构体就是 http.response,详见 net/http 包下 server.go 中的 readRequest 方法(调用处理器处理 HTTP 请求时调用了该方法返回响应对象),并且其返回值是 response 指针,这也是为什么在处理器方法声明的时候 Request 是指针类型,而 ResponseWriter 不是,实际上在底层,响应对象也是指针类型(因为在应用代码中需要设置响应头和响应实体,所以响应对象理应是指针类型):

func (c *conn) readRequest(ctx context.Context) (w *response, err error) {
	...

	w = &response{
		conn:          c,
		cancelCtx:     cancelCtx,
		req:           req,
		reqBody:       req.Body,
		handlerHeader: make(Header),
		contentLength: -1,
		closeNotifyCh: make(chan bool, 1),
		wants10KeepAlive: req.wantsHttp10KeepAlive(),
		wantsClose:       req.wantsClose(),
	}
	if isH2Upgrade {
		w.closeAfterReply = true
	}
	w.cw.res = w
	w.w = newBufioWriterSize(&w.cw, bufferBeforeChunkingSize)
	return w, nil
}

response 结构体定义和 ResponseWriter 一样都位于 server.go,感兴趣的同学可以去看下源码,不过由于 response 对外不可见,所以只能通过 ResponseWriter 接口访问它。两者之间的关系是 ResponseWriter 是一个接口,而 http.response 实现了它。当我们引用 ResponseWriter 时,实际上引用的是 http.response 对象实例。

设置响应状态码

如上面的 ResponseWriter 接口定义源码所示,它包含三个方法:

  • WriteHeader
  • Header
  • Write

WriteHeader 这个方法名有点误导,其实它并不是用来设置响应头的,该方法支持传入一个整型数据用来表示响应状态码,如果不调用该方法的话,默认响应状态码是 200 OK

WriteHeader 的主要作用是在 API 接口中返回错误码,我们在 goblog/handlers/common.go 中新增一个处理器方法 Error,并通过 w.WriteHeader 返回一个 401 未认证状态码(注意在运行时 w 代表的是对应的 response 对象实例,而不是接口):

func Error(w http.ResponseWriter, r *http.Request)  {
    w.WriteHeader(401)
    fmt.Fprintln(w, "认证后才能访问该接口")
}

注:这里通过 fmt.Fprintln 将文本字符串写入响应对象。

然后在 routes/web.go 中添加一个路由与之映射:

WebRoute{
    "ApiError",
    "GET",
    "/error",
    handlers.Error,
},

重启 HTTP 服务器,通过 curl 访问 http://localhost:8080/error,返回的完整响应报文如下:

-w708

可以看到响应状态码是 401 Unauthorized,表示该接口需要认证后才能访问。这里,我们在运行 curl 时带上 -i 选项,以便可以看到完整的响应报文,第一行是响应状态行,然后是响应头信息,响应头每一行是一个键值对映射,通过冒号分隔,左侧是字段名,右侧是字段值,最后是响应实体,也就是我们在代码中写入的响应数据,响应实体和响应头之间通过一个空行分隔(两个换行符)。

设置响应头

Header 方法用于设置响应头信息,我们可以通过 w.Header().Set 方法设置响应头(w.Header() 方法返回的是 Header 响应头对象,它和请求头共用一个结构体,因此请求头上支持的方法这里都支持,比如可以通过 w.Header().Add 方法新增响应头),这里我们设置一个 301 重定向响应,只需要通过 w.WriteHeader 方法将响应状态码设置为 301,再通过 w.Header().Set 方法将负责重定向的响应头 Location 设置为一个可访问域名即可。

goblog/handlers/common.go 中新建一个处理器方法 Redirect,在其中编写重定向实现代码如下:

func Redirect(w http.ResponseWriter, r *http.Request)  {
    // 设置一个 301 重定向
    w.Header().Set("Location", "https://xueyuanjun.com")
    w.WriteHeader(301)
}

对于重定向请求,无需设置响应实体,另外需要注意的是 w.Header().Set 必须在 w.WriteHeader 之前调用,因为一旦调用 w.WriteHeader 之后,就不能对响应头进行设置了。

接下来,在 routes/web.go 中注册对应的重定向路由:

WebRoute{
    "Redirect",
    "GET",
    "/redirect",
    handlers.Redirect,
},

重启 HTTP 服务器,通过 curl 访问该路由可以清楚看到响应被重定向,并且响应实体为空:

-w603

如果是在浏览器中访问的话,页面就会跳转到 https://xueyuanjun.com

-w1314

写入数据到响应实体

Write 方法用于写入数据到 HTTP 响应实体,如果调用 Write 方法时还不知道 Content-Type,会通过数据的前 512 个字节进行判断。

返回文本字符串

goblog/handlers/common.go 中定义的 Home 处理器方法为例,我们可以通过 w.Write 写入一段欢迎文本到响应实体:

func Home(w http.ResponseWriter, r *http.Request)  {
    w.Write([]byte("欢迎访问学院君个人网站👏"));
}

由于 Write 方法接受的参数类型是 []byte 切片,所以需要将字符串转换为字节切片类型。启动 HTTP 服务器,通过 curl 访问首页,就可以看到返回的文本信息了:

-w657

返回 HTML 文档

如果要返回 HTML 文档,可以这么写入响应数据:

func Home(w http.ResponseWriter, r *http.Request)  {
    //w.Write([]byte("欢迎访问学院君个人网站👏"));
    html := `<html> 
        <head>
            <title>学院君个人网站</title>
        </head> 
        <body>
            <h1>欢迎访问学院君个人网站👏</h1>
        </body> 
    </html>`
    w.Write([]byte(html))
}

当然,后面介绍视图模板后,可以通过视图模板渲染 HTML 文档,这里我们先通过一个简单的包含 HTML 文档信息的字符串替代,重启 HTTP 服务器,通过浏览器访问,就可以看到对应的 HTML 视图了:

-w1002

此外,由于响应数据的内容类型变成了 HTML,在响应头中,也可以看到 Content-Type 也自动调整成了 text/html,不再是纯文本格式。这里的 Content-Type 就是根据传入的数据自行判断出来的。

返回 JSON 格式数据

当然,我们也可以返回 JSON 格式数据:

type Greeting struct {
    Message string `json:"message"`
}

func Home(w http.ResponseWriter, r *http.Request)  {
    // 返回文本字符串
    //w.Write([]byte("欢迎访问学院君个人网站👏"));
    // 返回 HTML 文档
    /*html := `<html>
        <head>
            <title>学院君个人网站</title>
        </head> 
        <body>
            <h1>欢迎访问学院君个人网站👏</h1>
        </body> 
    </html>`
    w.Write([]byte(html))*/
    // 返回 JSON 格式数据
    greeting := Greeting{
        "欢迎访问学院君个人网站👏",
    }
    message, _ := json.Marshal(greeting)
    w.Write(message)
}

重启 HTTP 服务器,在浏览器中访问 http://localhost:8080

-w996

虽然返回的确实是合法的 JSON 格式数据,但是内容类型依然是 text/plain,而不是 application/json,要返回这个格式的响应头,需要设置响应头才能实现:

// 返回 JSON 格式数据
greeting := Greeting{
    "欢迎访问学院君个人网站👏",
}
message, _ := json.Marshal(greeting)
w.Header().Set("Content-Type", "application/json")
w.Write(message)

重启 HTTP 服务器,并再次通过 curl 访问首页,就可以看到内容类型变成 application/json 了:

-w635

上一篇: Go 语言获取 HTTP 请求数据(下):文件上传处理

下一篇: 在 Go 语言中设置、读取和删除 HTTP Cookie