go语言的 net/http包提供了一系列用于表示HTTP报文的结构,可以使用它处理请求和发送响应,其中Request结构代表了客户端发送的请求报文
type Request
type Request struct {
// Method指定HTTP方法(GET、POST、PUT等)。对客户端,""代表GET。
Method string
// URL在服务端表示被请求的URI,在客户端表示要访问的URL。
//
// 在服务端,URL字段是解析请求行的URI(保存在RequestURI字段)得到的,
// 对大多数请求来说,除了Path和RawQuery之外的字段都是空字符串。
// (参见RFC 2616, Section 5.1.2)
//
// 在客户端,URL的Host字段指定了要连接的服务器,
// 而Request的Host字段(可选地)指定要发送的HTTP请求的Host头的值。
URL *url.URL
// 接收到的请求的协议版本。本包生产的Request总是使用HTTP/1.1
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
// Header字段用来表示HTTP请求的头域。如果头域(多行键值对格式)为:
// accept-encoding: gzip, deflate
// Accept-Language: en-us
// Connection: keep-alive
// 则:
// Header = map[string][]string{
// "Accept-Encoding": {"gzip, deflate"},
// "Accept-Language": {"en-us"},
// "Connection": {"keep-alive"},
// }
// HTTP规定头域的键名(头名)是大小写敏感的,请求的解析器通过规范化头域的键名来实现这点。
// 在客户端的请求,可能会被自动添加或重写Header中的特定的头,参见Request.Write方法。
Header Header
// Body是请求的主体。
//
// 在客户端,如果Body是nil表示该请求没有主体买入GET请求。
// Client的Transport字段会负责调用Body的Close方法。
//
// 在服务端,Body字段总是非nil的;但在没有主体时,读取Body会立刻返回EOF。
// Server会关闭请求的主体,ServeHTTP处理器不需要关闭Body字段。
Body io.ReadCloser
// ContentLength记录相关内容的长度。
// 如果为-1,表示长度未知,如果>=0,表示可以从Body字段读取ContentLength字节数据。
// 在客户端,如果Body非nil而该字段为0,表示不知道Body的长度。
ContentLength int64
// TransferEncoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。
// 本字段一般会被忽略。当发送或接受请求时,会自动添加或移除"chunked"传输编码。
TransferEncoding []string
// Close在服务端指定是否在回复请求后关闭连接,在客户端指定是否在发送请求后关闭连接。
Close bool
// 在服务端,Host指定URL会在其上寻找资源的主机。
// 根据RFC 2616,该值可以是Host头的值,或者URL自身提供的主机名。
// Host的格式可以是"host:port"。
//
// 在客户端,请求的Host字段(可选地)用来重写请求的Host头。
// 如过该字段为"",Request.Write方法会使用URL字段的Host。
Host string
// Form是解析好的表单数据,包括URL字段的query参数和POST或PUT的表单数据。
// 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
Form url.Values
// PostForm是解析好的POST或PUT的表单数据。
// 本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
PostForm url.Values
// MultipartForm是解析好的多部件表单,包括上传的文件。
// 本字段只有在调用ParseMultipartForm后才有效。
// 在客户端,会忽略请求中的本字段而使用Body替代。
MultipartForm *multipart.Form
// Trailer指定了会在请求主体之后发送的额外的头域。
//
// 在服务端,Trailer字段必须初始化为只有trailer键,所有键都对应nil值。
// (客户端会声明哪些trailer会发送)
// 在处理器从Body读取时,不能使用本字段。
// 在从Body的读取返回EOF后,Trailer字段会被更新完毕并包含非nil的值。
// (如果客户端发送了这些键值对),此时才可以访问本字段。
//
// 在客户端,Trail必须初始化为一个包含将要发送的键值对的映射。(值可以是nil或其终值)
// ContentLength字段必须是0或-1,以启用"chunked"传输编码发送请求。
// 在开始发送请求后,Trailer可以在读取请求主体期间被修改,
// 一旦请求主体返回EOF,调用者就不可再修改Trailer。
//
// 很少有HTTP客户端、服务端或代理支持HTTP trailer。
Trailer Header
// RemoteAddr允许HTTP服务器和其他软件记录该请求的来源地址,一般用于日志。
// 本字段不是ReadRequest函数填写的,也没有定义格式。
// 本包的HTTP服务器会在调用处理器之前设置RemoteAddr为"IP:port"格式的地址。
// 客户端会忽略请求中的RemoteAddr字段。
RemoteAddr string
// RequestURI是被客户端发送到服务端的请求的请求行中未修改的请求URI
// (参见RFC 2616, Section 5.1)
// 一般应使用URI字段,在客户端设置请求的本字段会导致错误。
RequestURI string
// TLS字段允许HTTP服务器和其他软件记录接收到该请求的TLS连接的信息
// 本字段不是ReadRequest函数填写的。
// 对启用了TLS的连接,本包的HTTP服务器会在调用处理器之前设置TLS字段,否则将设TLS为nil。
// 客户端会忽略请求中的TLS字段。
TLS *tls.ConnectionState
}
Request类型代表一个服务端接受到的或者客户端发送出去的HTTP请求。
Request各字段的意义和用途在服务端和客户端是不同的。除了字段本身上方文档,还可参见Request.Write方法和RoundTripper接口的文档
获取请求URL
Request结构中的URL字段用于表示请求行中包含的URL,改字段是一个指向url.URL结构的指针
type URL
type URL struct {
Scheme string
Opaque string //编码的不透明数据
User *Userinfo //用户名和密码信息
Host string //主机或主机:端口
Path string //路径(相对路径可能省略前斜杠)
RawPath string //编码路径提示(请参阅EscapedPath方法)
ForceQuery bool //追加查询('?'),即使RawQuery为空
RawQuery string //编码的查询值,不带'?'
Fragment string //用于引用的片段,不带'#'
}
表示的一般形式是
[scheme:][//[userinfo@]host][/]path[?query][#fragment]
scheme 后 不以斜杠开头的URL解释为
scheme:opaque[?query][#fragment]
请注意,Path字段以解码形式存储:/%47%6f%2f变为/ Go /。结果是,无法确定路径中的斜线是原始URL中的斜线,以及哪个是%2f。这种区别很少很重要,但是在这种情况下,代码应使用RawPath,这是一个可选字段,仅当默认编码与Path不同时才设置该字段
- Path字段
通过 r.URL.Path 只能得到 /hello
- RawQuery字段
- 获取请求的URL后面?后面的查询字符串
http://localhost:8080/hello?username=admin&password=123456
通过 r.URL.RawQuery 得到的是 username=admin&password=123456
package main
import (
"fmt"
"net/http"
)
//创建处理器函数
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "你发送的请求的请求地址是:", r.URL.Path)
fmt.Fprintln(w, "你发送的请求的请求地址后的查询字符串是:", r.URL.RawQuery)
}
func main() {
http.HandleFunc("/hello", handler)
http.ListenAndServe(":8080", nil)
}
获取请求头中的信息
通过Request结果中的Header字段用来获取请求头中的所有信息,Header字段的类型Header类型,而Header类型是一个map[string][]string, string类型的key,string切片类型的值
- type Header
type Header map[string][]string
Header代表HTTP头域的键值对。
- func (Header) Get
func (h Header) Get(key string) string
Get返回键对应的第一个值,如果键不存在会返回""。如要获取该键对应的值切片,请直接用规范格式的键访问map。
- func (Header) Set
func (h Header) Set(key, value string)
Set添加键值对到h,如键已存在则会用只有新值一个元素的切片取代旧值切片。
- func (Header) Add
func (h Header) Add(key, value string)
Add添加键值对到h,如键已存在则会将新的值附加到旧值切片后面。
- func (Header) Del
func (h Header) Del(key string)
Del删除键值对。
- func (Header) Write
func (h Header) Write(w io.Writer) error
Write以有线格式将头域写入w
- 获取请求头中的所有信息
r.Header
结果
map[Connection:[keep-alive] Cache-Control:[max-age=0] User-Agent:[Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36] Accept:[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8] Accept-Encoding:[gzip, deflate, br] Upgrade-Insecure-Requests:[1] Accept-Language:[zh-CN,zh;q=0.9]]
- 获取请求头中的某个具体属性的值,如 获取 Accept-Encoding的值
- 方式1: r.Header[“Accept-Encoding”]
得到的是一个字符串切片
结果
[gzip, deflate, br]
- 方式2: r.Header.Get(“Accept-Encoding”)
得到的是字符串形式的值,多个值使用逗号分隔
结果
gzip, deflate, br
package main
import (
"fmt"
"net/http"
)
//创建处理器函数
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "你发送的请求的请求地址是:", r.URL.Path)
fmt.Fprintln(w, "你发送的请求的请求地址后的查询字符串是:", r.URL.RawQuery)
fmt.Fprintln(w, "请求头中的所有信息:", r.Header)
fmt.Fprintln(w, "请求头中Accept-Encoding的信息:", r.Header["Accept-Encoding"])
fmt.Fprintln(w, "请求头中Accept-Encoding的属性值是:", r.Header.Get("Accept-Encoding"))
}
func main() {
http.HandleFunc("/hello", handler)
http.ListenAndServe(":8080", nil)
}
获取请求体中的信息
请求和响应的主体都是有Request结构中的Body字段表示,这个字段的类型是 io.ReadCloser接口,该接口包含了Reader接口和Closer接口,Reader接口拥有Read方法,Closer接口拥有Close方法
- type ReadCloser
type ReadCloser interface {
Reader
Closer
}
ReadCloser 接口组合了基本的 Read 和 Close 方法
- type Reader
type Reader interface {
Read(p []byte) (n int, err error)
}
Reader 接口包装了基本的 Read 方法。
Read 将 len§ 个字节读取到 p 中。它返回读取的字节数 n(0 <= n <= len§) 以及任何遇到的错误。即使 Read 返回的 n < len§,它也会在调用过程中使用 p 的全部作为暂存空间。若一些数据可用但不到 len§ 个字节,Read 会照例返回可用的东西, 而不是等待更多。
当 Read 在成功读取 n > 0 个字节后遇到一个错误或 EOF 情况,它就会返回读取的字节数。 它会从相同的调用中返回(非nil的)错误或从随后的调用中返回错误(和 n == 0)。 这种一般情况的一个例子就是 Reader 在输入流结束时会返回一个非零的字节数, 可能的返回不是 err == EOF 就是 err == nil。无论如何,下一个 Read 都应当返回 0, EOF。
调用者应当总在考虑到错误 err 前处理 n > 0 的字节。这样做可以在读取一些字节, 以及允许的 EOF 行为后正确地处理I/O错误。
Read 的实现在 len§ == 0 以外的情况下会阻止返回零字节的计数和 nil 错误, 调用者应将返回 0 和 nil 视作什么也没有发生;特别是它并不表示 EOF
- type Closer
type Closer interface {
Close() error
}
Closer 接口包装了基本的 Close 方法。
Close 的行为在第一次调用后没有定义。具体实现可能有自己的行为描述
1.由于GET请求没有请求体,所以需要在HTML页面中创建一个form表单,通过指定 method=“post” 来发送一个POST请求
表单
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<form action="http://localhost:8080/hello" method="POST">
用户名: <input type="text" name="username" /><br/>
密码: <input type="password" name="password" /><br/>
<input type="submit" />
</form>
</body>
</html>
服务器处理请求代码
package main
import (
"fmt"
"net/http"
)
//创建处理器函数
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "你发送的请求的请求地址是:", r.URL.Path)
fmt.Fprintln(w, "你发送的请求的请求地址后的查询字符串是:", r.URL.RawQuery)
fmt.Fprintln(w, "请求头中的所有信息:", r.Header)
fmt.Fprintln(w, "请求头中Accept-Encoding的信息:", r.Header["Accept-Encoding"])
fmt.Fprintln(w, "请求头中Accept-Encoding的属性值是:", r.Header.Get("Accept-Encoding"))
//获取请求体内容的长度
len := r.ContentLength
//创建byte切片
body := make([]byte, len)
//将请求体中的内容读到body中
r.Body.Read(body)
//在浏览器中显示请求体中的内容
fmt.Fprintln(w, "请求体中的内容有: ", string(body))
}
func main() {
http.HandleFunc("/hello", handler)
http.ListenAndServe(":8080", nil)
}
浏览器显示结果
请求体中的内容有: username=admin&password=123456
获取请求参数
form字段
- 类型是 url.Values类型,form是解析好的表单数据,包括URL字段的query参数和POST或PUT的表单数据
- type Values
type Values map[string][]string
值将字符串键映射到值列表。它通常用于查询参数和表单值。与http.Header映射不同,Values映射中的键区分大小写
- form字段只有在调用Request的ParseForm方法后才有效。在客户端,会忽略请求中的本字段而使用Body替代
- func (*Request) ParseForm
func (r *Request) ParseForm() error
ParseForm解析URL中的查询字符串,并将解析结果更新到r.Form字段。
对于POST或PUT请求,ParseForm还会将body当作表单解析,并将结果既更新到r.PostForm也更新到r.Form。解析结果中,POST或PUT请求主体要优先于URL查询字符串(同名变量,主体的值在查询字符串的值前面)
如果请求的主体的大小没有被MaxBytesReader函数设定限制,其大小默认限制为开头10MB
ParseMultipartForm会自动调用ParseForm。重复调用本方法是无意义的
- 获取表单中提交的请求参数(username和password)
package main
import (
"fmt"
"net/http"
)
//创建处理器函数
func handler(w http.ResponseWriter, r *http.Request) {
//解析表单,在调用r.Form之前必须执行该操作
r.ParseForm()
//获取请求参数
fmt.Fprintln(w, "请求参数有:", r.Form)
}
func main() {
http.HandleFunc("/hello", handler)
http.ListenAndServe(":8080", nil)
}
表单
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<form action="http://localhost:8080/hello?username=admin&password=123456" method="POST">
用户名: <input type="text" name="username"><br/>
密码: <input type="password" name="password"><br/>
<input type="submit" />
</form>
</body>
</html>
运行结果:
请求参数有: map[username:[linux admin] password:[888 123456]]
发现: 表单中的请求参数username和URL中的请求参数username都获取到了,而且表单中的请求参数的值排在URL请求参数值的前面
PostForm字段
- 类型也是 url.Values 类型,用来获取表单中的请求参数
package main
import (
"fmt"
"net/http"
)
//创建处理器函数
func handler(w http.ResponseWriter, r *http.Request) {
//解析表单,在调用r.Form之前必须执行该操作
r.ParseForm()
//获取请求参数
fmt.Fprintln(w, "请求参数有:", r.Form)
fmt.Fprintln(w, "POST请求的form表单中的请求参数有:", r.PostForm)
}
func main() {
http.HandleFunc("/hello", handler)
http.ListenAndServe(":8080", nil)
}
运行结果:
请求参数有: map[username:[admin admin] password:[123 123456]]
POST请求的form表单中的请求参数有: map[username:[admin] password:[123]]
- 但是PostForm字段只支持 application/x-www-form-urlencoded 编码,如果form表单的enctype属性值为 multipart/form-data, 那么使用PostForm字段无法获取表单中的数据,此时需要使用 MultipartForm 字段
说明: form表单的 enctype 属性的默认值是 application/x-www-form-urlencoded 编码,实现上传文件时需要将该属性的值设置为 multipart/form-data 编码格式
FormValue方法和 PostFormValue方法
- FormValue方法
可以通过FormValue 方法快速获取某一个请求参数,该方法调用之前会自动调用 ParseMultipartForm 和 ParseForm方法对表单进行解析
- func (*Request) FormValue
func (r *Request) FormValue(key string) string
FormValue返回key为键查询r.Form字段得到结果[]string切片的第一个值。POST和PUT主体中的同名参数优先于URL查询字符串。如果必要,本函数会隐式调用ParseMultipartForm和ParseForm
- PostFormValue方法
可以通过PostFormValue方法快速获取表单中的某一个请求参数,该方法调用之前会自动调用ParseMultipartForm 和 ParseForm方法对表单进行解析
- func (*Request) PostFormValue
func (r *Request) PostFormValue(key string) string
PostFormValue返回key为键查询r.PostForm字段得到结果[]string切片的第一个值。如果必要,本函数会隐式调用ParseMultipartForm和ParseForm
代码
package main
import (
"fmt"
"net/http"
)
//创建处理器函数
func handler(w http.ResponseWriter, r *http.Request) {
//通过直接调用FormValue方法和PostFormValue方法直接获取请求参数的值
fmt.Fprintln(w, "URL中的user请求参数的值:", r.FormValue("user"))
fmt.Fprintln(w, "Form表单中的username请求参数的值:", r.PostFormValue("username"))
}
func main() {
http.HandleFunc("/hello", handler)
http.ListenAndServe(":8080", nil)
}
表单
<html>
<head>
<meta charset="UTF-8" />
</head>
<body>
<form action="http://localhost:8080/hello?user=wuxing&password=666" method="POST" enctype="application/x-www-form-urlencoded">
用户名: <input type="text" name="username"><br/>
密码: <input type="password" name="password"><br/>
<input type="submit" />
</form>
</body>
</html>
运行结果:
URL中的user请求参数的值: wuxing
Form表单中的username请求参数的值: admin
MultipartForm字段
为了取得 multipart/form-data编码的表单数据,需要用到Request结构的 ParseMultipartForm方法和MultipartForm字段,通常上传文件时会将form表单的 enctype属性值设置为 multipart/form-data
- func (*Request) ParseMultipartForm
func (r *Request) ParseMultipartForm(maxMemory int64) error
ParseMultipartForm将请求的主体作为multipart/form-data解析。请求的整个主体都会被解析,得到的文件记录最多maxMemery字节保存在内存,其余部分保存在硬盘的temp文件里。如果必要,ParseMultipartForm会自行调用ParseForm。重复调用本方法是无意义的
给客户端响应
- type ResponseWriter
type ResponseWriter interface {
// Header返回一个Header类型值,该值会被WriteHeader方法发送。
// 在调用WriteHeader或Write方法后再改变该对象是没有意义的。
Header() Header
// WriteHeader该方法发送HTTP回复的头域和状态码。
// 如果没有被显式调用,第一次调用Write时会触发隐式调用WriteHeader(http.StatusOK)
// WriterHeader的显式调用主要用于发送错误码。
WriteHeader(int)
// Write向连接中写入作为HTTP的一部分回复的数据。
// 如果被调用时还未调用WriteHeader,本方法会先调用WriteHeader(http.StatusOK)
// 如果Header中没有"Content-Type"键,
// 本方法会使用包函数DetectContentType检查数据的前512字节,将返回值作为该键的值。
Write([]byte) (int, error)
}
ResponseWriter接口被HTTP处理器用于构造HTTP回复
- 给客户端响应一个字符串
- 处理器中的代码
package main
import (
"net/http"
)
//创建处理器函数
func testString(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("你的请求我已经收到!"))
}
func main() {
http.HandleFunc("/testStr", testString)
http.ListenAndServe(":8080", nil)
}
- 浏览器中的结果
你的请求我已经收到!
- 响应报文中的内容
HTTP/1.1 200 OK
Date: Thu, 02 Apr 2020 05:18:15 GMT
Content-Length: 30
Content-Type: text/plain; charset=utf-8
- 给客户端响应一个HTML页面
- 处理器中的代码
package main
import (
"net/http"
)
//创建处理器函数
func testHtml(w http.ResponseWriter, r *http.Request) {
html := `
<html>
<head>
<title>测试响应内容为网页</title>
<meta charset="utf-8"/>
</head>
<body>
我是以网页的形式响应过来的!
</body>
</html>`
w.Write([]byte(html))
}
func main() {
http.HandleFunc("/html", testHtml)
http.ListenAndServe(":8080", nil)
}
- 浏览器中的结果
我是以网页的形式响应过来的!
- 响应报文中的内容
HTTP/1.1 200 OK
Date: Thu, 02 Apr 2020 05:25:31 GMT
Content-Length: 174
Content-Type: text/html; charset=utf-8
- 给客户端响应JSON格式数据
- 处理器端代码
user.go
package model
//User 结构体
type User struct {
ID int
Username string
Password string
Email string
}
main.go
package main
import (
"encoding/json"
"fmt"
"goweb/web02_req/model"
"net/http"
)
//创建处理器函数
func testJsonRes(w http.ResponseWriter, r *http.Request) {
//设置响应内容的类型
w.Header().Set("Content-Type", "application/json")
//创建User
user := model.User{
ID: 1,
Username: "admin",
Password: "123456",
Email: "admin@wuxing.com",
}
//将User转换为Json格式
json, _ := json.Marshal(user)
//将json格式的数据响应给客户端
w.Write(json)
}
func main() {
http.HandleFunc("/testJson", testJsonRes)
http.ListenAndServe(":8080", nil)
}
- 浏览器中的结果
{"ID":1,"Username":"admin","Password":"123456","Email":"admin@wuxing.com"}
- 响应报文中的内容
HTTP/1.1 200 OK
Content-Type: application/json
Date: Thu, 02 Apr 2020 04:17:34 GMT
Content-Length: 74
- 让客户端重定向
- 处理器端代码
package main
import (
"net/http"
)
//创建处理器函数
func testRedire(w http.ResponseWriter, r *http.Request) {
//设置响应头中的Location属性
w.Header().Set("Location", "https://www.baidu.com")
//设置响应的状态码
w.WriteHeader(302)
}
func main() {
http.HandleFunc("/testRedirect", testRedire)
http.ListenAndServe(":8080", nil)
}
- 响应报文中的内容
HTTP/1.1 302 Found
Location: https://www.baidu.com
Date: Thu, 02 Apr 2020 04:30:42 GMT
Content-Length: 0
Content-Type: text/plain; charset=utf-8