Skip to content

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.1

1.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>&copy; {{.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.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
}
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)                              写入响应体