Go Web 编程快速入门 · 04 - 请求对象 Request:头、体与查询参数

在前面的文章中,我们学会了如何搭建基础的Web服务和处理路由。现在该深入了解HTTP请求的核心------Request对象了。每个HTTP请求都包含丰富的信息:URL参数、请求头、请求体等,掌握这些数据的提取和处理是Web开发的基本功。

1 Request对象结构解析

Go的http.Request结构体包含了HTTP请求的所有信息。我们先来看看它的主要字段:

go 复制代码
type Request struct {
    Method string        // HTTP方法:GET、POST、PUT等
    URL    *url.URL      // 请求URL,包含路径和查询参数
    Header Header        // 请求头信息
    Body   io.ReadCloser // 请求体
    Form   url.Values    // 解析后的表单数据
    // ... 其他字段
}

这个结构体设计得很巧妙,把HTTP协议的各个部分都映射成了Go的数据类型。URL字段不是简单的字符串,而是一个结构体,这样我们就能方便地访问路径、查询参数等信息。

1.1 URL结构深入理解

URL结构体包含了请求地址的各个组成部分:

go 复制代码
// 演示URL结构的各个部分
func urlAnalysisHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "完整URL: %s\n", r.URL.String())
    fmt.Fprintf(w, "路径: %s\n", r.URL.Path)
    fmt.Fprintf(w, "原始查询字符串: %s\n", r.URL.RawQuery)
    fmt.Fprintf(w, "片段标识符: %s\n", r.URL.Fragment)
    
    // 如果有查询参数,逐个显示
    if len(r.URL.Query()) > 0 {
        fmt.Fprintf(w, "\n查询参数详情:\n")
        for key, values := range r.URL.Query() {
            for _, value := range values {
                fmt.Fprintf(w, "  %s = %s\n", key, value)
            }
        }
    }
}

当你访问/analysis?name=张三&age=25&hobby=编程&hobby=阅读时,这个处理器会清晰地展示URL的各个组成部分。

2 查询参数处理技巧

查询参数是Web应用中最常见的数据传递方式。Go提供了多种方法来处理这些参数。

2.1 基础参数获取

go 复制代码
func queryParamsHandler(w http.ResponseWriter, r *http.Request) {
    // 获取单个参数值
    name := r.URL.Query().Get("name")
    if name == "" {
        name = "匿名用户"
    }
    
    // 获取可能有多个值的参数
    hobbies := r.URL.Query()["hobby"]
    
    // 检查参数是否存在
    _, hasAge := r.URL.Query()["age"]
    
    fmt.Fprintf(w, "用户名: %s\n", name)
    fmt.Fprintf(w, "是否提供年龄: %t\n", hasAge)
    
    if len(hobbies) > 0 {
        fmt.Fprintf(w, "爱好列表:\n")
        for i, hobby := range hobbies {
            fmt.Fprintf(w, "  %d. %s\n", i+1, hobby)
        }
    }
}

这里有个细节需要注意:Get()方法只返回第一个值,如果参数可能有多个值,要直接访问map。

2.2 参数验证和类型转换

实际项目中,我们经常需要验证参数格式和转换数据类型:

go 复制代码
import (
    "strconv"
    "regexp"
    "time"
)

// 用户查询参数结构体
type UserQuery struct {
    Name     string
    Age      int
    Email    string
    Page     int
    PageSize int
}

func parseUserQuery(r *http.Request) (*UserQuery, error) {
    query := &UserQuery{
        Page:     1,    // 默认第一页
        PageSize: 10,   // 默认每页10条
    }
    
    // 解析姓名(必需参数)
    query.Name = strings.TrimSpace(r.URL.Query().Get("name"))
    if query.Name == "" {
        return nil, fmt.Errorf("姓名参数不能为空")
    }
    
    // 解析年龄(可选参数)
    if ageStr := r.URL.Query().Get("age"); ageStr != "" {
        age, err := strconv.Atoi(ageStr)
        if err != nil || age < 0 || age > 150 {
            return nil, fmt.Errorf("年龄参数格式错误")
        }
        query.Age = age
    }
    
    // 解析邮箱(可选参数,但需要格式验证)
    if email := r.URL.Query().Get("email"); email != "" {
        emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
        if !emailRegex.MatchString(email) {
            return nil, fmt.Errorf("邮箱格式不正确")
        }
        query.Email = email
    }
    
    // 解析分页参数
    if pageStr := r.URL.Query().Get("page"); pageStr != "" {
        page, err := strconv.Atoi(pageStr)
        if err != nil || page < 1 {
            return nil, fmt.Errorf("页码参数错误")
        }
        query.Page = page
    }
    
    if sizeStr := r.URL.Query().Get("page_size"); sizeStr != "" {
        size, err := strconv.Atoi(sizeStr)
        if err != nil || size < 1 || size > 100 {
            return nil, fmt.Errorf("每页数量参数错误")
        }
        query.PageSize = size
    }
    
    return query, nil
}

func userSearchHandler(w http.ResponseWriter, r *http.Request) {
    // 解析查询参数
    userQuery, err := parseUserQuery(r)
    if err != nil {
        http.Error(w, "参数错误: "+err.Error(), http.StatusBadRequest)
        return
    }
    
    // 模拟数据库查询
    fmt.Fprintf(w, "搜索条件:\n")
    fmt.Fprintf(w, "姓名: %s\n", userQuery.Name)
    if userQuery.Age > 0 {
        fmt.Fprintf(w, "年龄: %d\n", userQuery.Age)
    }
    if userQuery.Email != "" {
        fmt.Fprintf(w, "邮箱: %s\n", userQuery.Email)
    }
    fmt.Fprintf(w, "分页: 第%d页,每页%d条\n", userQuery.Page, userQuery.PageSize)
}

这种封装方式让参数处理变得更加规范和可维护。

3 请求头信息处理

HTTP请求头包含了客户端的各种信息:浏览器类型、接受的内容格式、认证信息等。

3.1 常用请求头获取

go 复制代码
func headerAnalysisHandler(w http.ResponseWriter, r *http.Request) {
    // 获取常用请求头
    userAgent := r.Header.Get("User-Agent")
    contentType := r.Header.Get("Content-Type")
    accept := r.Header.Get("Accept")
    authorization := r.Header.Get("Authorization")
    
    // 获取客户端IP(考虑代理情况)
    clientIP := getClientIP(r)
    
    fmt.Fprintf(w, "客户端信息分析:\n")
    fmt.Fprintf(w, "IP地址: %s\n", clientIP)
    fmt.Fprintf(w, "浏览器: %s\n", parseUserAgent(userAgent))
    fmt.Fprintf(w, "内容类型: %s\n", contentType)
    fmt.Fprintf(w, "接受格式: %s\n", accept)
    
    if authorization != "" {
        fmt.Fprintf(w, "认证信息: %s\n", maskSensitiveInfo(authorization))
    }
    
    // 显示所有自定义请求头
    fmt.Fprintf(w, "\n自定义请求头:\n")
    for name, values := range r.Header {
        if strings.HasPrefix(name, "X-") {
            fmt.Fprintf(w, "  %s: %s\n", name, strings.Join(values, ", "))
        }
    }
}

// 获取真实客户端IP
func getClientIP(r *http.Request) string {
    // 检查X-Forwarded-For头(代理服务器设置)
    if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
        // 取第一个IP(原始客户端IP)
        if ips := strings.Split(xff, ","); len(ips) > 0 {
            return strings.TrimSpace(ips[0])
        }
    }
    
    // 检查X-Real-IP头
    if xri := r.Header.Get("X-Real-IP"); xri != "" {
        return xri
    }
    
    // 使用RemoteAddr(可能包含端口号)
    if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
        return ip
    }
    
    return r.RemoteAddr
}

// 简单的User-Agent解析
func parseUserAgent(ua string) string {
    if strings.Contains(ua, "Chrome") {
        return "Chrome浏览器"
    } else if strings.Contains(ua, "Firefox") {
        return "Firefox浏览器"
    } else if strings.Contains(ua, "Safari") {
        return "Safari浏览器"
    } else if strings.Contains(ua, "curl") {
        return "curl命令行工具"
    }
    return "未知客户端"
}

// 脱敏处理敏感信息
func maskSensitiveInfo(info string) string {
    if len(info) <= 8 {
        return "***"
    }
    return info[:4] + "***" + info[len(info)-4:]
}

3.2 内容协商处理

根据客户端的Accept头返回不同格式的数据:

go 复制代码
func contentNegotiationHandler(w http.ResponseWriter, r *http.Request) {
    accept := r.Header.Get("Accept")
    
    // 用户数据
    user := map[string]interface{}{
        "id":    1,
        "name":  "张三",
        "email": "zhangsan@example.com",
        "age":   25,
    }
    
    // 根据Accept头返回不同格式
    switch {
    case strings.Contains(accept, "application/json"):
        w.Header().Set("Content-Type", "application/json; charset=utf-8")
        json.NewEncoder(w).Encode(user)
        
    case strings.Contains(accept, "application/xml"):
        w.Header().Set("Content-Type", "application/xml; charset=utf-8")
        fmt.Fprintf(w, `<?xml version="1.0" encoding="UTF-8"?>
<user>
    <id>%v</id>
    <name>%s</name>
    <email>%s</email>
    <age>%v</age>
</user>`, user["id"], user["name"], user["email"], user["age"])
        
    default:
        // 默认返回纯文本
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        fmt.Fprintf(w, "用户信息:\nID: %v\n姓名: %s\n邮箱: %s\n年龄: %v\n",
            user["id"], user["name"], user["email"], user["age"])
    }
}

4 请求体数据解析

POST、PUT等请求通常会携带请求体数据,常见的格式有JSON、表单数据、文件上传等。

4.1 JSON数据处理

go 复制代码
// 用户注册数据结构
type UserRegistration struct {
    Username string `json:"username"`
    Email    string `json:"email"`
    Password string `json:"password"`
    Age      int    `json:"age"`
    Hobbies  []string `json:"hobbies"`
}

func userRegistrationHandler(w http.ResponseWriter, r *http.Request) {
    // 检查请求方法
    if r.Method != http.MethodPost {
        http.Error(w, "只支持POST请求", http.StatusMethodNotAllowed)
        return
    }
    
    // 检查Content-Type
    contentType := r.Header.Get("Content-Type")
    if !strings.Contains(contentType, "application/json") {
        http.Error(w, "请求头Content-Type必须是application/json", http.StatusBadRequest)
        return
    }
    
    // 限制请求体大小(防止恶意大文件攻击)
    r.Body = http.MaxBytesReader(w, r.Body, 1024*1024) // 1MB限制
    
    // 解析JSON数据
    var user UserRegistration
    decoder := json.NewDecoder(r.Body)
    decoder.DisallowUnknownFields() // 不允许未知字段
    
    if err := decoder.Decode(&user); err != nil {
        http.Error(w, "JSON格式错误: "+err.Error(), http.StatusBadRequest)
        return
    }
    
    // 数据验证
    if err := validateUserRegistration(&user); err != nil {
        http.Error(w, "数据验证失败: "+err.Error(), http.StatusBadRequest)
        return
    }
    
    // 模拟保存用户
    fmt.Printf("新用户注册: %+v\n", user)
    
    // 返回成功响应
    w.Header().Set("Content-Type", "application/json")
    response := map[string]interface{}{
        "success": true,
        "message": "注册成功",
        "user_id": 12345,
    }
    json.NewEncoder(w).Encode(response)
}

func validateUserRegistration(user *UserRegistration) error {
    if user.Username == "" {
        return fmt.Errorf("用户名不能为空")
    }
    if len(user.Username) < 3 {
        return fmt.Errorf("用户名至少3个字符")
    }
    
    emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
    if !emailRegex.MatchString(user.Email) {
        return fmt.Errorf("邮箱格式不正确")
    }
    
    if len(user.Password) < 6 {
        return fmt.Errorf("密码至少6个字符")
    }
    
    if user.Age < 13 || user.Age > 120 {
        return fmt.Errorf("年龄必须在13-120之间")
    }
    
    return nil
}

4.2 表单数据处理

go 复制代码
func formDataHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method == http.MethodGet {
        // 显示表单页面
        showRegistrationForm(w)
        return
    }
    
    if r.Method != http.MethodPost {
        http.Error(w, "只支持GET和POST请求", http.StatusMethodNotAllowed)
        return
    }
    
    // 解析表单数据(包括multipart/form-data)
    err := r.ParseMultipartForm(10 << 20) // 10MB内存限制
    if err != nil {
        // 如果不是multipart,尝试解析普通表单
        if err := r.ParseForm(); err != nil {
            http.Error(w, "表单解析失败", http.StatusBadRequest)
            return
        }
    }
    
    // 获取表单字段
    username := r.FormValue("username")
    email := r.FormValue("email")
    password := r.FormValue("password")
    
    // 获取多选字段
    hobbies := r.Form["hobbies"]
    
    // 处理文件上传
    file, header, err := r.FormFile("avatar")
    if err == nil {
        defer file.Close()
        fmt.Printf("上传文件: %s, 大小: %d字节\n", header.Filename, header.Size)
        
        // 这里可以保存文件
        // saveUploadedFile(file, header)
    }
    
    fmt.Fprintf(w, "表单提交成功!\n")
    fmt.Fprintf(w, "用户名: %s\n", username)
    fmt.Fprintf(w, "邮箱: %s\n", email)
    fmt.Fprintf(w, "爱好: %s\n", strings.Join(hobbies, ", "))
    
    if err == nil {
        fmt.Fprintf(w, "头像: %s\n", header.Filename)
    }
}

func showRegistrationForm(w http.ResponseWriter) {
    html := `
<!DOCTYPE html>
<html>
<head>
    <title>用户注册</title>
    <meta charset="utf-8">
</head>
<body>
    <h2>用户注册表单</h2>
    <form method="post" enctype="multipart/form-data">
        <p>
            <label>用户名:</label><br>
            <input type="text" name="username" required>
        </p>
        <p>
            <label>邮箱:</label><br>
            <input type="email" name="email" required>
        </p>
        <p>
            <label>密码:</label><br>
            <input type="password" name="password" required>
        </p>
        <p>
            <label>爱好:</label><br>
            <input type="checkbox" name="hobbies" value="编程"> 编程<br>
            <input type="checkbox" name="hobbies" value="阅读"> 阅读<br>
            <input type="checkbox" name="hobbies" value="运动"> 运动<br>
        </p>
        <p>
            <label>头像:</label><br>
            <input type="file" name="avatar" accept="image/*">
        </p>
        <p>
            <input type="submit" value="注册">
        </p>
    </form>
</body>
</html>`
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.Write([]byte(html))
}

5 综合实战:用户管理API

让我们把前面学到的知识整合起来,构建一个完整的用户管理API:

go 复制代码
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "strconv"
    "strings"
    "time"
)

// 用户数据结构
type User struct {
    ID       int       `json:"id"`
    Username string    `json:"username"`
    Email    string    `json:"email"`
    Age      int       `json:"age"`
    CreateAt time.Time `json:"create_at"`
}

// 模拟用户数据库
var users = []User{
    {1, "张三", "zhangsan@example.com", 25, time.Now().AddDate(0, -1, 0)},
    {2, "李四", "lisi@example.com", 30, time.Now().AddDate(0, -2, 0)},
    {3, "王五", "wangwu@example.com", 28, time.Now().AddDate(0, -3, 0)},
}
var nextID = 4

func main() {
    // 用户相关路由
    http.HandleFunc("/users", usersHandler)
    http.HandleFunc("/users/", userDetailHandler)
    
    // 启动服务器
    fmt.Println("用户管理API服务启动在 http://localhost:8080")
    fmt.Println("API端点:")
    fmt.Println("  GET  /users          - 获取用户列表")
    fmt.Println("  POST /users          - 创建新用户")
    fmt.Println("  GET  /users/{id}     - 获取用户详情")
    fmt.Println("  PUT  /users/{id}     - 更新用户信息")
    fmt.Println("  DELETE /users/{id}   - 删除用户")
    
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        getUserList(w, r)
    case http.MethodPost:
        createUser(w, r)
    default:
        http.Error(w, "不支持的请求方法", http.StatusMethodNotAllowed)
    }
}

func userDetailHandler(w http.ResponseWriter, r *http.Request) {
    // 提取用户ID
    path := strings.TrimPrefix(r.URL.Path, "/users/")
    if path == "" {
        http.Error(w, "缺少用户ID", http.StatusBadRequest)
        return
    }
    
    userID, err := strconv.Atoi(path)
    if err != nil {
        http.Error(w, "用户ID格式错误", http.StatusBadRequest)
        return
    }
    
    switch r.Method {
    case http.MethodGet:
        getUserDetail(w, r, userID)
    case http.MethodPut:
        updateUser(w, r, userID)
    case http.MethodDelete:
        deleteUser(w, r, userID)
    default:
        http.Error(w, "不支持的请求方法", http.StatusMethodNotAllowed)
    }
}

// 获取用户列表(支持分页和搜索)
func getUserList(w http.ResponseWriter, r *http.Request) {
    // 解析查询参数
    page, _ := strconv.Atoi(r.URL.Query().Get("page"))
    if page < 1 {
        page = 1
    }
    
    pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
    if pageSize < 1 || pageSize > 100 {
        pageSize = 10
    }
    
    search := strings.TrimSpace(r.URL.Query().Get("search"))
    
    // 过滤用户
    filteredUsers := users
    if search != "" {
        filteredUsers = []User{}
        for _, user := range users {
            if strings.Contains(strings.ToLower(user.Username), strings.ToLower(search)) ||
               strings.Contains(strings.ToLower(user.Email), strings.ToLower(search)) {
                filteredUsers = append(filteredUsers, user)
            }
        }
    }
    
    // 分页处理
    total := len(filteredUsers)
    start := (page - 1) * pageSize
    end := start + pageSize
    
    if start >= total {
        filteredUsers = []User{}
    } else if end > total {
        filteredUsers = filteredUsers[start:]
    } else {
        filteredUsers = filteredUsers[start:end]
    }
    
    // 构建响应
    response := map[string]interface{}{
        "users": filteredUsers,
        "pagination": map[string]interface{}{
            "page":       page,
            "page_size":  pageSize,
            "total":      total,
            "total_pages": (total + pageSize - 1) / pageSize,
        },
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(response)
}

// 创建新用户
func createUser(w http.ResponseWriter, r *http.Request) {
    var newUser User
    if err := json.NewDecoder(r.Body).Decode(&newUser); err != nil {
        http.Error(w, "JSON格式错误", http.StatusBadRequest)
        return
    }
    
    // 数据验证
    if newUser.Username == "" || newUser.Email == "" {
        http.Error(w, "用户名和邮箱不能为空", http.StatusBadRequest)
        return
    }
    
    // 检查邮箱是否已存在
    for _, user := range users {
        if user.Email == newUser.Email {
            http.Error(w, "邮箱已存在", http.StatusConflict)
            return
        }
    }
    
    // 创建用户
    newUser.ID = nextID
    nextID++
    newUser.CreateAt = time.Now()
    users = append(users, newUser)
    
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(newUser)
}

// 获取用户详情
func getUserDetail(w http.ResponseWriter, r *http.Request, userID int) {
    for _, user := range users {
        if user.ID == userID {
            w.Header().Set("Content-Type", "application/json")
            json.NewEncoder(w).Encode(user)
            return
        }
    }
    
    http.Error(w, "用户不存在", http.StatusNotFound)
}

// 更新用户信息
func updateUser(w http.ResponseWriter, r *http.Request, userID int) {
    // 找到用户
    userIndex := -1
    for i, user := range users {
        if user.ID == userID {
            userIndex = i
            break
        }
    }
    
    if userIndex == -1 {
        http.Error(w, "用户不存在", http.StatusNotFound)
        return
    }
    
    // 解析更新数据
    var updateData User
    if err := json.NewDecoder(r.Body).Decode(&updateData); err != nil {
        http.Error(w, "JSON格式错误", http.StatusBadRequest)
        return
    }
    
    // 更新用户信息(保留原有ID和创建时间)
    users[userIndex].Username = updateData.Username
    users[userIndex].Email = updateData.Email
    users[userIndex].Age = updateData.Age
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users[userIndex])
}

// 删除用户
func deleteUser(w http.ResponseWriter, r *http.Request, userID int) {
    for i, user := range users {
        if user.ID == userID {
            // 删除用户
            users = append(users[:i], users[i+1:]...)
            w.WriteHeader(http.StatusNoContent)
            return
        }
    }
    
    http.Error(w, "用户不存在", http.StatusNotFound)
}

这个完整的用户管理API展示了Request对象的各种用法:查询参数处理、JSON请求体解析、路径参数提取等。

通过这篇文章,我们深入了解了Go Web编程中Request对象的使用方法。从基础的URL解析到复杂的数据验证,从简单的查询参数到完整的RESTful API,这些技能是构建现代Web应用的基础。

下一篇文章我们将学习响应对象Response的处理,包括状态码设置、响应头管理、不同格式数据的输出等内容。

相关推荐
祈祷苍天赐我java之术3 小时前
Redis 数据类型与使用场景
java·开发语言·前端·redis·分布式·spring·bootstrap
草莓熊Lotso4 小时前
C++ 方向 Web 自动化测试入门指南:从概念到 Selenium 实战
前端·c++·python·selenium
Olrookie4 小时前
若依前后端分离版学习笔记(二十)——实现滑块验证码(vue3)
java·前端·笔记·后端·学习·vue·ruoyi
533_5 小时前
[vue] dayjs 显示实时时间
前端·javascript·vue.js
故事与他6455 小时前
XSS_and_Mysql_file靶场攻略
前端·学习方法·xss
莫的感情6 小时前
下载按钮点击一次却下载两个文件问题
前端
一个很帅的帅哥6 小时前
JavaScript事件循环
开发语言·前端·javascript
驰羽6 小时前
[GO]gin框架:ShouldBindJSON与其他常见绑定方法
开发语言·golang·gin
小宁爱Python6 小时前
Django Web 开发系列(二):视图进阶、快捷函数与请求响应处理
前端·django·sqlite