41·网络编程进阶

HTTP 服务器基础

httpserverhandlerlisten

第 41 课 - HTTP 服务器基础

所属模块: 网络编程
难度: Intermediate
标签: http, server, handler, listen
上一课: HTTP 客户端
下一课: Handler 与中间件


学习目标

完成本课学习后,你将能够:

  1. 理解 Go 语言中 HTTP 服务器(Server)的核心组件及其作用。
  2. 掌握 使用 net/http 标准库搭建一个基础 HTTP 服务器的完整步骤。
  3. 能够 为服务器编写处理不同路径(路由)和请求方法(GET、POST 等)的处理函数(Handler)。
  4. 了解 如何优雅地启动和关闭 HTTP 服务器。

核心概念

想象一下,一个 HTTP 服务器就像一个公司的“总机接线员”。它的职责是监听外界的电话呼叫(HTTP 请求),并根据呼叫方想要找的分机号(URL路径)或事务类型(HTTP方法),将电话转接给对应的客服专员(Handler 函数)去处理,最后把专员的答复(HTTP 响应)传达给呼叫方。

在 Go 语言的 net/http 包中,有几个关键的“角色”:

  1. http.Handler 接口:这是最核心的概念。任何实现了 ServeHTTP(ResponseWriter, *Request) 方法的类型,都是一个 Handler。它负责处理具体的请求逻辑。就像上面的“客服专员”。
  2. http.HandlerFunc:这是一个便捷的函数类型,它允许我们将一个普通的函数 func(w http.ResponseWriter, r *http.Request) 自动适配成 http.Handler 接口。这样我们就不必每次都去定义一个结构体了。
  3. http.ServeMux(多路复用器/路由器):可以把它看作“总机接线员的核心路由表”。它根据请求的 URL 模式,将请求分发到不同的 Handler。http.DefaultServeMuxnet/http 包默认提供并使用的路由器。
  4. http.Server:代表一个真实的服务器实例。它负责监听网络端口、接收连接,并将连接交给配置好的 Handler(通常是 ServeMux)去处理。它包含了超时、TLS 等服务器级配置。

基本工作流程

  1. 创建一个 http.Server 实例,并指定一个 Handler(通常是 http.ServeMux)。
  2. ServeMux 上注册不同的 URL 路径模式(Pattern)与对应的 HandlerHandlerFunc 的映射关系。
  3. 调用服务器的 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)
	}
}

运行与测试

  1. 保存为 main.go,运行 go run main.go
  2. 浏览器访问 http://localhost:8080,将看到 “Hello, World!”。
  3. 访问 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)
	}
}

测试

  1. GET http://localhost:8081/greet?name=Developer -> 返回 “Hello, Developer!”
  2. POST http://localhost:8081/user (Body: {"name":"Alice","email":"[email protected]"}) -> 返回相同的 JSON。
  3. 如果当前目录下存在 ./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:

  1. GET /user/{id}:其中 {id} 是一个数字。模拟从数据库查询,如果 id 为 1,返回 JSON {"id": 1, "name": "Bob"};如果 id 为 2,返回 {"id": 2, "name": "Charlie"};其他 ID 返回 404。
  2. 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.TrimPrefixpath.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 设置的状态码。


常见错误

  1. 忘记启动服务器:写了 Handler 和路由,但最后一行忘了调用 ListenAndServeserver.ListenAndServe()。程序会立刻结束,不会监听端口。
  2. 端口已被占用:再次运行程序时报错 bind: address already in use。这是因为上一次的服务器进程还在运行。可以通过 Ctrl+C 或任务管理器结束它,或换一个端口(如 :8082)。
  3. 混淆了 http.HandleFuncmux.HandleFunc:如果你创建了自定义的 mux := http.NewServeMux(),那么注册路由时应该用 mux.HandleFunc,而不是 http.HandleFunc(它操作的是全局的 DefaultServeMux)。最终 server.Handler 也要设置为 mux
  4. 不理解请求与响应的单向性:在 Handler 内部,r *http.Request 是只读的输入,w http.ResponseWriter 是只写的输出。你不能从 w 读取信息,也不能向 r 写入信息。
  5. 并发访问共享数据时未加锁:HTTP 服务器为每个请求启动一个 Goroutine。如果多个 Handler 都读写同一个全局变量(如一个计数器),就会产生数据竞争(Data Race)。解决方案是使用 sync.Mutex 或通道(Channel)。

小结

本节课我们学习了构建 Go 语言 HTTP 服务器的基础知识:

  • 核心组件Handler 接口定义了处理逻辑,ServeMux 负责路由分发,Server 负责网络监听和生命周期管理。
  • 搭建步骤
    1. 编写处理函数(func(w http.ResponseWriter, r *http.Request))。
    2. 将函数注册到路由器上,指定 URL 模式。
    3. 创建 Server 实例,设置 Handler 和监听地址。
    4. 调用 ListenAndServe 启动服务器。
  • 实践关键:在 Handler 中通过 r.Methodr.URLr.Headerr.Body 获取请求信息;通过 w.WriteHeaderfmt.Fprint(w, ...)json.NewEncoder(w).Encode 等方式构造响应。
  • 注意事项:注意区分默认路由器和自定义路由器,理解并发安全的重要性,并为生产环境考虑优雅关闭等高级特性(将在后续课程介绍)。

掌握了这些基础,你就可以开始构建属于自己的 Web 服务或 API 了。下一课我们将深入探讨 Handler 和中间件(Middleware)的更多高级用法。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「Handler 与中间件」 以巩固所学知识。