10.1 最小 HTTP 服务器
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// fmt.Fprintf() 将格式化后的字符串写入到指定输出流的函数
fmt.Fprintf(w, "Hello, World!")
})
fmt.Println("服务器启动在 http://localhost:8080")
// 用 log.Fatal 捕获错误,如果启动失败会打印错误信息并退出
log.Fatal(http.ListenAndServe(":8080", nil))
}
运行:go run main.go,浏览器访问 http://localhost:8080 就能看到 "Hello, World!"。
对比 Node.js 原生 http:
const http = require('http')
http.createServer((req, res) => {
res.end('Hello, World!')
}).listen(8080)
Go 版本结构类似,但路由注册是声明式的(http.HandleFunc),不用写 if-else 判断 URL。
解析两个核心参数
func handler(w http.ResponseWriter, r *http.Request) {
// w:写入响应(ResponseWriter)
// r:读取请求(Request)
}
http.ResponseWriter:往这里写响应内容(写头、写 body)*http.Request:从这里读请求信息(方法、URL、Header、Body)
10.2 路由:HandleFunc 与 Handle
HandleFunc(最常用,函数式)
http.HandleFunc("/users", usersHandler)
http.HandleFunc("/posts", postsHandler)
func usersHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "用户列表")
}
路由匹配规则
http.HandleFunc("/api", handler1) // 只匹配 /api
http.HandleFunc("/api/", handler2) // 匹配 /api/ 开头的所有路径(带斜杠是子树)
⚠️ 标准库路由的局限
标准库不支持路径参数:
// 想实现 /users/123 获取 ID=123 的用户
// 标准库做不到!只能写 /users/ 然后在 handler 里手动解析 r.URL.Path
这就是为什么需要框架(Gin、Echo、Chi)。框架提供:
- 路径参数
/users/:id - 查询参数便捷访问
- 中间件
- JSON 绑定
- 路由分组
下一章我们用 Gin,但先看标准库能做什么。
10.3 读取请求:方法、URL、Header、Body
func handler(w http.ResponseWriter, r *http.Request) {
// 1. HTTP 方法
method := r.Method // "GET" / "POST" / "PUT" / "DELETE"
// 2. URL 和路径
path := r.URL.Path // "/users"
query := r.URL.Query()
name := query.Get("name") // 查询参数 ?name=xxx
page := query.Get("page") // ?page=1
// 3. Header
contentType := r.Header.Get("Content-Type")
auth := r.Header.Get("Authorization")
// 4. Body(POST 的 JSON 等)
body, err := io.ReadAll(r.Body)
defer r.Body.Close()
if err != nil { ... }
fmt.Println(string(body))
}
对比 Node.js Express:
// Express
app.get('/users', (req, res) => {
req.method // 'GET'
req.path // '/users'
req.query.name // 查询参数
req.params.id // 路径参数
req.headers['auth'] // 请求头
req.body // body(需要 body-parser)
})
// Go 标准库
// req.Method, req.URL.Path, req.URL.Query().Get()
// req.Header.Get()
// 手动读 req.Body
10.4 写 JSON 响应(Go API 核心)
API 返回 JSON 是最常见场景。Go 用 encoding/json 包:
type User struct {
ID int `json:"id"` // ← 结构体 tag:控制 JSON 字段名
Name string `json:"name"`
Age int `json:"age"`
}
func userHandler(w http.ResponseWriter, r *http.Request) {
user := User{ID: 1, Name: "张三", Age: 28}
// 设置响应头
w.Header().Set("Content-Type", "application/json")
// 编码 JSON 并写入响应
json.NewEncoder(w).Encode(user)
// 或:
// data, _ := json.Marshal(user)
// w.Write(data)
}
访问后会得到:
{"id":1,"name":"张三","age":28}
结构体 Tag 详解
type User struct {
ID int `json:"id"` // JSON 字段叫 id
Name string `json:"name"` // JSON 字段叫 name
Email string `json:"email,omitempty"` // 空值时省略
Password string `json:"-"` // 完全忽略,不输出
Age int `json:"age,string"` // 序列化成字符串
}
对比 JS:JS 的对象字段名就是 JSON 字段名,无需配置。Go 是静态类型,结构体字段名(如 Name)和 JSON 字段名(如 name)通过 tag 映射。
10.5 解析 JSON 请求体(POST/PUT)
type CreateUserRequest struct {
Name string `json:"name"`
Age int `json:"age"`
}
func createUserHandler(w http.ResponseWriter, r *http.Request) {
// 只接受 POST
if r.Method != "POST" {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
// 解析 JSON body
var req CreateUserRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// 用 req.Name、req.Age ...
fmt.Fprintf(w, "创建用户: %s, %d 岁", req.Name, req.Age)
}
对比 Express:
// Express(需 body-parser 中间件)
app.post('/users', (req, res) => {
const { name, age } = req.body // 自动解析好的对象
})
// Go 标准库:手动 Decode
var req CreateUserRequest
json.NewDecoder(r.Body).Decode(&req)
10.6 完整的 CRUD API 示例
下面是一个内存版「用户管理 API」,演示 GET / POST / PUT / DELETE:
// GET /users 获取所有用户
// GET /users?id=1 获取单个用户(用 query 参数模拟)
// POST /users 创建用户
// PUT /users?id=1 更新用户
// DELETE /users?id=1 删除用户
完整代码在 code/ch10-httpapi/main.go。强烈建议你运行它,用浏览器或 Postman 测试各个接口。
测试方法
启动后:
# 浏览器访问
http://localhost:8080/users
# 或用 curl(cmd 里)
curl http://localhost:8080/users
curl -X POST http://localhost:8080/users -H "Content-Type: application/json" -d "{\"name\":\"张三\",\"age\":28}"
10.7 Handler 接口(理解框架的基础)
第 06 章学了接口,这里看它在 HTTP 里怎么用。理解这一节,你就能看懂 Gin 等框架的底层原理。
10.7.1 Handler 接口是什么
http.Handler 是一个接口(第 06 章学的接口):
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
意思是:任何有 ServeHTTP 方法的类型,都能当 HTTP 处理器用。这就是 Go HTTP 框架的根基。
10.7.2 你一直在用的 HandleFunc 是语法糖
// 这两种写法等价
http.HandleFunc("/x", myFunc)
http.Handle("/x", http.HandlerFunc(myFunc))
HandleFunc 内部把普通函数包装成了 HandlerFunc 类型(它实现了 ServeHTTP 方法)。这就是为什么你一直能传函数给 HandleFunc——背后是接口实现。
10.7.3 自己实现 Handler 接口(实战)
什么时候你需要自己实现 Handler?写中间件、自定义路由器时。看个最小例子——一个「记录访问日志」的 Handler:
// 自己定义一个类型,实现 Handler 接口
type LogHandler struct {
next http.Handler // 包装下一个 handler(中间件模式)
}
// 实现 ServeHTTP 方法 → 满足 http.Handler 接口
func (h *LogHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
h.next.ServeHTTP(w, r) // 调用真正的处理逻辑
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
}
func main() {
// 真正的业务 handler
hello := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
// 用 LogHandler 包装它
http.Handle("/", &LogHandler{next: hello})
http.ListenAndServe(":8080", nil)
}
// 访问 / 后,控制台打印:GET / 12.3µs
这就是中间件的原理——一个 Handler 包装另一个 Handler,在调用前后加自己的逻辑。第 11 章的 Gin 中间件、各种 Web 框架,底层都是这个套路。
10.7.4 为什么理解这个很重要
框架(Gin)本质上:Gin 的 gin.Engine 就是实现了 http.Handler 接口(有自己的 ServeHTTP 方法,内部做路由分发)。所以你可以这样把 Gin 嵌进标准库:
r := gin.Default()
// ... 注册路由
http.ListenAndServe(":8080", r) // r 本身就是 http.Handler
理解了「框架 = 自己实现的 Handler」,下一章学 Gin 会非常顺。
10.8 优雅关闭服务器
本节涉及信号处理和 context(第 08 章 8.8 节学的)。如果你跳读了,先去看 8.8 的 context 专题。
10.8.1 先搞懂:为什么要「优雅关闭」
假设你的服务器正在处理一个耗时 2 秒的订单请求(写数据库、发邮件)。这时候你按了 Ctrl+C 想重启服务,会发生什么?
- 直接关闭(粗暴):正在处理的请求立刻中断,可能钱扣了但订单没建完,数据不一致
- 优雅关闭(平滑):先停止接收新请求,给正在处理的请求一段缓冲时间处理完,然后再退出
生产环境必须用优雅关闭,否则会产生大量「半成品」请求。
10.8.2 最小可用版(先理解思路)
srv := &http.Server{Addr: ":8080", Handler: mux}
// 1. 把「启动服务」放 goroutine 里跑(不阻塞 main)
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err) // 真正的启动错误才退出(ErrServerClosed 是正常关闭信号)
}
}()
// 2. main 在这里等,直到收到 Ctrl+C
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt) // 监听 Ctrl+C(SIGINT 信号)
<-stop // 阻塞,直到收到信号
fmt.Println("收到关闭信号,开始优雅关闭...")
// 3. 调 Shutdown:停止接收新请求,等现有请求处理完
if err := srv.Shutdown(context.Background()); err != nil {
log.Fatal(err)
}
fmt.Println("服务器已关闭")
三个关键点:
signal.Notify(stop, os.Interrupt):监听 Ctrl+C 信号,收到时往 stop channel 发一个值<-stop:main 在这里阻塞等待,收到信号才往下走srv.Shutdown(ctx):停止接新请求,等在处理的请求完成。会阻塞直到处理完或超时
10.8.3 加超时版(生产推荐)
上面用的是 context.Background()(无限等待),万一有个请求卡死,服务永远关不掉。生产环境加个超时:
// 收到关闭信号后,给最多 10 秒处理剩余请求
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Println("超时强制关闭:", err) // 10 秒还没处理完,强制关
}
这里 context.WithTimeout 的作用就是「最多等 10 秒」。10 秒内请求处理完就正常关,10 秒还没完就强制关(参考第 08 章 8.8 节 context 超时控制)。
10.8.4 完整生产版代码
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second) // 模拟耗时请求
w.Write([]byte("done"))
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
// 启动服务(非阻塞)
go func() {
log.Println("服务器启动在 :8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("启动失败: %v", err)
}
}()
// 等待中断信号(Ctrl+C 或 kill)
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM) // 监听两种信号
<-quit
log.Println("正在关闭服务器...")
// 10 秒内处理完现有请求
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("强制关闭: %v\n", err)
}
log.Println("服务器已退出")
}
测试:启动后,访问 http://localhost:8080(请求会处理 2 秒),然后立即按 Ctrl+C。你会发现服务器没有立刻退出,而是等那个请求处理完返回 done 后才退出——这就是优雅关闭。
10.8.5 术语速查
| 概念 | 是什么 |
|---|---|
os.Signal |
操作系统的信号(Ctrl+C 是 SIGINT,kill 默认是 SIGTERM) |
signal.Notify |
注册信号监听,收到信号时往 channel 发 |
http.ErrServerClosed |
Shutdown 后 ListenAndServe 返回的错误,表示正常关闭,不是真错误 |
srv.Shutdown(ctx) |
优雅关闭:停止接新请求,等现有请求完成(受 ctx 超时控制) |
10.9 标准库的局限(为什么要用框架?)
用标准库写 5 个接口没问题,但写到 50 个接口时会遇到:
| 痛点 | 标准库 | 框架(Gin) |
|---|---|---|
路径参数 /users/:id |
手动解析 | c.Param("id") |
| 查询参数 | r.URL.Query().Get() |
c.Query("name") |
| JSON 绑定 | 手动 Decode | c.ShouldBindJSON(&req) |
| 中间件 | 自己实现 | 内置支持 |
| 路由分组 | 自己实现 | router.Group("/api") |
| 错误处理 | 手动 | 统一中间件 |
| 参数校验 | 手动 | 内置 validator |
下一章的 Gin 会优雅地解决这些问题。
10.10 Node.js / PHP ↔ Go 对照表
| 概念 | Node.js / PHP | Go | 备注 |
|---|---|---|---|
| HTTP 服务器 | http 模块 / nginx | net/http | Go 标准库很完整 |
| 路由注册 | app.get/post | http.HandleFunc | |
| 请求对象 | req / $_REQUEST | *http.Request | |
| 响应对象 | res / echo | http.ResponseWriter | |
| 查询参数 | req.query | r.URL.Query().Get() | |
| 请求体 | req.body | io.ReadAll(r.Body) | |
| 返回 JSON | res.json() | json.NewEncoder(w).Encode() | |
| 状态码 | res.status(404) | w.WriteHeader(404) | |
| 设置头 | res.set() | w.Header().Set() | |
| 监听端口 | .listen(8080) | http.ListenAndServe(":8080") | |
| JSON 字段名 | 对象属性名 | struct tag | Go 特有 |
💬 评论 0
还没有评论,快来抢沙发吧~