HTTP 编程基础
Go 语言的标准库 net/http 提供了功能强大且易于使用的 HTTP 服务器与客户端实现,无需任何第三方框架即可构建完整的 Web 应用。本章将系统讲解 Go HTTP 编程的核心概念与实战技巧。
http.Handler 与 http.HandlerFunc
http.Handler 是 Go HTTP 处理的核心接口。任何实现了 ServeHTTP(ResponseWriter, *Request) 方法的类型都可以作为 HTTP 请求的处理器。
package main
import (
"fmt"
"net/http"
)
// 自定义类型实现 http.Handler 接口
type MyHandler struct {
greeting string
}
func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s, World!", h.greeting)
}
func main() {
handler1 := &MyHandler{greeting: "Hello"}
handler2 := &MyHandler{greeting: "Hi"}
http.Handle("/hello", handler1)
http.Handle("/hi", handler2)
http.ListenAndServe(":8080", nil)
}http.HandlerFunc 是一个适配器类型,它将普通的函数转换为 http.Handler 接口的实现。这样你无需定义新类型,直接使用函数即可处理请求。
package main
import (
"fmt"
"net/http"
)
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
// http.HandlerFunc 自动满足 http.Handler 接口
http.HandleFunc("/hello", helloHandler)
http.ListenAndServe(":8080", nil)
}http.ServeMux 路由器
Go 1.22 对 http.ServeMux 进行了重大升级,支持路径参数和方法匹配:
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
// Go 1.22+ 支持路径参数
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "获取用户: %s", id)
})
// 支持方法匹配
mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "创建用户")
})
// 通配符匹配
mux.HandleFunc("/static/{path...}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "静态文件路径: %s", r.PathValue("path"))
})
http.ListenAndServe(":8080", mux)
}http.Request 详解
http.Request 表示服务器收到的 HTTP 请求,包含了请求方法、URL、Header、Body、表单数据、Cookie 等所有请求信息。
func handler(w http.ResponseWriter, r *http.Request) {
// 请求方法
method := r.Method // "GET", "POST", "PUT", "DELETE" 等
// URL 信息
path := r.URL.Path // 请求路径,如 "/users/123"
query := r.URL.Query() // 查询参数 map
name := query.Get("name") // 获取单个查询参数
// 请求头
contentType := r.Header.Get("Content-Type")
userAgent := r.Header.Get("User-Agent")
// 解析表单数据(Content-Type: application/x-www-form-urlencoded)
r.ParseForm()
username := r.FormValue("username")
// 解析多部分表单(文件上传)
r.ParseMultipartForm(10 << 20) // 最大内存 10MB
file, handler, err := r.FormFile("avatar")
if err == nil {
defer file.Close()
// handler.Filename: 文件名
// handler.Size: 文件大小
// handler.Header: 文件头信息
}
// 请求上下文
ctx := r.Context()
requestID := ctx.Value("requestID")
}http.ResponseWriter 详解
http.ResponseWriter 用于构造 HTTP 响应,支持设置状态码、响应头和响应体。
func handler(w http.ResponseWriter, r *http.Request) {
// 设置响应头(必须在 WriteHeader 之前调用)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Custom-Header", "my-value")
// 设置状态码(默认为 200,调用后不能再修改头)
w.WriteHeader(http.StatusCreated) // 201
// 写入响应体
w.Write([]byte(`{"message": "created"}`))
}注意:WriteHeader 只能调用一次。如果未显式调用,第一次调用 Write 时会隐式发送 200 OK。一旦调用了 Write,就不能再设置响应头。
处理查询参数与表单数据
查询参数(Query Parameters)
// GET /search?q=golang&page=1&limit=10
func searchHandler(w http.ResponseWriter, r *http.Request) {
q := r.URL.Query()
keyword := q.Get("q") // "golang"
page := q.Get("page") // "1"(字符串类型,需手动转换)
limit := q.Get("limit") // "10"
// 获取多值参数
// GET /tags?tag=go&tag=web
tags := q["tag"] // []string{"go", "web"}
// 带默认值的获取方式
if page == "" {
page = "1"
}
}表单数据(Form Data)
func formHandler(w http.ResponseWriter, r *http.Request) {
// 必须先调用 ParseForm 或 ParseMultipartForm
err := r.ParseForm()
if err != nil {
http.Error(w, "解析表单失败", http.StatusBadRequest)
return
}
// 获取表单值
username := r.FormValue("username")
password := r.FormValue("password")
// PostForm 只包含 POST 请求体中的数据,不含 URL query
email := r.PostFormValue("email")
fmt.Fprintf(w, "用户名: %s", username)
}JSON 请求体
import "encoding/json"
type CreateUserRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Age int `json:"age"`
}
func createUserHandler(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
// 解析 JSON 请求体
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "无效的请求数据", http.StatusBadRequest)
return
}
// 使用请求参数...
fmt.Fprintf(w, "创建用户: %s (%d岁)", req.Username, req.Age)
}文件上传处理
package main
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
)
func uploadHandler(w http.ResponseWriter, r *http.Request) {
// 限制请求体大小(10MB)
r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
if err := r.ParseMultipartForm(10 << 20); err != nil {
http.Error(w, "文件太大", http.StatusRequestEntityTooLarge)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "获取文件失败", http.StatusBadRequest)
return
}
defer file.Close()
// 生成安全的文件名
ext := filepath.Ext(header.Filename)
filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
// 保存到指定目录
dst, err := os.Create(filepath.Join("./uploads", filename))
if err != nil {
http.Error(w, "保存文件失败", http.StatusInternalServerError)
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
http.Error(w, "写入文件失败", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "文件上传成功: %s (%d bytes)", header.Filename, header.Size)
}对应的 HTML 上传表单:
<form method="POST" action="/upload" enctype="multipart/form-data">
<input type="file" name="file" />
<button type="submit">上传</button>
</form>Cookie 与 Session 基础
Cookie 操作
func setCookieHandler(w http.ResponseWriter, r *http.Request) {
// 设置 Cookie
cookie := &http.Cookie{
Name: "username",
Value: "zhangsan",
Path: "/",
MaxAge: 3600, // 1小时(秒)
HttpOnly: true, // 禁止 JavaScript 访问
Secure: true, // 仅 HTTPS 传输
SameSite: http.SameSiteLaxMode,
}
http.SetCookie(w, cookie)
fmt.Fprint(w, "Cookie 已设置")
}
func getCookieHandler(w http.ResponseWriter, r *http.Request) {
// 读取 Cookie
cookie, err := r.Cookie("username")
if err != nil {
http.Error(w, "Cookie 不存在", http.StatusNotFound)
return
}
fmt.Fprintf(w, "用户名: %s", cookie.Value)
}
func deleteCookieHandler(w http.ResponseWriter, r *http.Request) {
// 删除 Cookie(将 MaxAge 设为 -1)
cookie := &http.Cookie{
Name: "username",
Value: "",
MaxAge: -1,
}
http.SetCookie(w, cookie)
fmt.Fprint(w, "Cookie 已删除")
}简易 Session 实现
package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"sync"
"time"
)
type Session struct {
Data map[string]interface{}
ExpireAt time.Time
}
type SessionManager struct {
mu sync.RWMutex
sessions map[string]*Session
}
func NewSessionManager() *SessionManager {
sm := &SessionManager{
sessions: make(map[string]*Session),
}
go sm.cleanup() // 后台清理过期 session
return sm
}
func (sm *SessionManager) generateID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
func (sm *SessionManager) Get(r *http.Request) (*Session, error) {
cookie, err := r.Cookie("session_id")
if err != nil {
return nil, err
}
sm.mu.RLock()
defer sm.mu.RUnlock()
session, ok := sm.sessions[cookie.Value]
if !ok || time.Now().After(session.ExpireAt) {
return nil, fmt.Errorf("session not found or expired")
}
return session, nil
}
func (sm *SessionManager) Create(w http.ResponseWriter) string {
sm.mu.Lock()
defer sm.mu.Unlock()
id := sm.generateID()
sm.sessions[id] = &Session{
Data: make(map[string]interface{}),
ExpireAt: time.Now().Add(30 * time.Minute),
}
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: id,
Path: "/",
})
return id
}
func (sm *SessionManager) cleanup() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
sm.mu.Lock()
for id, session := range sm.sessions {
if time.Now().After(session.ExpireAt) {
delete(sm.sessions, id)
}
}
sm.mu.Unlock()
}
}静态文件服务
http.FileServer 返回一个使用根目录文件系统提供文件服务的 http.Handler。配合 http.StripPrefix 可以将 URL 前缀映射到文件系统目录。
package main
import (
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
// 静态文件服务
// 访问 /static/css/style.css → 读取 ./public/css/style.css
fileServer := http.FileServer(http.Dir("./public"))
mux.Handle("/static/", http.StripPrefix("/static/", fileServer))
// 单页应用(SPA)配置:所有未匹配路由返回 index.html
spaHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "./public/index.html")
})
mux.Handle("/", spaHandler)
log.Println("服务器启动于 :8080")
http.ListenAndServe(":8080", mux)
}http.StripPrefix("/static/", fileServer) 会将请求 URL 中的 /static/ 前缀去掉,再交给 FileServer 处理。这样文件系统的目录结构不受 URL 路径前缀的影响。
练习题
练习 1:实现一个请求信息回显 API
编写一个 HTTP 处理器,接收任何 HTTP 请求,将请求的方法、URL、Header、请求体等信息以 JSON 格式返回。
package main
import (
"encoding/json"
"io"
"net/http"
)
type EchoResponse struct {
Method string `json:"method"`
URL string `json:"url"`
Header map[string]string `json:"header"`
Body string `json:"body"`
}
func echoHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
// 提取所有请求头
header := make(map[string]string)
for k, v := range r.Header {
if len(v) > 0 {
header[k] = v[0]
}
}
resp := EchoResponse{
Method: r.Method,
URL: r.URL.String(),
Header: header,
Body: string(body),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func main() {
http.HandleFunc("/echo", echoHandler)
http.ListenAndServe(":8080", nil)
}练习 2:实现图片上传 API
编写一个图片上传处理器,支持上传 JPG/PNG/GIF 格式的图片,限制文件大小为 5MB,保存到 ./uploads 目录,并返回图片的访问 URL。
package main
import (
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
var allowedExts = map[string]bool{
".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
}
func uploadImageHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "仅支持 POST 请求", http.StatusMethodNotAllowed)
return
}
// 限制 5MB
r.Body = http.MaxBytesReader(w, r.Body, 5<<20)
file, header, err := r.FormFile("image")
if err != nil {
http.Error(w, "获取文件失败", http.StatusBadRequest)
return
}
defer file.Close()
// 验证文件扩展名
ext := strings.ToLower(filepath.Ext(header.Filename))
if !allowedExts[ext] {
http.Error(w, "仅支持 JPG/PNG/GIF 格式", http.StatusBadRequest)
return
}
// 创建上传目录
if err := os.MkdirAll("./uploads", 0755); err != nil {
http.Error(w, "创建目录失败", http.StatusInternalServerError)
return
}
// 生成唯一文件名
filename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
dstPath := filepath.Join("./uploads", filename)
dst, err := os.Create(dstPath)
if err != nil {
http.Error(w, "保存文件失败", http.StatusInternalServerError)
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
http.Error(w, "写入文件失败", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"url": "/uploads/%s", "filename": "%s"}`, filename, header.Filename)
}
func main() {
http.HandleFunc("/upload/image", uploadImageHandler)
http.Handle("/uploads/", http.StripPrefix("/uploads/",
http.FileServer(http.Dir("./uploads"))))
http.ListenAndServe(":8080", nil)
}练习 3:基于 Cookie 的登录状态管理
实现 /login 和 /profile 两个接口。/login 接收用户名密码(表单提交),验证成功后设置 Cookie;/profile 通过 Cookie 识别用户,返回用户信息。
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type UserInfo struct {
Username string `json:"username"`
Role string `json:"role"`
}
// 模拟数据库
var users = map[string]string{
"admin": "admin123",
"zhangsan": "123456",
}
var userInfos = map[string]UserInfo{
"admin": {Username: "admin", Role: "administrator"},
"zhangsan": {Username: "zhangsan", Role: "member"},
}
func loginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "仅支持 POST", http.StatusMethodNotAllowed)
return
}
r.ParseForm()
username := r.FormValue("username")
password := r.FormValue("password")
if pwd, ok := users[username]; !ok || pwd != password {
http.Error(w, `{"error":"用户名或密码错误"}`, http.StatusUnauthorized)
return
}
http.SetCookie(w, &http.Cookie{
Name: "session_user",
Value: username,
Path: "/",
MaxAge: 86400,
HttpOnly: true,
})
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"message":"登录成功"}`)
}
func profileHandler(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session_user")
if err != nil {
http.Error(w, `{"error":"未登录"}`, http.StatusUnauthorized)
return
}
info, ok := userInfos[cookie.Value]
if !ok {
http.Error(w, `{"error":"用户不存在"}`, http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(info)
}
func main() {
http.HandleFunc("/login", loginHandler)
http.HandleFunc("/profile", profileHandler)
http.ListenAndServe(":8080", nil)
}