Go HTTP 编程教程
📖 关于本教程本教程系统讲解 Go HTTP 编程的核心知识:HTTP 协议原理、Server/Client 实现、URL 转义、流式传输、模板引擎、Cookie、路由、大文件传输等,配合完整可运行代码和协议级别的抓包分析。
1. HTTP 协议讲解
1.1 HTTP 请求报文
text
一个完整的 HTTP 请求报文结构:
┌──────────────────────────────────────────────┐
│ GET /api/users?page=1&size=10 HTTP/1.1 │ ← 请求行(方法 路径 版本)
├──────────────────────────────────────────────┤
│ Host: example.com │
│ User-Agent: Go-http-client/1.1 │
│ Accept: application/json │ ← 请求头(key: value)
│ Content-Type: application/json │
│ Authorization: Bearer eyJhbGciOiJIUzI1NiJ9 │
│ Content-Length: 27 │
├──────────────────────────────────────────────┤
│ │
│ {"username":"alice","age":25} │ ← 请求体(可选,GET 通常没有)
└──────────────────────────────────────────────┘
请求行三要素:
方法(Method) 路径(Path + Query) 协议版本
GET /api/users?page=1 HTTP/1.11.2 HTTP 响应报文
text
┌──────────────────────────────────────────────┐
│ HTTP/1.1 200 OK │ ← 状态行(版本 状态码 原因短语)
├──────────────────────────────────────────────┤
│ Content-Type: application/json │
│ Content-Length: 85 │ ← 响应头
│ Date: Mon, 01 Jan 2024 12:00:00 GMT │
│ Set-Cookie: session=abc123; Path=/ │
├──────────────────────────────────────────────┤
│ │
│ {"id":1,"username":"alice","age":25} │ ← 响应体
└──────────────────────────────────────────────┘1.3 常用状态码
text
状态码 含义 常见场景
─────────────────────────────────────────────────
200 OK 请求成功
201 Created 资源创建成功(POST)
204 No Content 成功但无返回体(DELETE)
301 Moved 永久重定向
302 Found 临时重定向
304 Not Modified 缓存未过期
400 Bad Request 请求参数错误
401 Unauthorized 未认证
403 Forbidden 无权限
404 Not Found 资源不存在
405 Method Not Allow 方法不支持
409 Conflict 资源冲突(重复创建)
413 Entity Too Large 请求体过大
429 Too Many Reqs 限流
500 Internal Error 服务端错误
502 Bad Gateway 网关/代理错误
503 Service Unavail 服务不可用
504 Gateway Timeout 网关超时1.4 常用请求方法
text
方法 语义 幂等性 有请求体 有响应体
──────────────────────────────────────────────────
GET 获取资源 ✅ 是 ❌ 无 ✅ 有
POST 创建资源 ❌ 否 ✅ 有 ✅ 有
PUT 全量更新 ✅ 是 ✅ 有 ✅ 有
PATCH 部分更新 ❌ 否 ✅ 有 ✅ 有
DELETE 删除资源 ✅ 是 ❌ 无 ✅ 有
HEAD 获取头部 ✅ 是 ❌ 无 ❌ 无
OPTIONS 预检请求 ✅ 是 ❌ 无 ✅ 有
幂等性:多次请求和一次请求的结果相同2. 启动 HTTP Server 和 Client,通过 Go 代码窥探 HTTP 协议
2.1 最简 HTTP Server
go
package main
import (
"fmt"
"log"
"net/http"
"strings"
)
func main() {
// 注册路由处理器
http.HandleFunc("/", homeHandler)
http.HandleFunc("/hello", helloHandler)
http.HandleFunc("/debug", debugHandler)
fmt.Println("Server 启动: http://localhost:8080")
// ListenAndServe 会阻塞,监听并处理请求
log.Fatal(http.ListenAndServe(":8080", nil))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
// w 用于写响应,r 是请求对象
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "欢迎访问首页!\n方法: %s\n路径: %s\n", r.Method, r.URL.Path)
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
fmt.Fprintf(w, "Hello, %s!\n", name)
}
// debugHandler 打印完整的请求信息,窥探 HTTP 协议
func debugHandler(w http.ResponseWriter, r *http.Request) {
var sb strings.Builder
// ==================== 请求行 ====================
sb.WriteString(fmt.Sprintf("=== 请求行 ===\n"))
sb.WriteString(fmt.Sprintf("方法: %s\n", r.Method))
sb.WriteString(fmt.Sprintf("URL: %s\n", r.URL.String()))
sb.WriteString(fmt.Sprintf("协议: %s\n", r.Proto))
sb.WriteString(fmt.Sprintf("Host: %s\n", r.Host))
sb.WriteString(fmt.Sprintf("远程地址: %s\n\n", r.RemoteAddr))
// ==================== URL 各部分 ====================
sb.WriteString("=== URL 解析 ===\n")
sb.WriteString(fmt.Sprintf("Scheme: %s\n", r.URL.Scheme))
sb.WriteString(fmt.Sprintf("Host: %s\n", r.URL.Host))
sb.WriteString(fmt.Sprintf("Path: %s\n", r.URL.Path))
sb.WriteString(fmt.Sprintf("RawQuery: %s\n", r.URL.RawQuery))
sb.WriteString(fmt.Sprintf("Fragment: %s\n\n", r.URL.Fragment))
// ==================== 请求头 ====================
sb.WriteString("=== 请求头 ===\n")
for key, values := range r.Header {
for _, v := range values {
sb.WriteString(fmt.Sprintf("%s: %s\n", key, v))
}
}
sb.WriteString(fmt.Sprintf("\nContent-Length: %d\n", r.ContentLength))
sb.WriteString(fmt.Sprintf("Transfer-Encoding: %v\n\n", r.TransferEncoding))
// ==================== Query 参数 ====================
sb.WriteString("=== Query 参数 ===\n")
for key, values := range r.URL.Query() {
sb.WriteString(fmt.Sprintf("%s = %s\n", key, strings.Join(values, ", ")))
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Custom-Header", "go-debug")
w.Write([]byte(sb.String()))
}2.2 HTTP Client 窥探协议
go
package main
import (
"fmt"
"io"
"net/http"
"net/http/httputil"
)
func main() {
// ==================== 最简单的 GET 请求 ====================
resp, err := http.Get("http://localhost:8080/debug?name=alice&age=25")
if err != nil {
fmt.Println("请求失败:", err)
return
}
defer resp.Body.Close()
// 打印完整的响应信息
fmt.Println("=== 响应状态 ===")
fmt.Println("状态码:", resp.StatusCode)
fmt.Println("状态:", resp.Status)
fmt.Println("协议:", resp.Proto)
fmt.Println("\n=== 响应头 ===")
for key, values := range resp.Header {
fmt.Printf("%s: %s\n", key, values)
}
fmt.Println("\n=== 响应体 ===")
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
// ==================== 用 httputil.DumpRequest 看原始报文 ====================
fmt.Println("\n=== 原始 HTTP 报文 ===")
req, _ := http.NewRequest("GET", "http://localhost:8080/hello?name=bob", nil)
req.Header.Set("Accept", "text/plain")
req.Header.Set("X-Request-Id", "req-001")
// 转储请求报文(不实际发送)
dump, _ := httputil.DumpRequestOut(req, true)
fmt.Println("--- 请求报文 ---")
fmt.Println(string(dump))
// 实际发送
client := &http.Client{}
resp2, _ := client.Do(req)
defer resp2.Body.Close()
// 转储响应报文
dumpResp, _ := httputil.DumpResponse(resp2, true)
fmt.Println("--- 响应报文 ---")
fmt.Println(string(dumpResp))
}输出的原始报文类似:
text
--- 请求报文 ---
GET /hello?name=bob HTTP/1.1
Host: localhost:8080
Accept: text/plain
X-Request-Id: req-001
--- 响应报文 ---
HTTP/1.1 200 OK
Content-Length: 12
Content-Type: text/plain; charset=utf-8
Date: Mon, 01 Jan 2024 12:00:00 GMT
Hello, bob!3. URL 参数转义
go
package main
import (
"fmt"
"net/url"
)
func main() {
// ==================== URL 的组成部分 ====================
// scheme://user:pass@host:port/path?query#fragment
//
// https://alice:secret@example.com:8080/api/search?q=hello+world&lang=中文#result
// ==================== 解析 URL ====================
rawURL := "https://example.com/search?q=hello+world&lang=%E4%B8%AD%E6%96%87&page=1"
u, err := url.Parse(rawURL)
if err != nil {
fmt.Println("解析失败:", err)
return
}
fmt.Println("Scheme:", u.Scheme) // https
fmt.Println("Host:", u.Host) // example.com
fmt.Println("Path:", u.Path) // /search
fmt.Println("RawQuery:", u.RawQuery) // q=hello+world&lang=%E4%B8%AD%E6%96%87&page=1
// 解析 Query 参数(自动解码)
params := u.Query()
fmt.Println("q:", params.Get("q")) // hello world(+ 被解码为空格)
fmt.Println("lang:", params.Get("lang")) // 中文(%E4%B8%AD%E6%96%87 被解码)
fmt.Println("page:", params.Get("page")) // 1
// ==================== 编码 URL 参数 ====================
// url.QueryEscape:将特殊字符编码为 %XX 格式
raw := "hello world/你好?key=value&foo=bar"
escaped := url.QueryEscape(raw)
fmt.Println("\n编码前:", raw)
fmt.Println("编码后:", escaped)
// hello+world%2F%E4%BD%A0%E5%A5%BD%3Fkey%3Dvalue%26foo%3Dbar
// 解码
unescaped, _ := url.QueryUnescape(escaped)
fmt.Println("解码后:", unescaped) // hello world/你好?key=value&foo=bar
// ==================== 路径转义 ====================
// url.PathEscape 比 QueryEscape 少编码一些字符
path := "/api/users/张三/orders"
pathEscaped := url.PathEscape(path)
fmt.Println("\n路径编码:", pathEscaped)
// %2Fapi%2Fusers%2F%E5%BC%A0%E4%B8%89%2Forders
// ==================== 构建 URL(推荐方式)====================
u2 := &url.URL{
Scheme: "https",
Host: "api.example.com:8443",
Path: "/v2/search",
}
// 用 url.Values 构建 Query 参数(自动编码)
q := url.Values{}
q.Set("keyword", "Go 语言 & Web 开发")
q.Set("page", "1")
q.Set("size", "20")
q.Add("tag", "golang")
q.Add("tag", "http") // 同一个 key 多个值
u2.RawQuery = q.Encode()
fmt.Println("\n构建的 URL:", u2.String())
// https://api.example.com:8443/v2/search?keyword=Go+%E8%AF%AD%E8%A8%80+%26+Web+%E5%BC%80%E5%8F%91&page=1&size=20&tag=golang&tag=http
// ==================== 常见编码对照 ====================
fmt.Println("\n=== 常见字符编码 ===")
specials := []string{" ", "&", "=", "?", "/", "+", "#", "中", "%"}
for _, ch := range specials {
fmt.Printf(" '%s' → %s\n", ch, url.QueryEscape(ch))
}
// ' ' → +
// '&' → %26
// '=' → %3D
// '?' → %3F
// '/' → %2F
// '+' → %2B
// '#' → %23
// '中' → %E4%B8%AD
// '%' → %25
}⚠️ 转义常见坑
- 空格在 Query 中编码为
+(QueryEscape),在路径中编码为%20(PathEscape) url.Values.Encode()会自动排序 key(字母序),便于缓存但可能影响签名- 不要手动拼接 URL 参数,用
url.Values构建,避免注入和编码错误
4. Body 流式传输大数据
4.1 流式发送(Client 端)
go
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func main() {
// ==================== 方式1:直接用文件作为 Body(零拷贝流式)====================
file, err := os.Open("largefile.bin")
if err != nil {
fmt.Println("打开文件失败:", err)
return
}
defer file.Close()
// file 实现了 io.Reader,http 会流式读取,不会全部加载到内存
req, _ := http.NewRequest("POST", "http://localhost:8080/upload", file)
// 设置 Content-Length(可选,不设置会用 chunked 编码)
info, _ := file.Stat()
req.ContentLength = info.Size()
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("请求失败:", err)
return
}
defer resp.Body.Close()
fmt.Println("状态:", resp.Status)
// ==================== 方式2:用 io.Pipe 边生产边发送 ====================
pr, pw := io.Pipe()
// 生产者协程:往 pipe 写数据
go func() {
defer pw.Close()
for i := 0; i < 100; i++ {
fmt.Fprintf(pw, "数据块 %d: %s\n", i,
string(make([]byte, 1024))) // 每块约 1KB
}
}()
// pipe 的 Reader 端作为 Body,边写边发送
req2, _ := http.NewRequest("POST", "http://localhost:8080/stream", pr)
req2.Header.Set("Content-Type", "text/plain")
// 不设置 Content-Length → Transfer-Encoding: chunked(自动分块传输)
resp2, _ := http.DefaultClient.Do(req2)
if resp2 != nil {
defer resp2.Body.Close()
fmt.Println("流式发送完成:", resp2.Status)
}
}4.2 流式接收(Server 端)
go
package main
import (
"crypto/sha256"
"fmt"
"io"
"log"
"net/http"
"os"
)
func main() {
http.HandleFunc("/upload", uploadHandler)
http.HandleFunc("/stream", streamReceiver)
log.Fatal(http.ListenAndServe(":8080", nil))
}
// uploadHandler 流式接收上传文件
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed", 405)
return
}
fmt.Printf("接收上传: Content-Length=%d, Transfer-Encoding=%v\n",
r.ContentLength, r.TransferEncoding)
// r.Body 是 io.ReadCloser,天然支持流式读取
// 不要用 io.ReadAll,否则大文件会撑爆内存
// 方式1:流式存储到文件
dst, _ := os.Create("uploaded_file.bin")
defer dst.Close()
// io.Copy 内部使用 32KB 缓冲区,不会吃内存
written, err := io.Copy(dst, r.Body)
if err != nil {
http.Error(w, "接收失败", 500)
return
}
fmt.Fprintf(w, "接收完成: %d 字节\n", written)
}
// streamReceiver 流式边接收边处理
func streamReceiver(w http.ResponseWriter, r *http.Request) {
// 边接收边计算 SHA-256(不存储整个文件到内存)
hasher := sha256.New()
buf := make([]byte, 4096) // 4KB 缓冲区
var totalBytes int64
for {
n, err := r.Body.Read(buf)
if n > 0 {
totalBytes += int64(n)
hasher.Write(buf[:n])
// 这里可以做任何流式处理:
// 写入文件、发送到消息队列、转发到另一个服务...
}
if err == io.EOF {
break
}
if err != nil {
http.Error(w, "读取失败", 500)
return
}
}
hash := fmt.Sprintf("%x", hasher.Sum(nil))
fmt.Fprintf(w, "接收: %d 字节, SHA-256: %s\n", totalBytes, hash)
}5. HTTP 流式响应
go
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/sse", sseHandler)
http.HandleFunc("/chunked", chunkedHandler)
http.HandleFunc("/progress", progressHandler)
fmt.Println("Server: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// ==================== SSE(Server-Sent Events)====================
// 服务器推送事件,浏览器原生支持,适合实时通知
func sseHandler(w http.ResponseWriter, r *http.Request) {
// 检查是否支持 Flush
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", 500)
return
}
// SSE 必须的响应头
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
for i := 0; i < 10; i++ {
// SSE 格式:每条消息以 "data: " 开头,以 "\n\n" 结尾
fmt.Fprintf(w, "event: message\n")
fmt.Fprintf(w, "data: {\"count\": %d, \"time\": \"%s\"}\n\n",
i, time.Now().Format("15:04:05"))
flusher.Flush() // 立即发送(不等缓冲区满)
time.Sleep(1 * time.Second)
}
// 发送结束事件
fmt.Fprintf(w, "event: done\ndata: stream ended\n\n")
flusher.Flush()
}
// ==================== Chunked Transfer ====================
// 分块传输:不需要预知总大小
func chunkedHandler(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", 500)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
// 不设置 Content-Length → 自动使用 Transfer-Encoding: chunked
for i := 1; i <= 5; i++ {
chunk := fmt.Sprintf("第 %d 块数据(时间: %s)\n", i, time.Now().Format("15:04:05"))
w.Write([]byte(chunk))
flusher.Flush() // 每写一块就发送
time.Sleep(800 * time.Millisecond)
}
}
// ==================== 进度反馈 ====================
func progressHandler(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", 500)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
total := 100
for i := 0; i <= total; i += 10 {
fmt.Fprintf(w, "处理进度: %d%%\n", i)
flusher.Flush()
time.Sleep(300 * time.Millisecond)
}
fmt.Fprintf(w, "处理完成!\n")
flusher.Flush()
}客户端接收流式响应:
go
package main
import (
"bufio"
"fmt"
"net/http"
"strings"
)
func main() {
// ==================== 接收 SSE 流式响应 ====================
resp, err := http.Get("http://localhost:8080/sse")
if err != nil {
fmt.Println("请求失败:", err)
return
}
defer resp.Body.Close()
// 流式逐行读取(不要用 io.ReadAll,会等到流结束才返回)
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "data: ") {
data := strings.TrimPrefix(line, "data: ")
fmt.Println("收到:", data)
}
}
fmt.Println("流结束")
}6. 用 template 生成网页
6.1 基本模板
go
package main
import (
"html/template"
"log"
"net/http"
"time"
)
// PageData 页面数据
type PageData struct {
Title string
User string
IsAdmin bool
Items []Item
Now time.Time
}
type Item struct {
Name string
Price float64
Stock int
}
func main() {
http.HandleFunc("/", pageHandler)
http.HandleFunc("/list", listHandler)
log.Println("Server: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func pageHandler(w http.ResponseWriter, r *http.Request) {
// 内联模板
tmplStr := `<!DOCTYPE html>
<html>
<head><title>{{.Title}}</title></head>
<body>
<h1>欢迎, {{.User}}!</h1>
{{/* 这是模板注释 */}}
{{if .IsAdmin}}
<p style="color: red;">⚠️ 你是管理员</p>
{{else}}
<p>普通用户</p>
{{end}}
<h2>商品列表</h2>
<table border="1">
<tr><th>#</th><th>名称</th><th>价格</th><th>库存</th></tr>
{{range $i, $item := .Items}}
<tr>
<td>{{$i | plus1}}</td>
<td>{{$item.Name}}</td>
<td>¥{{printf "%.2f" $item.Price}}</td>
<td>{{if gt $item.Stock 0}}{{$item.Stock}}{{else}}<span style="color:red">缺货</span>{{end}}</td>
</tr>
{{end}}
</table>
<p>当前时间: {{.Now.Format "2006-01-02 15:04:05"}}</p>
</body>
</html>`
// 自定义函数
funcMap := template.FuncMap{
"plus1": func(i int) int { return i + 1 },
}
tmpl, err := template.New("page").Funcs(funcMap).Parse(tmplStr)
if err != nil {
http.Error(w, "模板解析失败: "+err.Error(), 500)
return
}
data := PageData{
Title: "Go 模板示例",
User: "Alice",
IsAdmin: true,
Items: []Item{
{"Go 编程指南", 89.90, 120},
{"机械键盘", 399.00, 0},
{"27 寸显示器", 1999.00, 35},
},
Now: time.Now(),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl.Execute(w, data)
}6.2 模板文件和嵌套
go
package main
import (
"html/template"
"log"
"net/http"
"os"
)
func main() {
// 创建模板文件
createTemplateFiles()
// 解析模板目录下所有 .html 文件
tmpl := template.Must(template.ParseGlob("templates/*.html"))
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"Title": "首页",
"Content": "这是首页内容",
"Year": 2024,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
// 执行名为 "page" 的模板
tmpl.ExecuteTemplate(w, "page", data)
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
func createTemplateFiles() {
os.MkdirAll("templates", 0755)
// 基础布局模板
os.WriteFile("templates/layout.html", []byte(`
{{define "page"}}
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
<style>
body { font-family: sans-serif; margin: 40px; }
header { background: #333; color: white; padding: 10px; }
footer { border-top: 1px solid #ccc; padding: 10px; margin-top: 20px; }
</style>
</head>
<body>
{{template "header" .}}
<main>{{template "content" .}}</main>
{{template "footer" .}}
</body>
</html>
{{end}}
`), 0644)
// 头部
os.WriteFile("templates/header.html", []byte(`
{{define "header"}}
<header>
<h1>{{.Title}}</h1>
<nav>首页 | 关于 | 联系</nav>
</header>
{{end}}
`), 0644)
// 内容
os.WriteFile("templates/content.html", []byte(`
{{define "content"}}
<div>
<p>{{.Content}}</p>
</div>
{{end}}
`), 0644)
// 底部
os.WriteFile("templates/footer.html", []byte(`
{{define "footer"}}
<footer>
<p>© {{.Year}} Go 教程</p>
</footer>
{{end}}
`), 0644)
}⚠️ html/template vs text/template html/template 会自动转义 HTML 特殊字符,防止 XSS 攻击。生成网页时必须用 html/template,生成非 HTML 内容(配置文件、邮件等)用 text/template。
7. HEAD 请求
go
package main
import (
"fmt"
"net/http"
"strconv"
)
// ==================== HEAD 请求 ====================
// HEAD 和 GET 相同,但服务器不返回响应体
// 用途:检查资源是否存在、获取文件大小、检查更新(Last-Modified / ETag)
func main() {
// ==================== 客户端发送 HEAD ====================
resp, err := http.Head("https://go.dev/dl/go1.22.0.linux-amd64.tar.gz")
if err != nil {
fmt.Println("请求失败:", err)
return
}
defer resp.Body.Close()
fmt.Println("状态:", resp.Status)
fmt.Println("Content-Type:", resp.Header.Get("Content-Type"))
fmt.Println("Content-Length:", resp.Header.Get("Content-Length"))
fmt.Println("Last-Modified:", resp.Header.Get("Last-Modified"))
fmt.Println("ETag:", resp.Header.Get("ETag"))
// 获取文件大小(不下载文件)
size, _ := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
fmt.Printf("文件大小: %.2f MB\n", float64(size)/1024/1024)
// ==================== 检查 URL 是否可访问 ====================
checkURL := func(url string) {
resp, err := http.Head(url)
if err != nil {
fmt.Printf("❌ %s: 请求失败\n", url)
return
}
defer resp.Body.Close()
if resp.StatusCode < 400 {
fmt.Printf("✅ %s: %s\n", url, resp.Status)
} else {
fmt.Printf("❌ %s: %s\n", url, resp.Status)
}
}
checkURL("https://go.dev")
checkURL("https://go.dev/nonexistent")
}go
// ==================== 服务端处理 HEAD ====================
// Go 的 http.HandleFunc 自动处理 HEAD 请求:
// 对于 HEAD 请求,处理器正常执行但 http 包会丢弃响应体
func fileInfoHandler(w http.ResponseWriter, r *http.Request) {
// GET 和 HEAD 都会进入这个处理器
// HEAD 请求时,w.Write 的内容会被自动丢弃,只保留 Header
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", "1048576") // 1MB
w.Header().Set("Last-Modified", "Mon, 01 Jan 2024 00:00:00 GMT")
w.Header().Set("Accept-Ranges", "bytes") // 支持断点续传
if r.Method == http.MethodHead {
return // HEAD 不需要写 Body
}
// GET 才写入实际数据
// w.Write(fileData)
}8. POST 常见的请求数据类型
go
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"os"
"strings"
)
// ==================== 1. application/json(最常用)====================
func postJSON() {
data := map[string]interface{}{
"username": "alice",
"age": 25,
"tags": []string{"golang", "web"},
}
body, _ := json.Marshal(data)
resp, _ := http.Post(
"http://localhost:8080/api/users",
"application/json", // Content-Type
bytes.NewReader(body), // Body
)
defer resp.Body.Close()
result, _ := io.ReadAll(resp.Body)
fmt.Println("JSON 响应:", string(result))
}
// ==================== 2. application/x-www-form-urlencoded ====================
// HTML 表单默认格式:key1=value1&key2=value2
func postForm() {
// 方式1:http.PostForm(最简单)
resp, _ := http.PostForm("http://localhost:8080/login", url.Values{
"username": {"alice"},
"password": {"secret123"},
})
defer resp.Body.Close()
// 方式2:手动构建
formData := url.Values{}
formData.Set("username", "bob")
formData.Set("password", "pass456")
req, _ := http.NewRequest("POST",
"http://localhost:8080/login",
strings.NewReader(formData.Encode()),
)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
http.DefaultClient.Do(req)
}
// ==================== 3. multipart/form-data(文件上传)====================
func postMultipart() {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf) // 创建 multipart 写入器
// 添加普通字段
writer.WriteField("username", "alice")
writer.WriteField("description", "我的头像")
// 添加文件字段
fileWriter, _ := writer.CreateFormFile("avatar", "photo.jpg")
file, _ := os.Open("photo.jpg")
defer file.Close()
io.Copy(fileWriter, file) // 流式写入文件内容
writer.Close() // 必须关闭,写入结束边界
// 发送请求
req, _ := http.NewRequest("POST", "http://localhost:8080/upload", &buf)
req.Header.Set("Content-Type", writer.FormDataContentType())
// Content-Type: multipart/form-data; boundary=--abc123...
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
}
// ==================== 4. text/plain ====================
func postText() {
body := strings.NewReader("这是一段纯文本内容")
http.Post("http://localhost:8080/text", "text/plain; charset=utf-8", body)
}
// ==================== 5. application/xml ====================
func postXML() {
xmlData := `<?xml version="1.0" encoding="UTF-8"?>
<user>
<name>alice</name>
<age>25</age>
</user>`
http.Post("http://localhost:8080/xml", "application/xml", strings.NewReader(xmlData))
}
func main() {
fmt.Println(`
POST 请求数据类型总结:
─────────────────────────────────────────────────────────────────
Content-Type 用途
─────────────────────────────────────────────────────────────────
application/json API 接口传递结构化数据(最常用)
application/x-www-form-urlencoded HTML 表单提交(简单键值对)
multipart/form-data 文件上传(含二进制数据)
text/plain 纯文本
application/xml XML 数据
application/octet-stream 原始二进制流
─────────────────────────────────────────────────────────────────`)
}服务端解析各类 POST 数据:
go
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
// 解析 JSON Body
func jsonHandler(w http.ResponseWriter, r *http.Request) {
var data map[string]interface{}
// json.NewDecoder 是流式解码,不需要先 ReadAll
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
http.Error(w, "Invalid JSON: "+err.Error(), 400)
return
}
fmt.Fprintf(w, "收到 JSON: %v\n", data)
}
// 解析 Form 表单
func formHandler(w http.ResponseWriter, r *http.Request) {
// ParseForm 解析 URL query 和 body 中的表单数据
r.ParseForm()
username := r.FormValue("username") // 同时搜索 URL 和 Body
password := r.PostFormValue("password") // 只搜索 Body
fmt.Fprintf(w, "用户: %s, 密码: %s\n", username, password)
}
// 解析 Multipart(文件上传)
func uploadHandler(w http.ResponseWriter, r *http.Request) {
// 限制最大内存 32MB(超出部分写入临时文件)
r.ParseMultipartForm(32 << 20)
// 获取普通字段
username := r.FormValue("username")
// 获取文件
file, header, err := r.FormFile("avatar")
if err != nil {
http.Error(w, "获取文件失败: "+err.Error(), 400)
return
}
defer file.Close()
fmt.Fprintf(w, "用户: %s, 文件名: %s, 大小: %d 字节\n",
username, header.Filename, header.Size)
// 读取文件内容
content, _ := io.ReadAll(file)
_ = content // 保存到磁盘或对象存储...
}9. HTTP 通用请求方式及 Cookie 讲解
9.1 通用请求方式
go
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
func main() {
// ==================== http.NewRequest(最通用)====================
body := bytes.NewReader([]byte(`{"name":"alice"}`))
req, err := http.NewRequest("PUT", "http://localhost:8080/api/users/1", body)
if err != nil {
fmt.Println("创建请求失败:", err)
return
}
// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer my-token-123")
req.Header.Set("X-Request-ID", "req-001")
req.Header.Add("Accept", "application/json")
req.Header.Add("Accept", "text/plain") // Add 允许同一 key 多个值
// ==================== 自定义 Client ====================
client := &http.Client{
Timeout: 10 * time.Second,
// 不自动跟随重定向
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 3 {
return fmt.Errorf("too many redirects")
}
return nil // 默认允许
// return http.ErrUseLastResponse // 不跟随重定向
},
}
resp, err := client.Do(req)
if err != nil {
fmt.Println("请求失败:", err)
return
}
defer resp.Body.Close()
bodyBytes, _ := io.ReadAll(resp.Body)
fmt.Println("响应:", string(bodyBytes))
// ==================== 带 context 的请求(超时/取消)====================
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req2, _ := http.NewRequestWithContext(ctx, "GET", "http://localhost:8080/slow", nil)
resp2, err := client.Do(req2)
if err != nil {
fmt.Println("超时或取消:", err) // context deadline exceeded
return
}
defer resp2.Body.Close()
}
// ==================== 封装通用请求函数 ====================
type APIClient struct {
baseURL string
client *http.Client
token string
}
func NewAPIClient(baseURL, token string) *APIClient {
return &APIClient{
baseURL: baseURL,
token: token,
client: &http.Client{
Timeout: 10 * time.Second,
},
}
}
func (c *APIClient) Request(ctx context.Context, method, path string, body interface{}) ([]byte, int, error) {
var bodyReader io.Reader
if body != nil {
data, err := json.Marshal(body)
if err != nil {
return nil, 0, fmt.Errorf("marshal: %w", err)
}
bodyReader = bytes.NewReader(data)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bodyReader)
if err != nil {
return nil, 0, err
}
req.Header.Set("Content-Type", "application/json")
if c.token != "" {
req.Header.Set("Authorization", "Bearer "+c.token)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
result, err := io.ReadAll(resp.Body)
return result, resp.StatusCode, err
}9.2 Cookie 详解
go
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/profile", profileHandler)
http.HandleFunc("/logout", logoutHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
// 登录:设置 Cookie
func loginHandler(w http.ResponseWriter, r *http.Request) {
// ==================== 设置 Cookie ====================
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: "abc123def456",
Path: "/", // 对所有路径生效
Domain: "", // 空=当前域名
MaxAge: 3600, // 过期时间(秒),优先于 Expires
Expires: time.Now().Add(1 * time.Hour),
Secure: false, // true=仅 HTTPS 传输
HttpOnly: true, // true=JS 无法读取(防 XSS)
SameSite: http.SameSiteLaxMode, // 防 CSRF
})
// 设置多个 Cookie
http.SetCookie(w, &http.Cookie{
Name: "user_name",
Value: "alice",
Path: "/",
MaxAge: 3600,
})
fmt.Fprintf(w, "登录成功,Cookie 已设置")
}
// 读取 Cookie
func profileHandler(w http.ResponseWriter, r *http.Request) {
// ==================== 读取单个 Cookie ====================
cookie, err := r.Cookie("session_id")
if err != nil {
if err == http.ErrNoCookie {
http.Error(w, "未登录", 401)
return
}
http.Error(w, err.Error(), 400)
return
}
fmt.Fprintf(w, "Session: %s\n", cookie.Value)
// ==================== 读取所有 Cookie ====================
for _, c := range r.Cookies() {
fmt.Fprintf(w, "Cookie: %s = %s\n", c.Name, c.Value)
}
}
// 登出:删除 Cookie
func logoutHandler(w http.ResponseWriter, r *http.Request) {
// ==================== 删除 Cookie ====================
// 设置 MaxAge = -1 或过去的 Expires
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: "",
Path: "/",
MaxAge: -1, // 立即过期
})
http.SetCookie(w, &http.Cookie{
Name: "user_name",
Value: "",
Path: "/",
MaxAge: -1,
})
fmt.Fprintf(w, "已登出,Cookie 已清除")
}客户端自动管理 Cookie:
go
package main
import (
"fmt"
"io"
"net/http"
"net/http/cookiejar"
)
func main() {
// ==================== CookieJar:自动管理 Cookie ====================
// 创建 Jar,后续请求会自动携带之前收到的 Cookie
jar, _ := cookiejar.New(nil)
client := &http.Client{Jar: jar}
// 登录(服务器 Set-Cookie)
resp, _ := client.Get("http://localhost:8080/login")
resp.Body.Close()
// 访问需要登录的页面(自动携带 Cookie)
resp, _ = client.Get("http://localhost:8080/profile")
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println(string(body))
// 输出: Session: abc123def456
}10. 路由 Mux
10.1 标准库 ServeMux(Go 1.22+ 增强版)
go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
// ==================== Go 1.22+ 新语法:方法 + 路径模式 ====================
// 精确匹配方法
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
// 路径参数(Go 1.22+)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
// 通配符匹配剩余路径
mux.HandleFunc("GET /files/{path...}", serveFile)
// 静态文件
mux.Handle("GET /static/",
http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
// 首页
mux.HandleFunc("GET /{$}", homeHandler) // {$} 精确匹配 /,不匹配 /abc
fmt.Println("Server: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "首页")
}
func listUsers(w http.ResponseWriter, r *http.Request) {
page := r.URL.Query().Get("page")
fmt.Fprintf(w, "用户列表, page=%s", page)
}
func createUser(w http.ResponseWriter, r *http.Request) {
var body map[string]interface{}
json.NewDecoder(r.Body).Decode(&body)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]interface{}{
"message": "created",
"data": body,
})
}
// Go 1.22+ 用 r.PathValue 获取路径参数
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // 获取 {id} 的值
fmt.Fprintf(w, "获取用户: %s", id)
}
func updateUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "更新用户: %s", id)
}
func deleteUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
w.WriteHeader(http.StatusNoContent)
_ = id
}
func serveFile(w http.ResponseWriter, r *http.Request) {
path := r.PathValue("path") // 获取 {path...} 的值
fmt.Fprintf(w, "请求文件: %s", path)
}10.2 中间件
go
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// ==================== 中间件 = 装饰 Handler ====================
// func(http.Handler) http.Handler
// 日志中间件
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 包装 ResponseWriter 以捕获状态码
ww := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(ww, r)
log.Printf("%s %s %d %s",
r.Method, r.URL.Path, ww.status, time.Since(start))
})
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (w *statusWriter) WriteHeader(code int) {
w.status = code
w.ResponseWriter.WriteHeader(code)
}
// 认证中间件
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, `{"error":"unauthorized"}`, 401)
return
}
// 验证 token...
next.ServeHTTP(w, r)
})
}
// CORS 中间件
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// Recovery 中间件(捕获 panic)
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("PANIC: %v", err)
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
// 中间件链式组合
func chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
// 从后往前包装,最后一个中间件最先执行
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/data", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `{"message":"hello"}`)
})
mux.HandleFunc("GET /api/panic", func(w http.ResponseWriter, r *http.Request) {
panic("something went wrong") // 会被 recovery 中间件捕获
})
// 组合中间件:请求 → Recovery → CORS → Logging → Auth → Handler
handler := chain(mux,
recoveryMiddleware,
corsMiddleware,
loggingMiddleware,
authMiddleware,
)
log.Fatal(http.ListenAndServe(":8080", handler))
}11. HTTP 传输大文件
11.1 大文件下载(服务端)
go
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
)
func main() {
http.HandleFunc("/download/", downloadHandler)
http.HandleFunc("/upload", uploadHandler)
fmt.Println("Server: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// downloadHandler 支持断点续传的大文件下载
func downloadHandler(w http.ResponseWriter, r *http.Request) {
filename := strings.TrimPrefix(r.URL.Path, "/download/")
if filename == "" {
http.Error(w, "filename required", 400)
return
}
file, err := os.Open(filename)
if err != nil {
http.Error(w, "File not found", 404)
return
}
defer file.Close()
info, _ := file.Stat()
// ==================== 方式1:http.ServeFile(最简单,自动处理 Range)====================
// http.ServeFile 自动处理:
// - Content-Type 检测
// - Range 请求(断点续传)
// - If-Modified-Since 缓存
// - Content-Disposition(下载文件名)
// 设置为附件下载(而非浏览器预览)
w.Header().Set("Content-Disposition",
fmt.Sprintf(`attachment; filename="%s"`, info.Name()))
http.ServeFile(w, r, filename)
}
// ==================== 方式2:手动处理 Range 请求 ====================
func manualRangeDownload(w http.ResponseWriter, r *http.Request, filepath string) {
file, err := os.Open(filepath)
if err != nil {
http.Error(w, "Not Found", 404)
return
}
defer file.Close()
info, _ := file.Stat()
fileSize := info.Size()
// 告诉客户端支持断点续传
w.Header().Set("Accept-Ranges", "bytes")
w.Header().Set("Content-Type", "application/octet-stream")
// 检查 Range 请求头
rangeHeader := r.Header.Get("Range")
if rangeHeader == "" {
// 普通完整下载
w.Header().Set("Content-Length", strconv.FormatInt(fileSize, 10))
w.WriteHeader(200)
io.Copy(w, file)
return
}
// 解析 Range: bytes=start-end
var start, end int64
fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end)
if end == 0 {
end = fileSize - 1
}
// 定位到起始位置
file.Seek(start, io.SeekStart)
contentLength := end - start + 1
// 206 Partial Content
w.Header().Set("Content-Length", strconv.FormatInt(contentLength, 10))
w.Header().Set("Content-Range",
fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize))
w.WriteHeader(http.StatusPartialContent)
io.CopyN(w, file, contentLength)
}11.2 大文件上传(服务端)
go
// 大文件分块上传服务端
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed", 405)
return
}
// 限制请求体大小(防止 OOM)
r.Body = http.MaxBytesReader(w, r.Body, 500*1024*1024) // 500MB 上限
// 解析 multipart,第一个参数是内存上限,超出写临时文件
err := r.ParseMultipartForm(32 << 20) // 32MB 内存
if err != nil {
http.Error(w, "文件太大或格式错误: "+err.Error(), 413)
return
}
defer r.MultipartForm.RemoveAll() // 清理临时文件
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "获取文件失败", 400)
return
}
defer file.Close()
// 流式写入磁盘
dst, err := os.Create("uploads/" + header.Filename)
if err != nil {
http.Error(w, "创建文件失败", 500)
return
}
defer dst.Close()
written, err := io.Copy(dst, file) // 流式拷贝,不吃内存
if err != nil {
http.Error(w, "保存失败", 500)
return
}
fmt.Fprintf(w, `{"filename":"%s","size":%d}`, header.Filename, written)
}11.3 大文件下载(客户端,断点续传)
go
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
)
// DownloadWithResume 支持断点续传的下载
func DownloadWithResume(url, filepath string) error {
// 检查已下载的部分
var startPos int64 = 0
if info, err := os.Stat(filepath); err == nil {
startPos = info.Size()
}
// 创建请求
req, _ := http.NewRequest("GET", url, nil)
if startPos > 0 {
// 设置 Range 头,从上次断点继续
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", startPos))
fmt.Printf("断点续传: 从 %d 字节继续\n", startPos)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
// 检查服务器是否支持 Range
if startPos > 0 && resp.StatusCode != http.StatusPartialContent {
// 服务器不支持断点续传,从头开始
startPos = 0
fmt.Println("服务器不支持断点续传,从头下载")
}
// 获取总大小
var totalSize int64
if resp.StatusCode == http.StatusPartialContent {
// Content-Range: bytes 1000-9999/10000
cr := resp.Header.Get("Content-Range")
fmt.Sscanf(cr, "bytes %d-%d/%d", new(int64), new(int64), &totalSize)
} else {
totalSize, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
}
// 打开文件(追加模式)
flag := os.O_CREATE | os.O_WRONLY
if startPos > 0 {
flag |= os.O_APPEND
} else {
flag |= os.O_TRUNC
}
file, err := os.OpenFile(filepath, flag, 0644)
if err != nil {
return err
}
defer file.Close()
// 流式下载,带进度
buf := make([]byte, 32*1024) // 32KB 缓冲
downloaded := startPos
for {
n, err := resp.Body.Read(buf)
if n > 0 {
file.Write(buf[:n])
downloaded += int64(n)
if totalSize > 0 {
pct := float64(downloaded) / float64(totalSize) * 100
fmt.Printf("\r下载进度: %.1f%% (%d / %d)", pct, downloaded, totalSize)
}
}
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("download error at %d bytes: %w", downloaded, err)
}
}
fmt.Printf("\n下载完成: %s (%d 字节)\n", filepath, downloaded)
return nil
}
func main() {
err := DownloadWithResume(
"http://localhost:8080/download/largefile.zip",
"largefile.zip",
)
if err != nil {
fmt.Println("下载失败:", err)
}
}附录:net/http 常用 API 速查
text
Server 端:
http.ListenAndServe(addr, handler) 启动 HTTP 服务
http.ListenAndServeTLS(addr, cert, key, h) 启动 HTTPS 服务
http.HandleFunc(pattern, handler) 注册路由
http.Handle(pattern, handler) 注册 Handler 接口
http.FileServer(root) 静态文件服务
http.StripPrefix(prefix, handler) 去除 URL 前缀
http.ServeFile(w, r, filename) 发送文件(支持 Range)
http.Error(w, message, code) 发送错误响应
http.Redirect(w, r, url, code) 重定向
http.SetCookie(w, cookie) 设置 Cookie
http.MaxBytesReader(w, r, n) 限制请求体大小
Client 端:
http.Get(url) GET 请求
http.Post(url, contentType, body) POST 请求
http.PostForm(url, values) 表单 POST
http.Head(url) HEAD 请求
http.NewRequest(method, url, body) 创建自定义请求
http.NewRequestWithContext(ctx, ...) 带 context 的请求
client.Do(req) 发送请求
Request 对象:
r.Method 请求方法
r.URL URL 对象
r.URL.Query() URL 参数
r.PathValue(name) 路径参数(Go 1.22+)
r.Header 请求头
r.Body 请求体(io.ReadCloser)
r.Cookie(name) 获取 Cookie
r.Cookies() 获取所有 Cookie
r.FormValue(key) 获取表单值
r.FormFile(key) 获取上传文件
r.ParseForm() 解析表单
r.ParseMultipartForm(maxMemory) 解析 multipart
r.RemoteAddr 客户端地址
r.ContentLength Content-Length
ResponseWriter 对象:
w.Header() 响应头(必须在 Write 前设置)
w.WriteHeader(statusCode) 设置状态码
w.Write(data) 写入响应体