本日关键词(实战):net/http、ListenAndServe、HandleFunc、路由、Request、ResponseWriter、JSON、编码解码、curl 调试
本日语法/概念(实战):
| 语法/概念 | 实战用途 | 本日示例 |
|---|---|---|
http.ListenAndServe(addr, nil) |
启动 HTTP 服务,日常写 API 必用 | server/main.go |
http.HandleFunc(path, handler) |
注册路由,一个路径一个处理函数 | server/main.go |
*http.Request |
读 Method、URL、Path、Header、Body(如 JSON) | 解析路径参数、请求体 |
http.ResponseWriter |
写状态码、写响应头、写 Body、写 JSON | 返回 JSON 响应 |
r.Header.Get / w.Header().Set |
读请求头、写响应头(如 X-Trace-Id 串联调用与日志) | server/main.go setTraceID |
json.Marshal / json.Unmarshal |
结构体 ↔ JSON,前后端联调必备 | 返回/解析 JSON |
获取实战代码 :如需在本地跑通本文示例,请克隆仓库 WenSongWang/go-quickstart-7days,本文示例在 day2 目录,克隆后在项目根目录执行下文中的命令即可。
一、本篇目标
学完本文并跑通本目录下的示例,你将掌握:
| 模块 | 内容 |
|---|---|
| 服务 | 用标准库 net/http 起一个 HTTP 服务 |
| 路由 | http.HandleFunc 或自建 mux 匹配路径 |
| 请求 | Method、URL、Body、Header 的读取 |
| 响应 | 状态码、响应头(如 X-Trace-Id)、JSON、Write 的写法 |
二、前置要求
- 已完成 Day 1 (至少会
go run、能看懂基本语法)。 - 本系列命令均在项目根目录执行。
三、示例与知识点(先混个眼熟)
| 示例目录 | 主要知识点 |
|---|---|
server/ |
http.HandleFunc 注册路由、(ResponseWriter, *Request) 处理函数、读 Path/Method/Header、设 Content-Type 与 X-Trace-Id 、WriteHeader/Write、json.NewEncoder(w).Encode 写 JSON、路径参数用 TrimPrefix+Atoi 解析 |
四、核心概念与最小示例(不看代码也能懂)
下面用极短说明 + 一句话把写 HTTP 接口时最容易懵的点说清。
Handler 的签名为什么是 (w http.ResponseWriter, r *http.Request)?
标准库规定:你注册的处理函数必须长这样,第一个参数写响应,第二个参数读请求 。w 用来设置状态码、响应头、响应体;r 用来读 Method、URL、Header、Body。名字可以改,类型不能改。
为什么结构体字段要写 `json:"id"`?
Go 里结构体字段默认转成 JSON 时用的是首字母大写的字段名 (如 ID),但前端通常习惯小写(id)。用 struct tag `json:"id"` 告诉编码器:转成 JSON 时用 "id" 这个键。这样 User{ID:1, Name:"张三"} 会变成 {"id":1,"name":"张三"},前后端联调更顺手。
为什么要在 http.Error 之前先 Set Content-Type?
http.Error 内部会调用 WriteHeader,而 响应头必须在第一次 WriteHeader 之前全部设好 ,之后就不能再改。所以若想返回 JSON 格式的错误体,必须先 w.Header().Set("Content-Type", "application/json"),再调用 http.Error,否则浏览器可能按纯文本解析。
Marshal / Encode 一句话
- json.Marshal :把结构体转成
[]byte,自己再w.Write(data);适合要改一改再写出的场景。 - json.NewEncoder(w).Encode(v) :把结构体直接编码并写入
w,一步到位,写 API 响应时常用。
线上项目更多用哪个:HandleFunc 还是 mux?为什么?
线上项目更多用「第三方路由库」(如 chi、gin、echo)或「自建 mux」 ,而不是只用 http.HandleFunc + 默认 mux。原因主要有:
| 需求 | 说明 |
|---|---|
| 路径参数 | 如 /api/users/:id,标准库要自己 TrimPrefix + 解析,第三方库直接提供 Param(r, "id")。 |
| 中间件链 | 日志、鉴权、超时等需要一层层包装 Handler,第三方库有现成的 Use/Group 等模式(Day 5 会学)。 |
| 路由分组与前缀 | 大量接口时按模块分组、统一加前缀更清晰,标准库要自己拼路径。 |
本日用 HandleFunc 是为了先吃透标准库的 Request/Response 模型,后面再用框架会更容易理解它在帮你封装什么。
本系列约定 :7 天全程使用标准库 net/http ,不在 Day 2 也不在后续某天引入 chi/gin 等第三方路由框架。后面 Day 5、Day 7 会循序渐进地用 http.NewServeMux + 自建中间件(日志、鉴权),仍是标准库写法,便于理解「框架在帮你封装什么」后再按需选型。
和实战上线项目的区别 :实战上线项目里很多会用框架(chi、gin、echo 等)或公司自研路由层,因为路径参数、中间件、分组、性能与可维护性更省事。本系列故意不用框架,是为了先打好标准库基础,将来上手框架或做技术选型时心里有数;真正做生产项目时,可以按团队规范选用框架或继续用标准库(小服务、内部工具用标准库也常见)。
五、Day 2 示例代码全文与逐段解读
下面把 day2/server/main.go 的完整代码贴出,并分段做简短解读。读者无需打开项目,按顺序看下去即可把 Day 2 学完。
完整代码(即 day2/server/main.go,含注释)
go
// Day2 示例:标准库 HTTP 服务 + 简单路由 + JSON + trace_id
package main
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"strconv"
"strings"
)
// setTraceID 从请求头读 X-Trace-Id,没有则生成,并写回响应头(便于调用方与日志串联)
func setTraceID(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Trace-Id")
if id == "" {
b := make([]byte, 8)
rand.Read(b)
id = hex.EncodeToString(b)
}
w.Header().Set("X-Trace-Id", id)
}
// User 返回给前端的结构体
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
// handleHello 处理 /hello
func handleHello(w http.ResponseWriter, r *http.Request) {
setTraceID(w, r)
if r.URL.Path != "/hello" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, Go HTTP!"))
}
// handleGetUser 处理 /api/users/1 这种路径
func handleGetUser(w http.ResponseWriter, r *http.Request) {
setTraceID(w, r)
if r.Method != http.MethodGet {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
// 从 /api/users/1 里取出 "1",再转成数字
path := strings.TrimPrefix(r.URL.Path, "/api/users/")
id, err := strconv.Atoi(path)
if err != nil || id <= 0 {
w.Header().Set("Content-Type", "application/json") // 须在 WriteHeader 前设置头
http.Error(w, `{"error":"invalid id"}`, http.StatusBadRequest)
return
}
user := User{ID: id, Name: "用户" + strconv.Itoa(id)}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(user) // 把结构体编码成 JSON 写到 w
}
func main() {
// 注册路径与处理函数的对应关系:访问 /hello 调 handleHello,访问 /api/users/xxx 调 handleGetUser
http.HandleFunc("/hello", handleHello)
http.HandleFunc("/api/users/", handleGetUser)
// 监听 8080 端口,nil 表示用默认的路由(就是上面 HandleFunc 注册的)
http.ListenAndServe(":8080", nil)
}
逐段解读
-
setTraceID :从请求头读
X-Trace-Id,没有则用crypto/rand生成一个并写回响应头。这样调用方(或网关)可以把同一次请求的 trace_id 串联起来,日志里也方便按 trace_id 查一整条链路;实战里通常会在中间件里统一做(Day 5/7 会见到)。 -
User 与 json tag :
User和 Day 1 的 basics 里结构体概念一致,这里多了 `json:"id"`、`json:"name"`,表示序列化成 JSON 时键名为id、name,方便前端或 curl 直接使用。 -
handleHello :先调用
setTraceID(w, r)再处理业务,保证响应头里都带 trace_id。再判断r.URL.Path != "/hello"则返回 404;否则设置Content-Type为纯文本、状态码 200,再Write一段字节。顺序不能反 :先Header().Set,再WriteHeader,再Write。 -
handleGetUser :同样先
setTraceID(w, r),再只接受 GET;用strings.TrimPrefix(r.URL.Path, "/api/users/")得到路径后缀(如"1"),再用strconv.Atoi转成数字。若转换失败或 id≤0,先设 Content-Type 再http.Error返回 400 和 JSON 错误体;成功则构造User,设 JSON 头、200,用json.NewEncoder(w).Encode(user)写回 JSON。易踩坑 :若先调用http.Error再Set("Content-Type", "application/json"),响应头已发出无法修改,浏览器会按纯文本解析;解法即本段写法:先设头再写 body。 -
main :
HandleFunc把路径和函数绑在一起:/hello→ handleHello,/api/users/注意带末尾斜杠,这样/api/users/1会匹配到 handleGetUser。易踩坑 :误以为注册/api/users/只能匹配该精确路径;解法 :标准库规定末尾有/为前缀匹配,无/为精确匹配,所以/api/users/1会进同一 handler。ListenAndServe(":8080", nil)表示监听 8080,nil表示使用默认的 DefaultServeMux(即刚才注册的路由)。
六、运行当天代码
在项目根目录执行:
bash
go run ./day2/server
启动后:
- 浏览器或 curl 访问:
http://localhost:8080/hello - 访问:
http://localhost:8080/api/users/1看 JSON 响应
示例里会演示:注册路由、读请求、写 JSON 等,可直接对照源码学习。
七、学习建议
- 先跑起来 :确保 8080 端口没被占用,跑通后再看
day2/server下的main.go。 - 动手改 :改路径(如
/hello→/hi)、改返回的 JSON 字段,再请求一次观察变化。 - 理解「一个 Handler 对应一个路径」和「如何从
*http.Request里拿参数、Body」。
八、小结
Day 2 用标准库就能写出可用的 HTTP 服务,不依赖框架。后面 Day 3 做配置、Day 5 加中间件、Day 7 做完整 API,都会基于同样的 net/http 模型。