第4章 发送POST请求:表单提交、JSON提交、文件上传
POST请求,说白了就是客户端往服务端塞数据。但塞的方式不同,服务端解析的逻辑就完全不同。很多新手在这个环节翻车,我就见过不少人把JSON当普通表单发,结果服务端死活拿不到参数。
我个人习惯把POST的三种主流方式记成:古老的传统、主流的现代、还有带附件的特殊场景。咱们一个一个来拆。
4.1 表单提交:最传统的方式
最早的Web交互就是靠表单。你想想看,用户在网页上填信息,点提交,浏览器自动把表单数据编码成 key=value&key=value 的格式发出去。这就是 application/x-www-form-urlencoded。
这段代码演示了手动构造表单提交的请求体:
POST /api/login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
username=admin&password=123456
看着简单吧?但坑就藏在简单里。我记得有一次排查线上问题,发现某个参数死活收不到。最后查出来——参数名拼错了。别笑,这种事真能发生,尤其是参数一多的时候。
+ 或 %20。大部分现代 HTTP 客户端会自动帮你做,但手动构造请求时别忘。
4.2 JSON提交:现代微服务的主流
现在写后端接口,很少用纯表单了。JSON 格式结构清晰,支持嵌套,而且几乎所有语言都能轻松解析。设置 Content-Type: application/json 就行。
POST /api/user HTTP/1.1
Host: example.com
Content-Type: application/json
Content-Length: 56
{
"name": "张三",
"email": "zhangsan@example.com",
"age": 28
}
这里有个很容易翻车的地方——请求头的 Content-Type 必须和服务端期望的一致。我见过有人明明发的是 JSON 体,Content-Type 却写成了 text/plain。服务端拿到请求,直接返回 415 Unsupported Media Type。
4.3 文件上传:multipart/form-data
文件上传就有点特殊了。你不能直接把二进制数据塞进 key=value 格式,也做不到把文件塞进 JSON 里(除非用 base64 编码,但那会让体积膨胀约 33%)。
所以 HTTP 设计了 multipart/form-data。它的核心思路是:用 boundary(边界分隔符)把不同的数据块隔开。每个数据块有自己的头部,指明它是普通字段还是文件。
POST /api/upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="username"
admin
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="avatar"; filename="photo.jpg"
Content-Type: image/jpeg
(此处为二进制文件数据,省略)
------WebKitFormBoundary7MA4YWxkTrZu0gW--
| 提交方式 | Content-Type | 适用场景 | 数据格式 |
|---|---|---|---|
| 表单提交 | application/x-www-form-urlencoded | 传统 HTML 表单、简单键值对 | URL 编码的字符串 |
| JSON 提交 | application/json | RESTful API、微服务通信 | JSON 字符串 |
| 文件上传 | multipart/form-data; boundary=... | 上传文件、混合表单数据 | 多部分二进制/文本数据 |
这里有个实践建议:如果你的接口既要传普通字段,又要传文件,那就直接用 multipart/form-data。没必要把字段塞 JSON 里再单独发一次。一个请求搞定所有事,既减少网络开销,又保证数据一致性。
4.4 三种方式的代码示例(Go 语言)
光说理论不够硬。我写三段代码,分别展示用 Go 发送三种 POST 请求。你拿回去就能跑。
表单提交示例
package main
import (
"fmt"
"net/http"
"net/url"
"strings"
)
func main() {
data := url.Values{
"username": {"admin"},
"password": {"123456"},
}
resp, err := http.Post("https://example.com/api/login",
"application/x-www-form-urlencoded",
strings.NewReader(data.Encode()))
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.StatusCode)
}
JSON提交示例
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
func main() {
user := User{
Name: "张三",
Email: "zhangsan@example.com",
Age: 28,
}
jsonData, _ := json.Marshal(user)
resp, err := http.Post("https://example.com/api/user",
"application/json",
bytes.NewReader(jsonData))
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.StatusCode)
}
文件上传示例
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
payload := &bytes.Buffer{}
writer := multipart.NewWriter(payload)
// 添加普通字段
_ = writer.WriteField("username", "admin")
// 添加文件字段
file, _ := os.Open("/path/to/photo.jpg")
defer file.Close()
part, _ := writer.CreateFormFile("avatar", "photo.jpg")
_, _ = io.Copy(part, file)
_ = writer.Close()
contentType := writer.FormDataContentType()
resp, err := http.Post("https://example.com/api/upload",
contentType, payload)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fmt.Println(resp.StatusCode)
}
避坑提醒:我曾经犯过一个错误——用 multipart.NewWriter 创建了 writer,但忘记调用 writer.Close()。结果文件数据没写全,服务端收到不完整的 multipart 报文。所以务必在写完所有字段后关闭 writer,否则最后的 boundary 结束符不会写入。
4.5 如何选择
选哪种方式,说白了就三条原则:
- 纯键值对、无文件 —— 用表单提交。简单高效,浏览器兼容性最广。
- 复杂结构、微服务调用 —— 用 JSON。可读性强,嵌套结构友好。
- 有文件、或混合字段 —— 用 multipart/form-data。它是专门为二进制数据传输设计的。
最后提一句,HTTP 不会限制你混用。你完全可以在 JSON 里嵌套 base64 的图片数据,但那就真的不优雅了。按照 HTTP 的设计哲学,什么场景用什么 Content-Type,别自己的折腾自己。