Go Web: Request & Response

http.Request#

结构#

  • 请求行(请求方法/URL/协议)
  • 0个或多个Header
  • 空行
  • 可选的消息体(Body)

例如:

GET www.baidu.com HTTP/1.1
Host: www.w3.org
User-Agent: Mozilla/5.0
(空行)
(无消息体)

读取查询参数#

常见的URL的结构为:

scheme://[userinfo@]host/path[?query][#fragment]

使用Request.URLQuery方法即可得到URL中的查询参数:

fmt.Println(r.URL.Query())

若请求的URL为: http://localhost:8080?name=kokt&age=3 ,则控制台输出:

map[age:[3] name:[kokt]]

读取请求头#

type Header map[string][]string

可以使用Request.Header获取请求头mapRequest.Header["key"]Request.Header.Get("key")获取map中的值,前者返回值类型是[]string,后者返回值类型为string(不同值以逗号分隔)。

读取消息体#

func obtainBody(w http.ResponseWriter, r *http.Request) {
	length := r.ContentLength
	body := make([]byte, length)
    // r.Body是一个io.ReadCloser类型
	r.Body.Read(body)
	fmt.Fprint(w, string(body))
}

使用REST Client向服务端发送带有消息体的POST请求:

POST http://localhost:8080/obtainBody  HTTP/1.1
Content-Type: application/json

{
    "name":kokt,
    "age":3
}

服务端得到请求的消息体:

HTTP/1.1 200 OK
Date: Wed, 06 Jan 2021 16:25:58 GMT
Content-Length: 32
Content-Type: text/plain; charset=utf-8
Connection: close

{
    "name":kokt,
    "age":3
}

处理来自表单的请求#

如果页面上有一个表单,其功能为:当用户点击提交按钮后,就会向服务端发起一个POST请求,请求的页面跳转至/process。上述页面的html代码如下:

<!DOCTYPE html>
<body>
<form method="POST" action="/process" enctype="application/x-www-form-urlencoded">
    <input name="name" type="text">
    <input name="age" type="text">
    <input type="submit">
</form>
</body>

其中,表单的enctype有"application/x-www-form-urlencoded"和"multipart/form-data"两种。前者会将表单数据编码到查询字符串(r.URL.Query())中,适用于简单文本;后者会将每一个键值对转换为一个MIME消息部分,每部分具有独立的Content-Type和Content-Disposition,适用于大量数据和文件的上传。

可以通过Request.Form获取提交的表单数据:

func form(w http.ResponseWriter, r *http.Request) {
	r.ParseForm() // 解析表单,若不调用则r.Form为空
	fmt.Println(r.Form)
}

若在页面填入kokt和3后点击提交按钮,则控制台将会输出:

map[name:[kokt] age:[3]]

如果表单提交后跳转的URL中带有查询字符串,Request.Form可以把它们连同表单中的数据一同输出,而若使用Request.PostForm则只会输出表单中的数据。另外,上述两种方法仅支持表单的enctype属性为"application/x-www-form-urlencoded",若该属性为"multipart/form-data",则需要使用Request.MultipartForm字段:

func form(w http.ResponseWriter, r *http.Request) {
	r.ParseMultipartForm(1024) // 作用类似于r.ParseForm(),其参数为需要读取数据的长度(字节数)
	fmt.Println(r.MultipartForm)
}

它的返回值不同于Request.Formmap,而是一个指向multipart.Form的指针:

type Request struct {
...
    MultipartForm *multipart.Form
...
}
type Form struct {
	Value map[string][]string
	File  map[string][]*FileHeader
}

multipart.Form中有两个map,第一个包含了表单中提交的数据,第二个包含了上传的文件。因此若在页面填入kokt和3后点击提交按钮,则控制台将会输出:

&{map[name:[kokt] age:[3]] map[]}

处理上传的文件#

创建一个可以让用户上传文件的页面,该页面的html代码如下:

<!DOCTYPE html>
<body>
<form method="POST" action="/process" enctype="multipart/form-data">
    <input name="upload" type="file">
    <input type="submit">
</form>
</body>

服务端利用Request.MultipartForm字段获取文件,随后写入到页面的响应中:

func upload(w http.ResponseWriter, r *http.Request) {
	r.ParseMultipartForm(1024)
    // 若上传多个文件,只读取第一个
	fileHeader := r.MultipartForm.File["upload"][0]
	// 忽略异常处理
    file, _ := fileHeader.Open()
    defer file.Close()
	data, _ := ioutil.ReadAll(file) //data的类型为[]byte
	fmt.Fprintf(w, string(data))
}

其中, fileHeader的类型为multipart.FileHeader

type FileHeader struct {
	Filename string
	Header   textproto.MIMEHeader
}

若上传的文件只有一个,则可使用更为简单的Request.FormFile字段。另外,下列代码还添加了一个将上传的文件下载到项目目录下的功能:

	// 无需调用r.ParseMultipartForm()方法
	file, fileHeader, err := r.FormFile("upload")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()
	// 新建一个与上传文件同名的文件
	newfile, err := os.OpenFile(fileHeader.Filename, os.O_WRONLY|os.O_CREATE, 0666)
	if err != nil {
		log.Fatal(err)
	}
	defer newfile.Close()
	// 将上传的文件内容拷贝至新建的文件中
	io.Copy(newfile, file)

上述处理表单数据和文件的方法均是将请求的消息体当作一个对象处理,一次性地获得整个或多个map。而Request.MultipartReader字段可以将请求的消息体当作一个流(stream)进行处理,它仅适用于enctype值为"multipart/form-data"的 POST请求。

func (r *Request) MultipartReader() (*multipart.Reader, error)

Request.ParseForm无法解析application/json。

http.response#

http.response代表对一个HTTP请求的响应。由于该结构体首字母小写,因此无法在包外调用。而*http.response实现了http.ResponseWriter的全部方法。因此我们可以将*http.response当作一个http.ResponseWriter传入函数,从而通过引用传递的方式操作http.response

```go
type ResponseWriter interface {
// Header返回一个Header类型值,该值会被WriteHeader方法发送。
// 在调用WriteHeaderWrite方法后再改变该对象是没有意义的。
Header() Header
// WriteHeader该方法发送HTTP回复的头域和状态码。
// 如果没有被显式调用,第一次调用Write时会触发隐式调用WriteHeader(http.StatusOK)
// WriterHeader的显式调用主要用于发送错误码。
WriteHeader(int) // 参数:HTTP状态码
// Write向连接中写入作为HTTP的一部分回复的数据。
// 如果被调用时还未调用WriteHeader,本方法会先调用WriteHeader(http.StatusOK)
// 如果Header中没有"Content-Type"键,本方法会使用包函数DetectContentType检查数据的前512字节,将返回值作为该键的值。
Write([]byte) (int, error)
}
```
type response struct {
	conn             *conn
	req              *Request // request for this response
	...
}

func (w *response) Header() Header {
    if w.cw.header == nil && w.wroteHeader && !w.cw.wroteHeader {
        w.cw.header = w.handlerHeader.Clone()
    }
    w.calledHeader = true
    return w.handlerHeader
}

func (w *response) WriteHeader(code int) {
    if w.conn.hijacked() {
        caller := relevantCaller()
        w.conn.server.logf("http: response.WriteHeader on hijacked connection from %s (%s:%d)", caller.Function, path.Base(caller.File), caller.Line)
        return
    }
    ...
}

func (w *response) Write(data []byte) (n int, err error) {
    return w.write(len(data), data, "")
}

这也解释了为什么ServeHTTP方法中的两个参数:ResponseWriter*Request,只有Request是按引用传递的,因为传入ResponseWriter的变量本质是*response

内置响应#

  • NotFound函数:包装了一个404状态码和额外信息;
  • ServeFile函数:从文件系统提供文件,返回给请求者;
  • ServeContent函数:可以把实现了io.ReadSeeker接口的任何东西里面的内容返回给请求者(还可以处理范围请求,即只请求了资源中的一部分内容,ServeFileio.Copy则无法做到);
  • Redirect函数:告诉客户端重定向到另一个URL。
posted @   koktlzz  阅读(473)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示
主题色彩