Learn HTTP Servers in Go(Boot.dev)
Boot.dev 的 Learn HTTP Servers in Go 课程框架笔记——用 net/http 搭建服务端、路由、中间件与可测试服务,依赖 HTTP 协议与客户端前置。
[!info] related notes
- 所属路线:Go Web 与后端 · Go 学习路线
- 来源平台:Boot.dev
- 依赖前置:boot-dev-learn-http-protocol-in-go · boot-dev-learn-http-clients-in-go · net/http
- 相关概念:HTTP 中间件 · 标准库构建 HTTP 服务 · 路由模式
Learn HTTP Servers in Go:课程知识整理
[!info] 课程概况 截至 2026 年 8 月 3 日,这门课程公开页面显示约 24 小时、51 节课、9 个章节。课程不依赖 Web 框架,主要使用 Go 标准库
net/http,逐步完成一个类似 Twitter 的微型社交平台后端 Chirpy。
一、课程最终要完成什么
整个课程围绕一条完整的后端请求链展开:
客户端
↓ HTTP 请求
ServeMux 路由
↓
中间件 Middleware
↓
Handler 处理器
↓
JSON 解析与参数校验
↓
身份认证与权限检查
↓
PostgreSQL 数据库
↓
JSON HTTP 响应
最终项目大致包含:
GET /healthz
GET /admin/metrics
POST /admin/reset
POST /api/users
PUT /api/users
POST /api/login
POST /api/chirps
GET /api/chirps
GET /api/chirps/{chirpID}
DELETE /api/chirps/{chirpID}
POST /api/refresh
POST /api/revoke
POST /api/polka/webhooks
同时实现:
- JSON REST API
- PostgreSQL 数据持久化
- 数据库迁移
- 类型安全 SQL
- 密码哈希
- JWT Access Token
- Refresh Token
- 用户资源权限检查
- Webhook
- API Key
- 查询参数与排序
- API 文档和 README
以上为 2026-08-03 课程记录的目标与路由示意,不代表当前课程版本或已完成的个人项目。后文代码包含独立程序与局部片段,后者需要对应 import、依赖和课程生成的 database 包。
二、课程目录(2026-08-03 记录)
Chapter 1:Servers
共 10 节:
- Welcome to Learn HTTP Servers
- Goroutines in Servers
- Project Setup
- Server
- Fileservers
- Fileserver Quiz
- Serving Images
- Workflow Tips
- Custom Handlers
- Handler Review
主要讲解服务器、并发请求、http.Server、静态文件服务和 Handler 模型。
Chapter 2:Routing
共 4 节:
- Middleware
- Stateful Handlers
- Routing
- Patterns
主要讲中间件、带状态的 Handler、HTTP 方法路由和 Go 1.22 的路由模式。
Chapter 3:Architecture
共 4 节:
- Monoliths and Decoupling
- Which Is Better?
- Admin Namespace
- Deployment Options
主要讲单体架构、前后端分离、接口命名空间和部署方式。
Chapter 4:JSON
共 4 节:
- HTTP Clients
- JSON
- JSON Review
- The Profane
主要讲使用 HTTP 客户端调试、解析请求 JSON、返回 JSON、输入校验和敏感词过滤。
Chapter 5:Storage
共 10 节:
- Storage
- Goose Migrations
- SQLC
- Database Review
- Context
- Create User
- Create Chirp
- Collections and Singletons
- Get All Chirps
- Get Chirp
主要讲 PostgreSQL、数据库迁移、SQLC、context.Context、表关系和 REST 资源设计。
Chapter 6:Authentication
共 9 节:
- Authentication With Passwords
- Password Review
- Types of Authentication
- JWTs
- Authentication With JWTs
- JWT Review
- Revoking JWTs
- Refresh Tokens
- Cookies
主要讲密码哈希、登录、JWT、Bearer Token、Access Token、Refresh Token 和 Cookie。
Chapter 7:Authorization
共 3 节:
- Authorization
- Authentication vs. Authorization
- Delete Chirp
主要讲用户身份和用户权限的区别,以及如何限制用户只能操作自己的资源。
Chapter 8:Webhooks
共 3 节:
- Webhooks
- Webhooks Review
- API Keys
主要讲第三方系统回调、幂等性、Webhook 重试和 API Key 验证。
Chapter 9:Documentation
共 4 节:
- Documentation
- Documentation
- Sorting Chirps
- Adding a README
主要讲 Markdown API 文档、查询参数、排序和项目 README。
三、核心知识点详解
1. Go HTTP 服务器的基本结构
一个最基础的 Go HTTP 服务器由三部分组成:
package main
import (
"log"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
server := &http.Server{
Addr: "127.0.x.x:8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
log.Fatal(server.ListenAndServe())
}
其中:
http.Server
├── Addr:监听地址
└── Handler:收到请求后交给谁处理
ListenAndServe() 会:
- 监听 TCP 端口。
- 接收 HTTP 请求。
- 将请求交给 Handler。
- 为请求执行相应的处理逻辑。
课程使用 http.ServeMux,不依赖 Gin、Echo 或 Fiber。此最小程序尚未注册路由,因此请求返回 404;监听回环地址仅供本机练习。上线需根据请求体、流式响应和代理行为配置超时、资源限制与优雅关闭,不能把这个骨架当完整部署配置。
2. Go 为什么适合写服务器
Go 的 HTTP 服务器会并发处理请求。可以从概念上理解为:
for 每个请求 {
go handler(request)
}
以上是伪代码,不是可编译的 Go。真实调度由 net/http 管理:HTTP/1 通常按连接处理请求,HTTP/2 可并发处理多个流;Handler 可以同时运行,不能假设共享状态串行访问。goroutine 轻量不等于并发没有上限。
与 Node.js 相比:
Node.js
└── 单线程事件循环
└── 非常适合 I/O 密集任务
Go
└── goroutine 并发模型
├── 适合 I/O 密集任务
└── 也更容易利用多核处理 CPU 密集任务
goroutine 比操作系统线程轻量,非常适合大量并发连接。
3. Handler、HandlerFunc 与 ResponseWriter
Handler 接口
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
任何实现了 ServeHTTP 方法的类型,都可以成为 HTTP Handler。
type myHandler struct{}
func (h myHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
w.Write([]byte("hello"))
}
HandlerFunc
普通函数通常写成:
func handlerHealth(
w http.ResponseWriter,
r *http.Request,
) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
它的函数签名符合:
type HandlerFunc func(
ResponseWriter,
*Request,
)
两个参数的含义
r *http.Request
代表客户端发来的请求,包括:
- HTTP Method
- URL
- Path
- Query Parameters
- Headers
- Body
w http.ResponseWriter
代表即将发送给客户端的响应,可以设置:
- 响应头
- 状态码
- 响应体
Go Handler 不直接 return Response,而是向 ResponseWriter 中写入响应。
4. 状态码和响应顺序
正确顺序是:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
w.Write(data)
必须先设置 Header,再调用 WriteHeader()。
如果直接调用:
w.Write(data)
在尚未发送最终状态码时,第一次 Write 会隐式发送:
200 OK
常见状态码:
200 OK 请求成功
201 Created 成功创建资源
204 No Content 成功,但没有响应体
400 Bad Request 请求格式或参数错误
401 Unauthorized 未认证或认证凭证无效
403 Forbidden 已认证,但没有权限
404 Not Found 资源不存在
405 Method Not Allowed
500 Internal Server Error
5. 静态文件服务器
Go 可以直接将一个目录作为静态网站:
fileServer := http.FileServer(
http.Dir("./app"),
)
mux.Handle(
"/app/",
http.StripPrefix("/app", fileServer),
)
例如:
./app/index.html
./app/assets/logo.png
可以通过:
http://localhost:8080/app/
http://localhost:8080/app/assets/logo.png
进行访问。
这里涉及两个概念:
FileServer
负责从文件系统读取文件。
StripPrefix
负责在访问文件系统前去除 URL 前缀。http.Dir 不会自动隐藏点文件,也可能跟随符号链接;没有 index.html 时可能列目录。只暴露明确的静态资源目录,不要把包含配置或源码的项目根目录挂出去。
6. 路由与 ServeMux Pattern
Go 1.22 起,标准库支持方法级路由:
mux.HandleFunc(
"GET /healthz",
handlerHealth,
)
mux.HandleFunc(
"POST /api/users",
handlerCreateUser,
)
mux.HandleFunc(
"DELETE /api/chirps/{chirpID}",
handlerDeleteChirp,
)
路由 Pattern 的基本格式是:
[METHOD ][HOST]/[PATH]
例如:
GET /articles
POST /articles
DELETE /articles/{articleID}
固定路径
"GET /about"
路径只匹配 /about,不匹配 /about/ 或子路径;GET 模式也匹配 HEAD。
子树路径
"/images/"
可以匹配:
/images/
/images/logo.png
/images/icons/user.svg
路由优先级
优先选择匹配请求集合更窄的模式,不是比较字符串长度。不可比较且有重叠的模式通常会在注册时 panic(带 host 的兼容规则另论)。末尾 / 表示子树,/images/{$} 可精确限制到目录根;缺少目录末尾斜杠时还可能重定向。
路径参数
chirpID := r.PathValue("chirpID")
对应:
"GET /api/chirps/{chirpID}"
Go 的标准库已经可以完成大部分基础 REST API 路由,不一定必须引入第三方 Router。
7. Middleware 中间件
中间件是对 Handler 的包装:
func middleware(next http.Handler) http.Handler {
return http.HandlerFunc(
func(
w http.ResponseWriter,
r *http.Request,
) {
// 请求前执行
next.ServeHTTP(w, r)
// 请求后执行
},
)
}
常见用途:
日志记录
请求计数
身份认证
跨域处理
耗时统计
Panic 恢复
请求 ID
限流
例如访问计数:
func (cfg *apiConfig) middlewareMetrics(
next http.Handler,
) http.Handler {
return http.HandlerFunc(
func(
w http.ResponseWriter,
r *http.Request,
) {
cfg.fileserverHits.Add(1)
next.ServeHTTP(w, r)
},
)
}
此例在 apiConfig 中使用 sync/atomic 的 atomic.Int64;读取用 Load,重置用 Store(0)。实例使用后不要按值复制,方法通过指针接收者共享同一计数器。普通整数 ++ 在并发 Handler 中会产生数据竞争。
8. Stateful Handler:带依赖的处理器
真实 Handler 通常需要访问:
- 数据库
- JWT Secret
- API Key
- 配置
- 日志器
- 第三方服务
课程采用类似配置结构体的方式:
type apiConfig struct {
fileserverHits atomic.Int64 // 需 import "sync/atomic"
db *database.Queries
jwtSecret string
polkaKey string
platform string
}
然后把 Handler 写成方法:
func (cfg *apiConfig) handlerCreateUser(
w http.ResponseWriter,
r *http.Request,
) {
// 可以访问 cfg.db
}
注册时:
mux.HandleFunc(
"POST /api/users",
cfg.handlerCreateUser,
)
这本质上是一种简单的依赖注入:
main
↓ 创建依赖
apiConfig
↓ 将依赖提供给
Handlers
比在 Handler 内部到处使用全局变量更容易测试和维护。
9. 单体架构与前后端分离
单体应用
一个服务
├── HTML
├── CSS / JS
├── API
└── 数据库访问
优点:
- 开发和部署简单
- 初期成本低
- 一个程序即可运行
缺点:
- 前后端耦合更明显
- 后续独立扩容比较困难
前后端分离
浏览器
↓
前端应用
↓ HTTP JSON API
Go 后端
↓
数据库
优点:
- 前后端可以独立开发
- 可以独立部署和扩容
- 同一套 API 可以供 Web、App、桌面端使用
缺点:
- 部署更复杂
- 需要处理 CORS、认证和接口版本
本课程记录采用先从单体开始的学习路径,但在逻辑上保持前端和 API 解耦,为以后拆分留出空间。
10. API 命名空间
课程将不同接口分为:
/app/
静态前端。
/api/
提供给客户端使用的业务接口。
/admin/
管理员、监控和开发接口。
例如:
GET /admin/metrics
POST /admin/reset
要注意:
/admin/只是路径组织方式,本身不提供安全保护。
真正的安全仍然需要身份认证和权限验证。
11. JSON 请求解析
客户端发送:
{
"email": "lane@example.com",
"password": "correct horse battery staple"
}
Go 中定义参数结构:
type parameters struct {
Email string `json:"email"`
Password string `json:"password"`
}
解析请求体:
params := parameters{}
// 例子最多接收 1 MiB;实际限制按接口契约选择。
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(¶ms); err != nil {
var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) {
http.Error(w, "request too large", http.StatusRequestEntityTooLarge)
} else {
http.Error(w, "invalid JSON", http.StatusBadRequest)
}
return
}
// Decode 一次成功不等于请求只含一个 JSON 值。
if err := decoder.Decode(new(any)); err != io.EOF {
http.Error(w, "expected one JSON value", http.StatusBadRequest)
return
}
关键规则:
- 字段必须导出,也就是首字母大写。
json:"email"是结构体标签。- 类型不匹配通常返回错误;null、缺失字段、未知字段和重复键的处理另有规则,不能只靠 Decode 判断业务有效。
- 缺少字段时,该字段通常保持 Go 零值。
- 解析成功不代表业务参数一定合法。
因此还需要进一步验证:
if params.Email == "" {
// 参数缺失
}
12. JSON 响应
下面的帮助函数用于普通 JSON 响应与 204,不用于流式输出或 1xx/304 等无正文协议状态:
func respondWithJSON(w http.ResponseWriter, code int, payload any) {
if code == http.StatusNoContent {
w.WriteHeader(code)
return // 204 不写响应体
}
data, err := json.Marshal(payload)
if err != nil {
log.Printf("encode response: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
if _, err := w.Write(data); err != nil {
log.Printf("write response: %v", err)
// 状态/正文可能已发送,不能再补写一个 500 响应。
}
}
错误响应:
func respondWithError(
w http.ResponseWriter,
code int,
message string,
) {
type errorResponse struct {
Error string `json:"error"`
}
respondWithJSON(
w,
code,
errorResponse{Error: message},
)
}
调用:
respondWithError(
w,
http.StatusBadRequest,
"Chirp is too long",
)
返回:
{
"error": "Chirp is too long"
}
成功响应:
respondWithJSON(
w,
http.StatusOK,
struct {
Valid bool `json:"valid"`
}{
Valid: true,
},
)
13. 参数校验与业务规则
本课程记录中的 Chirp 最长 140 个字符。Go 的 len(string) 统计 UTF-8 字节,不是字符;若按 Unicode 码点计数可用 utf8.RuneCountInString,但它仍不等于用户感知的字形数量。应明确采用哪一种长度契约。
大致流程:
解析 JSON
↓
检查 body 是否存在
↓
检查长度是否超过限制
↓
清理不允许出现的单词
↓
返回清理后的内容
应区分:
语法错误
JSON 本身无法解析:
400 Bad Request
参数错误
JSON 可以解析,但字段不符合要求:
400 Bad Request
服务器错误
数据库或内部组件出错:
500 Internal Server Error
客户端错误和服务器错误不能全部返回 500。
14. 为什么需要数据库
内存中的变量:
users := []User{}
服务器重启后会全部消失。
直接写 JSON 文件虽然简单,但存在:
- 并发写入冲突
- 查询效率低
- 数据关系难维护
- 文件越来越大
- 需要自己处理事务和索引
课程使用 PostgreSQL,将数据持久化到磁盘,并由数据库处理并发、查询和数据一致性。
15. 数据库迁移 Goose
数据库结构会不断变化,例如:
第一次:创建 users 表
第二次:增加 hashed_password
第三次:创建 chirps 表
第四次:创建 refresh_tokens 表
不能每次手动修改生产数据库,因此需要 migration。
示例:
-- +goose Up
CREATE TABLE users (
id UUID PRIMARY KEY,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
email TEXT UNIQUE NOT NULL
);
-- +goose Down
DROP TABLE users;
Up 表示应用修改。
Down 表示执行反向迁移;本例 DROP TABLE 会删除数据,不能称为无损回滚。生产迁移还需备份、锁表评估及前后版本兼容方案。
迁移文件应该:
- 进入 Git
- 按顺序执行
- 尽量只负责一次结构变更
- 同时提供回滚方案
16. SQLC:类型安全 SQL
传统方式:
row := db.QueryRow(
"SELECT id, email FROM users WHERE id = $1",
id,
)
查询结果和 Go 类型之间容易发生不匹配。
SQLC 的工作方式:
编写 SQL
↓
运行 sqlc generate
↓
生成 Go 函数和结构体
例如 SQL:
-- name: GetUser :one
SELECT *
FROM users
WHERE id = $1;
可能生成:
func (q *Queries) GetUser(
ctx context.Context,
id uuid.UUID,
) (User, error)
优点:
- 保留原生 SQL
- 编译期类型检查更强
- 减少手写扫描代码
- 查询结果自动转成 Go 类型
课程使用 PostgreSQL、Goose 和 SQLC 组成数据访问层。
17. Context 的作用
数据库调用通常接收:
ctx context.Context
Handler 中可以使用:
user, err := cfg.db.GetUser(
r.Context(),
userID,
)
r.Context() 与当前 HTTP 请求绑定。
客户端连接关闭、HTTP/2 请求取消或 ServeHTTP 返回会取消请求 Context;数据库能否及时中止取决于驱动是否响应取消。Server 的读写超时不等于给所有业务操作自动设置 Context deadline;需要业务截止时间时显式派生 WithTimeout 并 defer cancel。
Context 适合传递:
- 取消信号
- Deadline
- Timeout
- Request-scoped 数据
不适合拿来传递普通业务参数或全局依赖。
18. REST:集合资源与单个资源
资源通常使用复数名词。
创建资源
POST /api/chirps
获取资源集合
GET /api/chirps
获取单个资源
GET /api/chirps/{chirpID}
删除单个资源
DELETE /api/chirps/{chirpID}
相比:
POST /createChirp
GET /getAllChirps
REST 更倾向于:
HTTP Method 表达动作
URL Path 表达资源
/api/chirps 是集合,/api/chirps/{id} 是某个具体资源。
19. 密码不能明文保存
错误设计:
users
├── email
└── password = "123456"
数据库泄漏后,攻击者可以直接看到所有密码。
正确设计:
用户密码
↓
Argon2id
↓
哈希值
↓
保存到数据库
此处演示 Argon2id 密码哈希。函数签名取决于具体库,下面明确采用 github.com/alexedwards/argon2id(课程实现可能使用不同封装):
hash, err := argon2id.CreateHash(password, argon2id.DefaultParams)
登录时:
match, err :=
argon2id.ComparePasswordAndHash(
password,
storedHash,
)
密码哈希必须:
- 是单向运算
- 自动加入随机盐
- 计算成本足够高
- 专门用于密码存储
不应直接使用:
MD5
SHA-1
SHA-256
它们计算太快,不适合直接进行密码存储。
20. 登录过程
登录接口:
POST /api/login
请求:
{
"email": "lane@example.com",
"password": "password"
}
服务端流程:
根据 email 查询用户
↓
获取 hashed_password
↓
校验密码
↓
创建 Access Token
↓
创建 Refresh Token
↓
返回 Token
出于安全考虑,下面两种情况通常返回相同错误:
邮箱不存在
密码错误
例如:
{
"error": "Incorrect email or password"
}
这样可以减少攻击者枚举注册邮箱的机会。
21. JWT 是什么
课程使用的是签名 JWT(JWS 紧凑形式),包含三个部分:
Header.Payload.Signature
例如:
xxxxx.yyyyy.zzzzz
Header
描述签名算法和 Token 类型。
Payload
包含 Claims,例如:
{
"sub": "user-id",
"iss": "chirpy",
"exp": 1750000000
}
常见 Claims:
sub:Subject,Token 对应的用户
iss:Issuer,Token 签发者
exp:Expiration,过期时间
iat:Issued At,签发时间
Signature
HMAC 算法使用共享 Secret;非对称签名算法使用私钥签名、公钥验证。验证方必须固定允许的算法与预期签发者,不能由未验证的 Header 决定信任哪个算法或密钥。
JWT 的重要特点:
已签名,但没有加密
用户不能在不破坏签名的情况下修改 Payload,但任何拿到 JWT 的人通常都能读取 Payload。
因此绝不能在 JWT 中存放:
- 用户密码
- 私钥
- 银行卡信息
- 其他敏感数据
22. Bearer Token
客户端通常通过 Header 发送 JWT:
Authorization: Bearer eyJhbGciOi...
服务端需要:
- 查找
AuthorizationHeader。 - 解析认证 scheme 为 Bearer(大小写不敏感),并要求非空令牌。
- 提取 Token。
- 验证签名。
- 验证所需的 exp、iss、aud 等声明和时间边界,不能仅解码。
- 从已验证声明读取用户 ID,再检查资源权限。
可以封装:
func GetBearerToken(headers http.Header) (string, error) {
values := headers.Values("Authorization")
if len(values) != 1 {
return "", errors.New("expected one authorization header")
}
parts := strings.Fields(values[0])
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
return "", errors.New("invalid bearer authorization")
}
return parts[1], nil // 仅解析传输格式,JWT 验证仍在后续完成。
}
23. Access Token 与 Refresh Token
Access Token
通常是 JWT:
无状态
生命周期短
不能轻易撤销
用于访问受保护资源
例如:
POST /api/chirps
PUT /api/users
DELETE /api/chirps/{id}
Refresh Token
通常是高强度随机字符串:
有状态
生命周期长
保存在数据库
可以撤销
只能用来换取 Access Token
工作流程:
登录
↓
Access Token + Refresh Token
↓
Access Token 过期
↓
POST /api/refresh
↓
获得新的 Access Token
退出登录或发现 Token 泄漏:
POST /api/revoke
数据库中的 Refresh Token 可以设置:
revoked_at = 当前时间
撤销 Refresh Token 通常只阻止后续换取令牌,已签发 Access Token 可能仍可用到过期。若需要立即失效,需额外的服务端会话状态或撤销检查。Refresh Token 应由密码学随机源生成,数据库可存其摘要;若采用轮换,消费旧令牌与写入新令牌需原子化,不能让并发刷新重复成功。
24. Authentication 与 Authorization
Authentication:认证
回答:
你是谁?
例如:
- 密码
- JWT
- Session
- API Key
- OIDC(OAuth 2.0 本身是授权框架,不单独定义用户登录身份)
- Magic Link
Authorization:授权
回答:
你能做什么?
例如:
用户已经登录
但只能删除自己发布的 Chirp
删除流程:
验证 Access Token
↓
获取当前 userID
↓
获取 chirp
↓
判断 chirp.userID == currentUserID
↓
允许或拒绝删除
认证成功不等于拥有所有权限。把资源归属条件放进 DELETE/UPDATE 的 WHERE 并核对受影响行数,可避免“先查权限、再无条件修改”的竞争窗口;不存在与无权限如何对外响应需保持一致。
25. Webhook
Webhook 是外部服务主动发送给服务器的 HTTP 请求。
例如:
用户在支付平台完成付款
↓
支付平台向 Chirpy 发送 Webhook
↓
Chirpy 将用户升级为付费用户
Webhook 和 WebSocket 不同:
Webhook
├── 普通 HTTP 请求
├── 外部服务主动调用
└── 事件投递可能重试;是否批量承载由提供方协议决定
WebSocket
├── 持久连接
├── 双向通信
└── 适合实时消息
Webhook 的关键问题
重试
外部服务没有收到成功响应时,通常会重新发送。
幂等性
重复处理同一个事件不能产生错误结果。
例如升级用户:
UPDATE users
SET is_chirpy_red = true
WHERE id = $1;
重复执行仍然是 true,因此相对容易保持幂等。
正确确认
按提供方协议,在业务处理或可靠持久化入队成功后才确认 2xx;不能先确认再把任务仅留在可能丢失的内存队列。重复或无需处理的事件也需按协议确认,避免无限重试。
一旦返回 2xx,外部服务通常会认为事件已经成功处理并停止重试。
26. API Key
如果 Webhook 没有认证,任何人都可以伪造请求。
API Key 模式:
Authorization: ApiKey THE_KEY
服务端:
读取 Header
↓
提取 API Key
↓
与环境变量中的 Key 比较
↓
匹配后处理 Webhook
密钥应该保存在:
.env
环境变量
Secret Manager
不应该:
硬编码进代码
提交到 Git
打印到日志
返回给普通客户端
课程的 API Key 验证只证明请求方持有共享密钥,不自动证明每个载荷新鲜、唯一或不可重放。真实支付服务应按其签名协议验证原始请求体、时间戳并去重;不能把教学中的共享 Key 当通用支付验签方案。
27. 查询参数和排序
URL:
GET /api/chirps?sort=desc
Go 中读取:
sortOrder :=
r.URL.Query().Get("sort")
课程支持:
sort=asc
sort=desc
没有提供时默认升序。
查询参数适合表示:
- 排序
- 筛选
- 分页
- 搜索
- 可选配置
不需要为每种排序创建一个新接口:
不推荐:
GET /api/chirps-oldest
GET /api/chirps-newest
推荐:
GET /api/chirps?sort=asc
GET /api/chirps?sort=desc
本课程记录使用 sort.Slice 在内存排序。大数据集通常需数据库 ORDER BY 与分页;排序字段必须来自允许列表,不能直接拼接任意查询参数。时间相同时应增加稳定的唯一键作为次排序字段。
28. API 文档应该写什么
至少应该说明:
接口路径
HTTP 方法
请求 Header
Path 参数
Query 参数
请求 JSON
成功响应
错误响应
状态码
认证方式
示例:
## Create Chirp
POST /api/chirps
### Authentication
Authorization: Bearer <access-token>
### Request
{
"body": "Hello world"
}
### Response
201 Created
{
"id": "...",
"body": "Hello world",
"user_id": "..."
}
课程比较了:
- 手写 Markdown
- Swagger
- Postman
- Godoc
- GraphQL 文档工具
对于小型学习项目,课程建议优先从 Markdown 和 README 开始。
四、这门课真正需要掌握的 15 个重点
完成课程后,至少要能独立解释:
http.Server、ServeMux和 Handler 的关系。- 为什么 Handler 接收
ResponseWriter而不是返回 Response。 - Go 如何并发处理 HTTP 请求。
- 如何使用方法和路径注册路由。
- Middleware 如何包装 Handler。
- 如何通过结构体给 Handler 注入数据库和配置。
- 如何解析和返回 JSON。
- 如何正确使用状态码。
- 为什么需要 PostgreSQL,而不是只把数据放在内存中。
- Goose migration 解决什么问题。
- SQLC 如何把 SQL 转成类型安全的 Go 代码。
- 密码为什么必须使用 Argon2id 等密码哈希算法。
- 本课签名 JWT 为什么不提供载荷加密,以及如何验证算法和声明。
- Access Token 和 Refresh Token 的区别。
- Webhook 为什么必须考虑认证、重试和幂等性。
五、课程主线总结
这门课最重要的不是记住某个函数,而是理解一个后端请求完整经历了什么:
1. 客户端发起请求
2. ServeMux 匹配路由
3. Middleware 进行公共处理
4. Handler 解析参数
5. 校验 JSON 和业务规则
6. 验证 JWT 或 API Key
7. 检查用户权限
8. 使用 SQLC 操作 PostgreSQL
9. 根据结果选择状态码
10. 返回 JSON
它实际上是一门:
Go 标准库 HTTP
+ REST API
+ PostgreSQL
+ 后端认证授权
+ Webhook
的综合项目课,而不仅仅是一门介绍 net/http 函数的课程。
六、仅看知识点会遗漏什么
这份笔记整理课程概念,但不能据阅读推算掌握比例;以下能力仍需实际编码验证:
- 路由 Pattern 写错时如何排查
- Header 和状态码顺序错误
- SQL migration 执行失败
- UUID 和数据库类型转换
- JWT 过期和签名错误
context.Context的实际传递- Handler 之间如何复用代码
- 如何组织
main.go、internal/auth、数据库包和 Handler 文件 - 如何使用
curl、Bruno 或 Postman 调试接口
因此最值得亲手完成的部分是:
创建 HTTP Server
→ JSON API
→ PostgreSQL + SQLC
→ Password Login
→ JWT + Refresh Token
→ Authorization
→ Webhook