第 41 课 - HTTP 服务器基础
所属模块: 网络编程
难度: Intermediate
标签: http, server, handler, listen
上一课: HTTP 客户端
下一课: Handler 与中间件
学习目标
完成本课学习后,你将能够:
- 理解 Go 语言中 HTTP 服务器(Server)的核心组件及其作用。
- 掌握 使用
net/http标准库搭建一个基础 HTTP 服务器的完整步骤。 - 能够 为服务器编写处理不同路径(路由)和请求方法(GET、POST 等)的处理函数(Handler)。
- 了解 如何优雅地启动和关闭 HTTP 服务器。
核心概念
想象一下,一个 HTTP 服务器就像一个公司的“总机接线员”。它的职责是监听外界的电话呼叫(HTTP 请求),并根据呼叫方想要找的分机号(URL路径)或事务类型(HTTP方法),将电话转接给对应的客服专员(Handler 函数)去处理,最后把专员的答复(HTTP 响应)传达给呼叫方。
在 Go 语言的 net/http 包中,有几个关键的“角色”:
http.Handler接口:这是最核心的概念。任何实现了ServeHTTP(ResponseWriter, *Request)方法的类型,都是一个 Handler。它负责处理具体的请求逻辑。就像上面的“客服专员”。http.HandlerFunc:这是一个便捷的函数类型,它允许我们将一个普通的函数func(w http.ResponseWriter, r *http.Request)自动适配成http.Handler接口。这样我们就不必每次都去定义一个结构体了。http.ServeMux(多路复用器/路由器):可以把它看作“总机接线员的核心路由表”。它根据请求的 URL 模式,将请求分发到不同的 Handler。http.DefaultServeMux是net/http包默认提供并使用的路由器。http.Server:代表一个真实的服务器实例。它负责监听网络端口、接收连接,并将连接交给配置好的Handler(通常是ServeMux)去处理。它包含了超时、TLS 等服务器级配置。
基本工作流程:
- 创建一个
http.Server实例,并指定一个Handler(通常是http.ServeMux)。 - 在
ServeMux上注册不同的 URL 路径模式(Pattern)与对应的Handler或HandlerFunc的映射关系。 - 调用服务器的
ListenAndServe方法,开始监听指定端口并处理请求。
代码示例
示例 1:最简单的 HTTP 服务器
这个例子演示如何用最少的代码启动一个服务器,它对所有请求都返回 “Hello, World!”。
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
// 1. 定义一个处理函数 (HandlerFunc)
helloHandler := func(w http.ResponseWriter, r *http.Request) {
// 向响应体写入内容
fmt.Fprintln(w, "Hello, World!")
}
// 2. 将函数注册到默认的路由器(DefaultServeMux)的 “/” 路径上
http.HandleFunc("/", helloHandler)
// 3. 启动服务器,监听 8080 端口。nil 表示使用 DefaultServeMux
log.Println("Server starting on port :8080...")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("Server failed to start: ", err)
}
}
运行与测试:
- 保存为
main.go,运行go run main.go。 - 浏览器访问
http://localhost:8080,将看到 “Hello, World!”。 - 访问
http://localhost:8080/any/other/path,结果相同,因为路由是/。
示例 2:处理不同路径和方法的服务器
这个例子更贴近实际,演示如何处理不同的路由和区分请求方法。
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
// User 结构体用于演示 JSON 响应
type User struct {
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
// 使用自定义的 Mux 以便于理解,也可以继续用 http.DefaultServeMux
mux := http.NewServeMux()
// 路由 1: 处理 GET /greet?name=Go
mux.HandleFunc("/greet", func(w http.ResponseWriter, r *http.Request) {
// 只处理 GET 请求
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 从查询参数获取 name
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
fmt.Fprintf(w, "Hello, %s!", name)
})
// 路由 2: 处理 POST /user,接收 JSON 并返回 JSON
mux.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var user User
// 解析请求体中的 JSON 到 user 结构体
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// 设置响应头为 JSON
w.Header().Set("Content-Type", "application/json")
// 将修改后的 user 编码为 JSON 写入响应体
json.NewEncoder(w).Encode(user)
})
// 路由 3: 处理静态文件目录 (例如 ./static 文件夹)
// 使用 http.StripPrefix 去掉路径前缀 /static/
fileServer := http.FileServer(http.Dir("./static"))
mux.Handle("/static/", http.StripPrefix("/static/", fileServer))
// 创建服务器实例
server := &http.Server{
Addr: ":8081",
Handler: mux, // 使用我们自定义的路由表
}
log.Println("Server starting on port :8081...")
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
测试:
GET http://localhost:8081/greet?name=Developer-> 返回 “Hello, Developer!”POST http://localhost:8081/user(Body:{"name":"Alice","email":"[email protected]"}) -> 返回相同的 JSON。- 如果当前目录下存在
./static/index.html,则GET http://localhost:8081/static/index.html可以访问该文件。
实践练习
练习 1:基础温度转换服务 (易)
编写一个 HTTP 服务器,当用户访问 /c2f?celsius=100 时,返回摄氏度到华氏度的转换结果(F = C * 9/5 + 32)。例如,返回 "100°C = 212°F"。访问 /f2c?fahrenheit=212 时,进行反向转换。
预期输出:
请求: GET /c2f?celsius=100
响应: 100°C = 212.00°F
请求: GET /f2c?fahrenheit=212
响应: 212°F = 100.00°C
练习 2:用户信息接口 (中)
创建一个服务器,提供两个 API:
GET /user/{id}:其中{id}是一个数字。模拟从数据库查询,如果id为 1,返回 JSON{"id": 1, "name": "Bob"};如果id为 2,返回{"id": 2, "name": "Charlie"};其他 ID 返回 404。GET /users:返回所有模拟用户的 JSON 数组。
预期输出:
GET /user/1 -> 200 OK, {"id":1,"name":"Bob"}
GET /user/2 -> 200 OK, {"id":2,"name":"Charlie"}
GET /user/99 -> 404 Not Found
GET /users -> 200 OK, [{"id":1,"name":"Bob"},{"id":2,"name":"Charlie"}]
提示:从 r.URL.Path 中提取 id 参数,使用 strings.TrimPrefix 或 path.Match。
练习 3:请求日志中间件 (难)
概念:在将请求交给最终 Handler 处理之前或之后,执行一些通用操作,比如记录日志。这通常通过一个包装 Handler 的函数来实现。
编写一个函数 withLogging(handler http.Handler) http.Handler,它包装另一个 Handler,在每次请求处理时,向标准输出打印一行日志,格式为:[时间戳] 方法 URL -> 状态码。然后用这个包装函数来包装你的主 Handler。
预期输出(服务器控制台):
[2023-10-05 15:04:05] GET /greet?name=Go -> 200
[2023-10-05 15:04:10] POST /user -> 200
[2023-10-05 15:04:15] GET /notfound -> 404
提示:你需要创建一个自定义的 ResponseWriter 来捕获最终 Handler 设置的状态码。
常见错误
- 忘记启动服务器:写了 Handler 和路由,但最后一行忘了调用
ListenAndServe或server.ListenAndServe()。程序会立刻结束,不会监听端口。 - 端口已被占用:再次运行程序时报错
bind: address already in use。这是因为上一次的服务器进程还在运行。可以通过Ctrl+C或任务管理器结束它,或换一个端口(如:8082)。 - 混淆了
http.HandleFunc和mux.HandleFunc:如果你创建了自定义的mux := http.NewServeMux(),那么注册路由时应该用mux.HandleFunc,而不是http.HandleFunc(它操作的是全局的DefaultServeMux)。最终server.Handler也要设置为mux。 - 不理解请求与响应的单向性:在 Handler 内部,
r *http.Request是只读的输入,w http.ResponseWriter是只写的输出。你不能从w读取信息,也不能向r写入信息。 - 并发访问共享数据时未加锁:HTTP 服务器为每个请求启动一个 Goroutine。如果多个 Handler 都读写同一个全局变量(如一个计数器),就会产生数据竞争(Data Race)。解决方案是使用
sync.Mutex或通道(Channel)。
小结
本节课我们学习了构建 Go 语言 HTTP 服务器的基础知识:
- 核心组件:
Handler接口定义了处理逻辑,ServeMux负责路由分发,Server负责网络监听和生命周期管理。 - 搭建步骤:
- 编写处理函数(
func(w http.ResponseWriter, r *http.Request))。 - 将函数注册到路由器上,指定 URL 模式。
- 创建
Server实例,设置Handler和监听地址。 - 调用
ListenAndServe启动服务器。
- 编写处理函数(
- 实践关键:在 Handler 中通过
r.Method、r.URL、r.Header、r.Body获取请求信息;通过w.WriteHeader、fmt.Fprint(w, ...)、json.NewEncoder(w).Encode等方式构造响应。 - 注意事项:注意区分默认路由器和自定义路由器,理解并发安全的重要性,并为生产环境考虑优雅关闭等高级特性(将在后续课程介绍)。
掌握了这些基础,你就可以开始构建属于自己的 Web 服务或 API 了。下一课我们将深入探讨 Handler 和中间件(Middleware)的更多高级用法。