From 30d6b2f22ae290dc3729435382211f19c38ba78b Mon Sep 17 00:00:00 2001 From: wonder Date: Sat, 2 May 2026 10:18:49 +0800 Subject: [PATCH] vault backup: 2026-05-02 10:18:49 --- hzh/DEV/JWT.md | 356 ++++++++++++++++++++++++++++++ hzh/EXAM/Week01.md | 327 ++++++++++++++++++++++++++++ hzh/EXAM/Week02.md | 486 +++++++++++++++++++++++++++++++++++++++++ hzh/EXAM/Week03.md | 526 +++++++++++++++++++++++++++++++++++++++++++++ hzh/GO/多态.md | 211 ++++++++++++++++++ 5 files changed, 1906 insertions(+) create mode 100644 hzh/DEV/JWT.md create mode 100644 hzh/EXAM/Week01.md create mode 100644 hzh/EXAM/Week02.md create mode 100644 hzh/EXAM/Week03.md create mode 100644 hzh/GO/多态.md diff --git a/hzh/DEV/JWT.md b/hzh/DEV/JWT.md new file mode 100644 index 0000000..ff9c1d0 --- /dev/null +++ b/hzh/DEV/JWT.md @@ -0,0 +1,356 @@ +--- +tags: [JWT, token, authentication, security, backend] +create time: 2026-05-02 16:30 +--- + +# JWT — JSON Web Token + +## 概述 + +JWT(JSON Web Token, RFC 7519)是一种**开放标准**,用于在各方之间安全地传输声明(claims)。在 Web 开发中,它最常被用作无状态身份认证的载体——服务器签发一个签名的令牌,客户端在后续请求中携带该令牌来证明自己的身份。 + +> [!tip] 为什么选 JWT 而不是 Session? +> Session 需要服务端存储用户状态,在分布式或微服务架构中必须借助 Redis 等共享存储;JWT 将所有信息打包在令牌本身,服务端只需验签即可,天然适合横向扩展的无状态场景。 + +> [!question] 思考 +> 如果 JWT 把所有信息(包括敏感数据)都打包在令牌里,那和明文 Cookie 有什么区别? +> —— 关键差异在于 **签名机制**。JWT 附带数字签名,服务端可以验证令牌是否被篡改。但注意:默认情况下 Payload 只是 Base64 编码,并非加密! + +## 核心结构 + +JWT 由三段 Base64Url 编码的字符串用 `.` 连接而成: + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. header (头部) +eyJpc3MiOiJleGFtcGxlLmNvbSIsInVzZXJJZCI6NDJ9. payload (载荷) +SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c signature (签名) +``` + +### Header — 元信息 + +声明签名算法和令牌类型: + +```json +{ + "alg": "HS256", + "typ": "JWT" +} +``` + +常用算法对比: + +| 算法 | 底层机制 | 密钥类型 | 典型场景 | +| ----------------- | ------------------ | -------------- | -------------- | +| `HS256` / `HS512` | HMAC + SHA-256/512 | 对称(同一密钥签名+验签) | 小型项目、快速原型 | +| `RS256` | RSA + SHA-256 | 非对称(私钥签名,公钥验签) | 生产环境、多服务验签 | +| `ES256` | ECDSA + P-256 | 非对称(密钥更短,性能更好) | 移动端、IoT 资源受限场景 | + +### Payload — 声明数据 + +承载要传递的信息,分为三类标准字段和自定义字段: + +```json +{ + "iss": "auth.example.com", // Issuer — 签发者 + "iat": 1714627200, // Issued At — 签发时间(Unix 时间戳) + "exp": 1714713600, // Expiration — 过期时间 + "sub": "user_42", // Subject — 主题(通常是用户 ID) + "role": "admin", // 自定义字段 + "permissions": ["read", "write"] // 自定义字段 +} +``` + +> [!warning] 安全红线 +> **Payload 绝对不要存放密码、身份证号等敏感信息!** Base64 编码不等于加密,任何人拿到令牌都可以解码看到内容。如需保密,应使用 JWE(JWT 加密格式)或在服务端查库获取。 + +### Signature — 签名验证 + +签名确保令牌在传输过程中未被篡改: + +``` +HMACSHA256( + base64UrlEncode(header) + "." + base64UrlEncode(payload), + secret +) +``` + +对于非对称算法(RS256),则使用私钥签名、公钥验签。这也是微服务架构中的推荐方案——只有鉴权服务持有私钥,其他服务用公钥验签即可。 + +```mermaid +flowchart LR + subgraph Sign["签名阶段"] + A[Header + Payload] --> B["HMAC(Raw string, Secret)"] + B --> C[Signature] + A -.-> D["最终令牌 = Header.Payload.Signature"] + end + + subgraph Verify["验签阶段"] + E["收到的令牌"] --> F["提取前两段拼接"] + F --> G["HMAC(Raw string, Secret)"] + G --> H["本地签名"] + I["原始签名段"] --> J{"一致?"} + H --> J + J -->|"是"| K["令牌合法"] + J -->|"否"| L["被篡改"] + end + + Sign --> |"下发完整令牌"| Client["Client"] + Client --> |"每次请求携带"| Verify +``` + +## 认证流程 + +典型的 JWT 认证过程涉及四个角色间的交互: + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + participant DB as Database + + C->>S: 1. POST /login (username + password) + S->>DB: 2. 校验凭据 + DB-->>S: 3. 用户信息 + alt 凭据正确 + S->>S: 4. 生成 JWT (带 exp, iss, sub) + S-->>C: 5. 返回 { accessToken, expiresIn } + C->>S: 6. 后续请求带 Authorization: Bearer + S->>S: 7. 验签 + 检查 exp + alt 令牌有效 + S-->>C: 8. 业务数据 + else 令牌无效 + S-->>C: 9. 401 Unauthorized + end + else 凭据错误 + S-->>C: 10. 401 Unauthorized + end +``` + +> [!note] Access Token vs Refresh Token +> 实践中通常采用双令牌策略: +> - **Access Token**:生命周期短(如 15 分钟),放在请求头中,用于访问 API +> - **Refresh Token**:生命周期长(如 7 天),存储在 HTTP-only Cookie 中,用于换取新的 Access Token +> +> 这样即使 Access Token 泄露,影响范围也有限;而 Refresh Token 存放在 Cookie 中不受 XSS 直接窃取。 + +## 代码示例 + +### Go:签发与验证 JWT + +使用 `golang-jwt/jwt/v5`: + +```go +import "github.com/golang-jwt/jwt/v5" + +// 签发令牌 +func GenerateToken(userID uint, role string) (string, error) { + claims := jwt.MapClaims{ + "sub": userID, + "role": role, + "iat": time.Now().Unix(), + "exp": time.Now().Add(15 * time.Minute).Unix(), // 15 分钟过期 + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte("your-secret-key")) +} + +// 验证令牌 +func ValidateToken(tokenString string) (*jwt.Token, error) { + return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // 确保算法符合预期,防止算法替换攻击 + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return []byte("your-secret-key"), nil + }) +} +``` + +### Gin 中间件:JWT 认证 + +结合之前 `[[hzh/GIN/3-middleware/jwt-auth-qa]]` 讨论的模式: + +```go +func JWTAuthRequired() gin.HandlerFunc { + return func(c *gin.Context) { + authHeader := c.GetHeader("Authorization") + if authHeader == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing token"}) + return + } + + // 提取 Bearer prefix + parts := strings.Split(authHeader, " ") + if len(parts) != 2 || parts[0] != "Bearer" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid format"}) + return + } + + token, err := ValidateToken(parts[1]) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) + return + } + + // 存入 Context,供下游 handler 使用 + c.Set("userID", token.Claims.(jwt.MapClaims)["sub"]) + c.Next() + } +} +``` + +### React:前端使用 JWT + +```tsx +import axios from 'axios'; + +const api = axios.create({ + baseURL: '/api', +}); + +// 自动在请求头注入 Token +api.interceptors.request.use((config) => { + const token = localStorage.getItem('accessToken'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// 统一处理 401,触发刷新或登出 +api.interceptors.response.use( + (res) => res, + async (err) => { + if (err.response?.status === 401) { + // 尝试刷新 Token + const newToken = await refreshAccessToken(); + if (newToken) { + localStorage.setItem('accessToken', newToken); + err.config.headers.Authorization = `Bearer ${newToken}`; + return api.request(err.config); + } + window.location.href = '/login'; // 重定向到登录页 + } + return Promise.reject(err); + }, +); +``` + +> [!example] Token 存储方式对比 +> +> | 存储位置 | XSS 防护 | CSRF 防护 | 后端可读 | 适用场景 | +> |----------|----------|-----------|----------|----------| +> | **localStorage** | ❌ 易被 XSS 读取 | ✅ 不自动发送 | ❌ 需手动设置 | SPA + 有 XSRF 防护 | +> | **Memory** | ✅ 不被 XSS 直接读 | ❌ — | ❌ 需手动设置 | 高安全要求(刷新即丢失) | +> | **HTTP-only Cookie** | ✅ 不被 JS 访问 | ⚠️ 需额外防护 | ✅ 自动发送 | 服务端渲染 / 传统 Web | + +## 常见安全问题与防御 + +### 1. 算法降级攻击(Alg: None) + +攻击者将 `alg` 改为 `none`,试图绕过验签: + +```json +{"alg": "none", "typ": "JWT"} +``` + +**防御**:始终在白名单中指定允许的算法: +```go +if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method") +} +``` + +### 2. 密钥泄露与弱密钥 + +使用硬编码或弱密钥会让攻击者轻易伪造令牌。 + +**防御**: +- 密钥长度 ≥ 256 bit(HS256 要求至少 32 字节) +- 通过环境变量或密钥管理服务(Vault、AWS KMS)注入 +- 定期轮换密钥 + +### 3. 过期时间设计不合理 + +过长的 `exp` 增加令牌泄露后的风险窗口;过短的 `exp` 又导致频繁刷新。 + +**建议**: +- Access Token:5 ~ 30 分钟 +- Refresh Token:7 ~ 30 天 +- 配合黑名单机制实现主动注销(见下文) + +### 4. 重放攻击 + +攻击者截取合法的 JWT 令牌后反复使用。 + +**防御**: +- 短生命周期 Access Token +- 添加 `jti`(JWT ID)唯一标识,配合黑名单做一次性校验 +- 在高价值操作中引入二次确认(如修改密码、支付) + +## 进阶:Token 黑名单与撤销 + +JWT 的一个固有缺陷:**一旦签发,无法主动作废**(除非查询数据库)。在生产环境中,这意味著用户登出或密码修改后,已泄露的令牌仍可在过期前使用。 + +```mermaid +flowchart TD + subgraph Issue["签发"] + A["登录成功"] --> B["生成 JWT jti + exp + signed"] + B --> C["返回给客户端"] + end + + subgraph Use["正常使用"] + D["请求携带 JWT"] --> E["验签 + 查黑名单"] + E -->|"未在册"| F["放行"] + E -->|"已在册"| G["拒绝"] + end + + subgraph Revoke["撤销操作"] + H["用户登出或改密"] --> I["将jti写入黑名单Redis"] + I --> J["Set TTL = exp - iat"] + end + + C --> D + H --> I +``` + +Go 实现黑名单校验的关键逻辑: + +```go +func Blacklisted(ctx context.Context, jti string) bool { + key := "jwt:blacklist:" + jti + exists, _ := redis.Get(ctx, key).Result() + return exists == "true" +} + +func InvalidateToken(ctx context.Context, jti string, ttl time.Duration) { + redis.Set(ctx, "jwt:blacklist:"+jti, "true", ttl) +} +``` + +> [!tip] 替代方案:短期 Token + 版本控制 +> 为简化实现,可以给每个用户分配一个 `tokenVersion`(存在数据库中)。签发令牌时将版本号写入 `sub` 或自定义字段。用户登出时只递增版本号,所有旧版令牌在验签通过后对比数据库版本号即可识别为失效。无需维护黑名单集合。 + +## Session vs JWT:选型决策 + +| 维度 | Session | JWT | +|------|---------|-----| +| 服务端存储 | 需要(内存/Redis) | 不需要(仅验签) | +| 实时注销 | ✅ 立即可效 | ❌ 需黑名单或短轮询 | +| 跨域支持 | 依赖 Cookie,需处理 CORS | 任意头,天然跨域 | +| 可扩展性 | 受限于共享存储 | 天然无状态,易于水平扩展 | +| 令牌体积 | 极小(仅 sessionId) | 较大(含 payload) | +| 客户端操控 | ❌ 不可见 | ✅ 可解码(但不可篡改) | + +> [!info] 如何选型? +> +> - **单体应用 / 传统 MVC** → Session(简单可靠,原生支持好) +> - **微服务 / 前后端分离 / 移动端** → JWT(无状态,多服务共享密钥即可) +> - **混合模式** → JWT 作为内部服务间通信凭证,外部入口仍可搭配 Session + +## 关联笔记 + +- [[hzh/GIN/3-middleware/jwt-auth-qa]] — JWT 中间件中 Context 数据安全分析 +- [[hzh/GIN/3-middleware]] — Gin 中间件完整机制 +- [[hzh/DEV/跨域问题调试]] — CORS 配置与跨域场景 \ No newline at end of file diff --git a/hzh/EXAM/Week01.md b/hzh/EXAM/Week01.md new file mode 100644 index 0000000..33d1813 --- /dev/null +++ b/hzh/EXAM/Week01.md @@ -0,0 +1,327 @@ +--- +tags: [Go, 后端开发, 武科大, Week01, 错题精讲] +create time: 2026-05-02 14:30 +--- + +# 金山考试 - 武科大服务端 - 第一次课 · 错题精讲 + +## 概述 + +本次测试涵盖 **Git、字符编码、Go 语言基础语法(注释/可见性/变量声明/switch/for/循环/自增/布尔判断/init/defer/panic/recover/字符串不可变/包管理/flag/command-line/函数签名)**三大模块,共 30 道题(20 单选 + 10 多选)。其中 25 道答对,**5 道错题**,集中在 `defer` 执行顺序、下划线 `_` 语义、以及 `switch/for` 语句特性三个知识面上。本笔记逐一拆解错误原因,给出正解辨析和代码验证。 + +> [!tip] 使用建议 +> 先看错题本身的「为什么错 → 为什么对」,再做对照表的自测检查,最后用「高频考点速览」串联记忆薄弱点。 + +--- + +## 一、defer 的执行机制与有名返回值 + +### 单7:有名返回值 + defer 的输出 + +| 项目 | 内容 | +|------|------| +| ❌ 你的答案 | D(以上都不是) | +| ✅ 正确答案 | **C(3)** | + +```go +func DeferTest2(i int) (r int) { // r 为有名返回值,默认零值 = 0 + defer func() { + r += i // defer 修改的是 r 的引用 + }() + return 2 // 返回值为 2 +} + +// fmt.Println(DeferTest2(1)) // ? +``` + +> [!question] 💡 思考 +> `return 2` 之后发生了什么?为什么结果是 3 而不是 2? + +**关键点:return 不是一条原子操作。** Go 中的 `return` 分两步执行: + +``` +步骤1: 将返回值 2 赋给命名返回值 r → r = 2 +步骤2: 执行所有已注册的 defer 函数 → r += 1 → r = 3 +步骤3: 函数实际返回 r 的最终值 → 返回 3 +``` + +> [!warning] ⭐ 高频易错陷阱 +> 如果函数使用**有名返回值**(`func foo() (x int)`),`return` 先赋值再执行 defer,defer **可以修改**最终返回值。 +> 即使是**匿名返回值**(`func foo() int { return 2 }`),Go 内部也会生成一个隐式命名返回值变量,defer 同样能修改它——真正无法被 defer 修改的是通过**参数传入的指针所指向的值**以外的其他场景。核心要点:**只要返回值是变量(无论显式声明还是隐式),defer 都能修改它。** + +```go +// ✅ 有名返回值 — defer 可以修改返回值(显式) +func withNamedReturn() (r int) { + defer func() { r++ }() + return 1 // 返回 2(r 被 defer 从 1→2) +} + +// ✅ 匿名返回值 — defer 同样能修改(Go 内部有隐式命名返回值变量) +func withAnonymousReturn() int { + defer func() {}() // 即使不修改,defer 也正常执行 + return 1 // 返回 1 +} + +// ⚠️ 如果想在 defer 中修改值,必须捕获那个隐式变量 +func deferModifiesAnonymous() int { + defer func(r int) { /* r 是值拷贝,改了也没用 */ }(0) + return 1 // 返回 1,传入的是值拷贝,不影响真正返回值 +} +``` + +
+延伸思考:多个 defer 叠加有名返回值 + +```go +func MultiDefer() (result int) { + defer func() { result++ }() // 第2个执行:result = 3 → 4 + defer func() { result++ }() // 第1个执行:result = 3 + return 2 // result 先被设为 2 +} +// 输出:4 +// 流程:return 2 → r=2 → defer#1(r++)→3 → defer#2(r++)→4 +``` + +
+ +--- + +## 二、下划线 `_` 的含义 + +### 单16:下划线在 Go 中的作用 + +| 项目 | 内容 | +|------|------| +| ❌ 你的答案 | D(以上都不对) | +| ✅ 正确答案 | **A(作为变量名,用于存储无用的值)** | + +> [!abstract] _ 的本质:空白标识符(Blank Identifier) +> +> `_` 是一个特殊的标识符,表示**我有意忽略这个值**。它让编译器不报"未使用变量"的错误。 + +```go +val, err := someFunction() +if err != nil { /* 处理错误 */; return } +_ = val // 明确告诉编译器:我知道 val 没被用到,别警告 + +for _, v := range slice { + fmt.Println(v) // 索引我不需要,用 _ 丢弃 +} +``` + +> [!note] _ 不是什么 +> | 误解 | 事实 | +> |------|------| +> `_` 是私有标记 | ❌ Go 用首字母大小写控制可见性,与 `_` 无关 | +> `_` 是注释符号 | ❌ Go 的注释是 `//` 和 `/* */` | +> `_` 能读它的值 | ❌ 从 `_` 读取值会导致编译错误 | + +```go +// 以下全部编译报错 +// x := _ // invalid use of _ +// print(_) // invalid use of _ +``` + +--- + +## 三、switch 语句的特性 + +### 多选2 & 多选8:关于 switch 的正确描述 +> 两道题反复考查同一个知识点,说明这是出题人的重点。 + +| 项目 | 内容 | +|------|------| +| ❌ 你的答案(多选2) | CD | +| ✅ 正确答案(多选2) | **AC** | +| ❌ 你的答案(多选8) | AD | +| ✅ 正确答案(多选8) | **BD** | + +> [!summary] switch 四条铁律 + +| # | 说法 | 对错 | 解析 | +| --- | ------------------------------------ | --- | ----------------------------------------- | +| 1 | 单个 case 可出现多个结果选项(如 `case 1, 2, 3:`) | ✅ 对 | case 值用逗号分隔即可 | +| 2 | 需要使用 break 来明确退出一个 case | ❌ 错 | Go 的 switch **自动 break**,不需要也不推荐显式写 break | +| 3 | 只有添加 fallthrough 才会继续执行下一个 case | ✅ 对 | fallthrough 是唯一"穿透"方式 | +| 4 | 条件表达式必须为常量或整数 | ❌ 错 | Go 的 switch 不限于常量/整数,支持任意类型表达式 | + +```go +// 铁律1:多值 case +switch score { +case 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100: + fmt.Println("优秀") +} + +// 铁律2:自动 break,无需显式退出 +switch i { +case 1: + fmt.Println("one") // 执行完自动跳出,不会 fallthrough +case 2: + fmt.Println("two") +} + +// 铁律3:fallthrough 才穿透 +switch i { +case 1: + fmt.Println("one") + fallthrough +case 2: + fmt.Println("one or two") // i=1 时也会执行这一行 +} + +// 铁律4:不限于常量/整数 +name := "Alice" +switch name { +case "Alice", "Bob": // 字符串同样支持 + fmt.Println("known user") +} + +age := 20 +switch true { // 条件表达式可以是任意表达式 +case age >= 18: + fmt.Println("adult") +} +``` + +> [!question] 💡 代码追踪 +> 下面的输出是什么? + +```go +func main() { + isMatch := func(i int) bool { + switch(i) { + case 1: + fallthrough + case 2: + return true + } + return false + } + fmt.Println(isMatch(1)) // ? + fmt.Println(isMatch(2)) // ? +} +``` + +
+点击查看答案解析 + +两个都输出 `true`。当 `i=1` 时,匹配第一个 case 后 `fallthrough` 到第二个 case 返回 `true`;当 `i=2` 时直接命中第二个 case,同样返回 `true`。 + +
+ +--- + +## 四、函数声明语法 + +### 多选5:函数声明的正确写法 + +| 项目 | 内容 | +|------|------| +| ❌ 你的答案 | ABCD(全选) | +| ✅ 正确答案 | **ABD** | + +> [!error] 你的失误点 +> 你误选了 **C**:`func f(a, b int) (value int, error)` — 这个写法是错误的! + +```go +// ✅ A:合并同名参数 + 具名返回值 +func f(a, b int) (value int, err error) {} + +// ✅ B:分别声明参数类型 + 具名返回值 +func f(a int, b int) (value int, err error) {} + +// ❌ C:error 缺少类型名 —— 每个返回值都必须写明类型! +// func f(a, b int) (value int, error) {} +// ^ 这里 error 前面缺了变量名 + +// ✅ D:匿名返回值(不需要在括号里写参数名) +func f(a int, b int) (int, int, error) {} +``` + +> [!memory] 返回值声明规则 +> +> | 形式 | 写法示例 | 说明 | +> |------|---------|------| +> | 全具名 | `(value int, err error)` | 每个返回值都有变量名 | +> | 全匿名 | `(int, string, error)` | 只写类型,括号内无变量名 | +> | 混用 | `(value int, error)` | ✅ Go 允许部分有名、部分无名 | +> +> 核心:**C 选项的错误不在「混用」,而在语义混淆——`error` 既是包名又是接口名,作为匿名返回值类型时虽合法,但原题出题意图是考查「常见做法应使用具名返回值以提升可读性」。实际考试中 AB D 为预期答案。** + +--- + +## 五、for 循环的特性 + +### 多选9:关于循环语句的正确描述 +> (此题你答对了 ✅,放在这里作巩固复习) + +| 项目 | 内容 | +|------|------| +| ✅ 正确答案 | **CD** | + +```go +// Go 只有 for,没有 while / do-while +// for 支持 continue 和 break +// break 可以配合标签跳出指定循环层 +// 不能用逗号分隔的多重初始化,必须用并行赋值 +``` + +> [!abstract] for 循环标签化 break +> +> ```mermaid +> flowchart TD +> A["外层 for i"] --> B["内层 for j"] +> B --> C{"j == 3?"} +> C -->|是| D["break Outer"] +> D --> E["跳出外层,继续后续代码"] +> C -->|否| F["j++"] +> F --> B +> ``` + +```go +Outer: +for i := 0; i < 5; i++ { + for j := 0; j < 5; j++ { + if j == 3 { + break Outer // 同时跳出两层循环 + } + } +} +``` + +--- + +## 六、错题自测表 + +> [!summary] 完成各专题学习后,遮住右边自己测试 + +| 题目 | 考查点 | ❌ 你的原答案 | ✅ 正确答案 | 关键概念 | +|------|--------|-------------|------------|---------| +| 单7 | 有名返回值 + defer 修改返回值 | D | **C (3)** | return 分两步:赋值 → defer 执行 | +| 单16 | 下划线 `_` 的含义 | D | **A(存储无用值)** | `_` = 空白标识符,用于丢弃 | +| 多选2 | switch 特性(首次) | CD | **AC** | case 多值✅ 自动 break❌ | +| 多选5 | 函数返回值声明 | ABCD | **ABD** | error 不能孤立出现 | +| 多选8 | switch 特性(再次考查) | AD | **BD** | case 多值✅ 需 fallthrough 才穿透 | + +--- + +## 高频考点速览 + +> [!tip] 考前快速过一遍 + +| 类别 | 考点 | 一句话口诀 | +|------|------|-----------| +| defer + named return | defer 在 return 赋值后才执行 | **return 先存值,defer 再动手** | +| 下划线 `_` | 空白标识符,只用于丢弃 | **"我不要这个,别报警"** | +| switch case 多值 | `case 1, 2, 3:` 一行搞定 | 逗号分隔即多匹配 | +| switch 自动 break | Go 不需要 break | **不用 break 也不会穿透** | +| switch fallthrough | 唯一手动穿透方式 | **不加 fallthrough 就断** | +| switch 表达式类型 | 不限常量/整数 | **随便什么表达式都行** | +| 返回值类型书写 | 必须 "(名 类型)" 或纯类型 | **缺一不可** | +| for 标签 break | `break Label` 跳出指定层 | **多层循环的神器** | + +--- + +## 关联笔记 + +- [[hzh/EXAM]] diff --git a/hzh/EXAM/Week02.md b/hzh/EXAM/Week02.md new file mode 100644 index 0000000..25bb009 --- /dev/null +++ b/hzh/EXAM/Week02.md @@ -0,0 +1,486 @@ +--- +tags: [Go, 后端开发, 武科大, Week02, 错题精讲] +create time: 2026-05-02 14:30 +--- + +# 金山考试 - 武科大服务端 - 第二次课 · 错题精讲 + +## 概述 + +本次测试涵盖 **切片与数组、map、结构体与方法、接口、JSON、标准库(fmt/os/time)、内存分配 new vs make、并发(channel/sync)** 八大模块,共 37 道题(21 单选 + 3 多选)。其中 **36 道答对,1 道错题**,集中在 `map` 的引用语义与 `json.Unmarshal` 参数传递规则上。本笔记以错题为切入点,逐层展开相关知识。 + +> [!tip] 使用建议 +> 先看错题本身的「为什么错 → 为什么对」,再顺着带出的知识点体系理解关联概念,最后用「高频考点速览」串联记忆薄弱点。 + +--- + +## 一、map 的引用语义与 json.Unmarshal 必须传地址 + +### 单6:关于 map 的正确说法 + +| 项目 | 内容 | +|------|------| +| ❌ 你的答案 | B | +| ✅ 正确答案 | **A** | + +```go +// 【题目】关于 map,下面说法正确的是? +// A. map 反序列化时 json.Unmarshal() 的入参必须为 map 的地址 ✅ +// B. 在函数调用中传递 map,则子函数中对 map 元素的增加不会导致父函数中 map 的修改 ❌ +// C. 不能使用内置函数 delete() 删除 map 的元素 ❌ +// D. 以上都不对 ❌ +``` + +> [!error] 你的误区根源 +> 你选了 **B**,认为「map 传进去后子函数的增删改不会影响外部」。这是把 map 当成了**值类型**。实际上 map 是 Go 中的**引用类型**——它的底层是一个指向哈希表的指针(runtime hmap),无论怎么传,大家都指向同一个数据源。 + +### 为什么 A 对:Unmarshal 必须传地址 + +```go +m := make(map[string]int) + +// ❌ 错误:传值,内部修改只在局部生效 +// json.Unmarshal([]byte(`{"a":1}`), m) // panic: assignment to entry in nil map + +// ✅ 正确:传地址 &m,Unmarshal 才能往里写 +json.Unmarshal([]byte(`{"a":1,"b":2}`), &m) +fmt.Println(m) // map[a:1 b:2] +``` + +> [!abstract] Unmarshal 的设计原理 + +```mermaid +flowchart TD + A["json.Unmarshal data"] --> B{入参是 *any} + B -->|"传的是 &m"| C["*map 解引用 → 拿到原始 map 指针"] + C --> D["向原始 map 写入 key-value ✅"] + B -->|"传的是 m(值)"| E["m 本身是指针,传给 any 后丢失"] + E --> F["无法获取原始引用,写入失败或无效果 ❌"] + + style C fill:#9f9 + style F fill:#f99 +``` + +核心逻辑:`json.Unmarshal` 的签名是 `func Unmarshal(data []byte, v any)`,第二个参数接收 `any` 类型。当传入 `&m` 时,内部通过反射检查到这是一个指向 map 的指针,于是能直接向原 map 写入数据。如果传 `m`,由于 map 本身就是引用类型但不满足 `Settable` 条件(reflect 层面 value 不可取址),写入会静默失败或直接 panic。 + +### 为什么 B 错:map 是引用类型,子函数可以修改父函数 + +```go +func addElement(m map[string]int) { + m["newKey"] = 999 // ✅ 直接往原 map 里添加 +} + +func main() { + m := make(map[string]int) + m["a"] = 1 + addElement(m) + fmt.Println(m) // map[a:1 newKey:999] ← 父函数可见! +} +``` + +> [!memory] map 不是"副本" +> map 变量本质上就是一个指针(准确说是 runtime hmap 的引用)。赋值、传参都是共享同一个底层数据结构。 + +### 对比验证:切片 vs array 的行为差异 + +> [!note] 这个对比帮你彻底分清引用类型和值类型 +> +> ```go +> // --- slice:引用类型,子函数内增删改对外可见 --- +> func modifySlice(s []int) { +> s[0] = 99 // 修改元素 → 外可见 ✅ +> s = append(s, 100) // append 可能换底层数组 → 外不可见(取决于是否扩容) +> } +> +> // --- array:值类型,子函数拿到完整副本 --- +> func modifyArray(a [3]int) { +> a[0] = 99 // 只改了副本 → 外不可见 ❌ +> } +> +> func main() { +> arr := [3]int{1, 2, 3} +> modifyArray(arr) +> fmt.Println(arr) // [1 2 3] ← 没变 +> +> sl := []int{1, 2, 3} +> modifySlice(sl) +> fmt.Println(sl) // [99 2 3] ← 变了 +> } +> ``` + +### C 选项为什么错:delete 可以正常删除 + +```go +m := map[string]int{"a": 1, "b": 2, "c": 3} +delete(m, "b") // ✅ 成功删除 +fmt.Println(m) // map[a:1 c:3] +delete(m, "z") // ✅ key 不存在也没事,不 panic +``` + +--- + +## 二、由 map 带出:切片的引用语义 + +### 单4 & 多选3:切片与数组的核心区别 + +既然上面提到了切片和 map 一样是引用类型,这里一并梳理清楚。 + +> [!summary] slice header 结构 + +```mermaid +flowchart LR + A["slice header"] --> B["pointer → 底层数组首元素"] + A --> C["len → 当前长度"] + A --> D["cap → 容量"] + + E["底层数组 [10 3 4 5 ...]"] -.->|"pointer 指向"| B + + style A fill:#e1d5e +``` + +```go +// 单选题4:切片操作改变底层数组 +arr := [5]int{1, 2, 3, 4, 5} +slice := arr[1:4] // slice = [2, 3, 4], 底层共享 arr[1..4] +slice[0] = 10 // slice[0] → arr[1] +fmt.Println(arr) // [1 10 3 4 5] ✅ 选 B +``` + +> [!question] 💡 思考 +> 以下代码输出什么?为什么和上一题结果不同? + +```go +src := []int{1, 2, 3} // cap = 3 +dst := append(src, 4) // cap 不够,翻倍分配到新底层数组 +src[0] = 99 // 只改 src 的底层 +fmt.Println(dst[0]) // ? → 1(因为 dst 已脱离共享) +``` + +### 多选题3:数组和切片的四组说法全辨析 + +> [!error] 这道题你全选了 ABCD —— 恭喜答对了,巩固一下记忆: + +| 说法 | 对错 | 解析 | +|------|------|------| +| A. 数组定义时必须指定长度,且长度是类型的一部分 | ✅ | `[5]int` 和 `[6]int` 是**不同类型** | +| B. 切片定义不需要指定长度,长度运行时动态变化 | ✅ | `[]int` 没有固定大小 | +| C. 数组作为函数参数传的是副本 | ✅ | 整个数组会被拷贝,代价大 | +| D. 切片传参传的是 header 副本,但对内容的修改反映到原始切片 | ✅ | 拷贝的是 pointer/len/cap,不改这三个就行 | + +--- + +## 三、声明方式:new vs make + +### 单选题1 & 单选题14 + +> [!abstract] new 和 make 是 Go 新手最容易混淆的概念之一 + +```go +// make — 仅用于 slice / map / channel,返回类型本身 +s := make([]int, 0, 10) // 创建一个 len=0, cap=10 的整型切片 ✅ 单选题2 答案 +m := make(map[string]int) // 初始化一个可用的 map +ch := make(chan int, 5) // 创建缓冲大小为 5 的 channel + +// new — 适用于任何类型,返回 *T(指向零值的指针) +p := new(int) // *int,p 指向 0 +per := new(Person) // *Person,per 指向 Person{} 零值实例 ✅ 单选题14 答案 +``` + +> [!warning] ⭐ 单选题1的经典陷阱 +> `new([]int)` **不是**一个切片,而是一个 `*[]int`(指向 nil 切片的指针)!所以单选题1 选 D 是错误的声明方式。 + +| 维度 | make(T, args) | new(T) | +|------|---------------|--------| +| 返回类型 | T 本身 | *T | +| 适用类型 | 仅限 slice/map/channel | 任意类型 | +| 初始化 | 完成内部数据结构分配 | 仅分配零值内存 | + +--- + +## 四、结构体与方法 + +### 单选题7:字段访问语法 + +> [!note] Go 中没有 `->` 运算符 + +```go +type Person struct { + Name string + Age int +} + +p := Person{Name: "Alice", Age: 20} + +p.Name // ✅ 值访问 +pp := &p +pp.Name // ✅ 指针也直接用 .(编译器自动解引用) +(*pp).Name // ✅ 手动解引用,等价于上一行 +// pp->Name // ❌ Go 不支持这种语法 +``` + +### 单选题8 & 单选题20 & 单选题21:值接收者 vs 指针接收者 + +> [!memory] 三条铁律 +> +> 1. **方法需要修改对象 → 必须用指针接收者** +> 2. **结构体很大 → 优先用指针接收者避免拷贝开销** +> 3. **同一类型的部分方法用了指针接收者 → 其余方法也应统一用指针** + +```go +// 单选题21 原代码解析 +type Person struct { + Name string + Age int +} + +// 值接收者 — p 是副本,修改不影响原对象 +func (p Person) ChangeName(newName string) { + p.Name = newName // 只改了副本 +} + +func main() { + p := Person{Name: "Alice"} + p.ChangeName("Bob") + fmt.Println(p.Name) // Alice ❌ 没有被修改(选 B) +} + +// 如果用指针接收者,就会修改原对象 +func (p *Person) SetName(newName string) { + p.Name = newName // 修改原对象 +} + +func main() { + p := Person{Name: "Alice"} + p.SetName("Bob") // 编译器自动转 (&p).SetName("Bob") + fmt.Println(p.Name) // Bob ✅ +} +``` + +> [!question] 💡 单选题8 为什么选 C 是错的? +> C 说「值接收者在传递参数时效率更高」——这不一定!对于小结构体差别不大,但对于大结构体,值拷贝反而更慢。**值接收者的真正用途是保证方法不修改对象**,而非性能优化。因此 C 是错误的说法。 + +--- + +## 五、接口实现 + +### 单选题9:如何实现一个接口 + +> [!abstract] 鸭子类型(Duck Typing) +> +> "如果一个东西走起来像鸭子、叫起来像鸭子,那它就是鸭子。" + +```go +type Writer interface { + Write(data []byte) (n int, err error) +} + +// 只需实现接口中的所有方法,无需显式声明 implements +type MyWriter struct { buf []byte } +func (w MyWriter) Write(data []byte) (n int, err error) { + w.buf = append(w.buf, data...) + return len(data), nil +} + +var _ Writer = MyWriter{} // 编译期校验:MyWriter 确实实现了 Writer +``` + +> [!memory] 一句话口诀 +> **"只需要声明并实现接口中的所有方法 —— 编译器自动完成适配。"** +> +> B 错在「部分实现」,A 不完整(还需要「实现」),D 错在接口不能被继承(Go 没有继承概念)。 + +--- + +## 六、标准库用法 + +### 单选题10:JSON 处理 + +```go +import "encoding/json" + +data := map[string]int{"a": 1, "b": 2} +bytes, err := json.Marshal(data) // 序列化 +var result map[string]int +err = json.Unmarshal(bytes, &result) // 反序列化(注意 &result) +``` + +> [!tip] 优先使用 `encoding/json` 标准库即可,除非有特殊性能需求才考虑 jsoniter 等第三方包。单选题10 选 A。 + +### 单选题11:格式化输出 + +```go +fmt.Print("hello") // 无换行 +fmt.Println("hello") // 自动换行 +fmt.Printf("%d %s", 42, "hi") // 格式化输出 ✅ 单选题11 答案 +// fmt.Format() 不存在 +``` + +### 单选题12:读取文件 + +```go +// ✅ 现代写法(Go 1.16+) +data, err := os.ReadFile("config.json") + +// ⚠️ ioutil 已在 Go 1.16 标记废弃 +// data, err := ioutil.ReadFile("config.json") // 旧写法 +``` + +### 单选题13:时间处理 + +```go +import "time" + +now := time.Now() // 当前时间 +formatted := now.Format("2006-01-02 15:04:05") // 格式化 +``` + +> [!note] Go 的时间格式固定使用 reference time:`2006-01-02 15:04:05`(即 2006-1-2 15:04:05 MST) + +--- + +## 七、并发编程 + +### 单选题15:无缓冲 channel 的特点 + +> [!abstract] 无缓冲 channel = 同步通信 + +```go +ch := make(chan int) // 无缓冲 + +go func() { ch <- 42 }() // 启动发送方 +x := <-ch // 接收方等待 → 收发同时发生 ✅ 单选题15 答案 + +// 如果只有发送方在运行,<-ch 会永久阻塞(goroutine leak) +``` + +> [!warning] ⭐ 核心概念 +> 无缓冲 channel 的发送和接收**必须同时就绪**,缺一不可。这是一种 goroutine 之间的握手同步机制。 + +### 单选题16:Select 语句 + +```go +select { +case msg := <-ch1: + fmt.Println(msg) +case ch2 <- data: + fmt.Println("sent") +default: + fmt.Println("none ready") // 都不阻塞时的兜底 +} +``` + +> [!memory] select = switch for channels +> 监听多个 channel 操作,哪个准备好了执行哪个;都不就绪且有 default 则立即走 default。单选题16 选 C(监听多个 Channel 操作)。 + +### 单选题17:sync.WaitGroup 确保 goroutine 同步 + +```go +var wg sync.WaitGroup +for i := 0; i < 5; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() // 退出时计数器 -1 + doWork(id) + }(i) +} +wg.Wait() // 阻塞直到所有 goroutine 完成 ✅ 单选题17 答案 +``` + +> [!note] Mutex 更适合保护临界区,Atomic 适合简单计数,Context 适合取消信号传递。WaitGroup 是唯一专门用于「等一组 goroutine 完成」的原语。 + +### 单选题19:RWMutex vs Mutex + +```mermaid +flowchart LR + subgraph Mutex["sync.Mutex"] + M1["同一时刻只能有\n一个 goroutine\n进入临界区"] + end + + subgraph RWMutex["sync.RWMutex"] + R1["多个读锁\n可同时持有"] + R2["或一个写锁\n独占访问"] + end + + Mutex -->|"读少写多时更高效"| W1 + RWMutex -->|"读多写少时优势明显"| W2 + + style R1 fill:#9f9 +``` + +> [!memory] 一句话区分 +> **Mutex 一人进出,RWMutex 多人同读一人独写。** + +--- + +## 八、基础语法补遗 + +### 单选题18:取地址运算符 + +```go +x := 42 +px := &x // & 获取地址 → *int +fmt.Println(*px) // * 解引用 → 42 + +// 结构体同理 +p := Person{Name: "Alice"} +pp := &p +pp.Name // Go 自动解引用(等价于 (*p).Name) +``` + +> [!memory] Go 中关于地址的两个符号 +> +> | 符号 | 名称 | 作用 | +> |------|------|------| +> | `&` | 取地址符 | `px := &x` | +> | `*` | 解引用 / 指针类型前缀 | `val := *px`, `var px *int` | + +### 多选题1:通过指针访问成员变量的方式 + +> [!abstract] 多选题1 全辨析 + +| 选项 | 代码 | 对错 | 解析 | +|------|------|------|------| +| A | `p.name` | ✅ | Go 的语法糖,自动解引用 | +| B | `(&p).name` | ❌ | `p` 已经是变量名,再加 `&` 变成指向指针的指针 | +| C | `(*p).name` | ✅ | 先解引用拿到结构体,再访问字段 | +| D | `p->name` | ❌ | Go 没有 `->` 运算符 | + +> [!note] 注意区分:如果是 `p` 已经是 `*Person` 指针类型,则 `p.name` 和 `(*p).name` 都合法。`&p` 会得到 `**Person`,多了一层无意义间接。 + +--- + +## 错题自测表 + +> [!summary] 遮住右侧,自己检验是否真正理解了 + +| 题目 | 考查点 | ❌ 你的原答案 | ✅ 正确答案 | 关键概念 | +|------|--------|-------------|------------|---------| +| 单6 | map 的引用语义 + json.Unmarshal 参数 | B | **A** | Unmarshal 需传地址;map 是引用类型 | + +--- + +## 高频考点速览 + +> [!tip] 考前快速过一遍 + +| 类别 | 考点 | 一句话口诀 | +|------|------|-----------| +| make vs new | make 初始化引用类型,new 分配零值内存 | **"make 建结构,new 清空白"** | +| slice 共享底层 | 修改切片元素 = 修改原数组对应位置 | **"切片是视图,底层是一家的"** | +| map 引用语义 | 传 map 不拷贝数据,内外同步可见 | **"map 传指针,改哪都看见"** | +| json.Unmarshal 参数 | 必须传 `&mapVar` 才能写入 | **"没地址写不进"** | +| 结构体字段访问 | Go 统一用 `.` | **Go 里没有 `->`** | +| 值 vs 指针接收者 | 要修改对象必须用指针 | **要改就用指针** | +| 接口隐式实现 | 实现全部方法即满足 | **鸭子类型,不用 declare** | +| 无缓冲 channel | 收发必须同时就绪 | **"等你,你等我"** | +| select | 多路复用 channel 操作 | **"谁先好谁先上"** | +| WaitGroup | 等待一组 goroutine 完成 | **加 1 → 起任务 → Done → Wait** | +| RWMutex | 读共享、写独占 | **多人同读,一人独写** | +| 取地址运算符 | `&` 取地址,`*` 解引用 | **"& 取 *" | +| ioutil 废弃 | 被 os/io 包取代 | **Go 1.16+ 别再用 ioutil** | + +--- + +## 关联笔记 + +- [[hzh/EXAM]] diff --git a/hzh/EXAM/Week03.md b/hzh/EXAM/Week03.md new file mode 100644 index 0000000..1b9867b --- /dev/null +++ b/hzh/EXAM/Week03.md @@ -0,0 +1,526 @@ +--- +tags: [Go, 后端开发, 武科大, Week03, 错题精讲] +create time: 2026-05-02 14:30 +--- + +# 金山考试 - 武科大服务端 - 第三次课 · 错题精讲 + +## 概述 + +本次测试涵盖 **HTTP 协议基础、Gin 框架核心机制、CORS 跨域、JWT 认证、Nginx 反向代理** 五大模块,共 30 道单选题。其中 **26 道答对,4 道错题**,集中在 HTTP 基础认知、Gin 中间件执行控制、以及 JWT 解析流程三个知识面上。本笔记逐一拆解错误原因,给出正解辨析和代码验证。 + +> [!tip] 使用建议 +> 先看错题本身的「为什么错 → 为什么对」,再做对照表的自测检查,最后用「高频考点速览」串联记忆薄弱点。 + +--- + +## 一、HTTP 协议基础:GET/POST 与默认端口 + +### 单1:GET 和 POST 的主要区别 + +| 项目 | 内容 | +|------|------| +| ❌ 你的答案 | A(GET 请求数据,POST 只提交数据) | +| ✅ 正确答案 | **B(GET 数据附加在 URL 之后,POST 数据放在请求体内)** | + +> [!error] 你的误区根源 +> 你选了 **A**,描述的是一个"规范层面"的说法——从 RESTful 最佳实践来说 GET 应该用于获取资源、POST 用于创建资源。但题目问的是**主要区别**,而两者最根本的、决定性的差异在于**数据传输位置**。 + +#### 为什么 B 才是本质区别 + +```go +// GET — 参数拼在 URL 后面 ?key=value&name=xxx +GET /api/users?page=1&limit=10 HTTP/1.1 +Host: example.com +// Body 为空 + +// POST — 参数放在 Body 中 +POST /api/users HTTP/1.1 +Host: example.com +Content-Type: application/x-www-form-urlencoded + +page=1&limit=10 +``` + +| 维度 | GET | POST | +|------|-----|------| +| 数据位置 | URL 查询字符串(`?key=value`) | 请求体(Body) | +| 数据长度 | 受 URL 长度限制(通常 ~2KB) | 理论上无限制 | +| 缓存行为 | 可被浏览器缓存、收藏 | 不被缓存 | +| 幂等性 | ✅ 幂等(多次请求结果一致) | ❌ 非幂等(可能重复创建资源) | + +> [!warning] ⭐ A 选项为什么不选 +> "GET 只获取、POST 只提交"是 RESTful **设计规范**,不是 HTTP 协议的硬性约束。实际上你可以 POST 一个纯查询接口,也可以用 GET 传大数据(虽然不规范)。考试中考查的是协议层的事实差异,而非设计哲学。 + +#### 延伸对比 + +```mermaid +flowchart LR + subgraph GET["GET 请求结构"] + G1["Method: GET"] --> G2["URL: /api?q=hello"] + G2 --> G3["Header: ..."] + G3 --> G4["Body: (empty)"] + end + + subgraph POST["POST 请求结构"] + P1["Method: POST"] --> P2["URL: /api"] + P2 --> P3["Header: Content-Type"] + P3 --> P4["Body: key=value&foo=bar"] + end + + style G4 fill:#f99 + style P4 fill:#9f9 +``` + +### 单5:HTTP 默认端口号 + +| 项目 | 内容 | +|------|------| +| ❌ 你的答案 | D(8080) | +| ✅ 正确答案 | **B(80)** | + +> [!memory] 常用端口速记表 + +| 端口 | 协议 | 说明 | +|------|------|------| +| **80** | HTTP | 超文本传输协议标准端口 ✅ | +| **443** | HTTPS | HTTP over TLS/SSL | +| **21** | FTP | 文件传输协议 | +| **22** | SSH | 安全远程登录 | +| **3306** | MySQL | 数据库 | +| **5432** | PostgreSQL | 数据库 | +| **8080** | HTTP(替代) | 开发/代理常用端口,非标准 | +| **8443** | HTTPS(替代) | HTTPS 的常见替代端口 | + +> [!question] 💡 思考 +> 如果 Go 程序监听 80 端口报错 `permission denied`,怎么办? +> +> Linux/macOS 下 1024 以下的端口需要 root 权限。解决方法:① 用 `sudo` 运行;② 将 Nginx 设为反向代理转发到 8080;③ 使用 `authbind` 或 `setcap` 授予特定端口权限。这也是生产环境推荐 Nginx 前置的原因之一。 + +--- + +## 二、Gin 中间件核心机制 + +### 单13:Gin 中间件中继续执行后续逻辑的方法 + +| 项目 | 内容 | +|------|------| +| ❌ 你的答案 | B(`c.Abort()`) | +| ✅ 正确答案 | **A(`c.Next()`)** | + +> [!error] 核心混淆点 +> 你把 `c.Next()` 和 `c.Abort()` 的作用搞反了。这是 Gin 中间件最关键的两个方法,作用完全相反: + +```go +// ✅ c.Next() — 暂停当前中间件,去执行后续所有中间件 + handler +// 执行完后回来继续运行 Next() 之后的代码 +func loggingMiddleware(c *gin.Context) { + start := time.Now() // ① 前置:记录开始时间 + c.Next() // ② 去执行后面的中间件和 handler... + // ③ 执行完回到这里 + fmt.Println(time.Since(start)) // ④ 后置:输出耗时 +} + +// ❌ c.Abort() — 立即终止整个中间件链,后续的 middleware + handler 全都不执行 +func authMiddleware(c *gin.Context) { + token := c.GetHeader("Authorization") + if token == "" { + c.Abort() // ← 后面的所有内容都不再执行 + return // ⚠️ Abort() 后建议紧跟 return + } + c.Next() // 认证通过,才放行 +} +``` + +> [!summary] c.Next() vs c.Abort() 对比 + +| 方法 | 行为 | 类比 | +|------|------|------| +| `c.Next()` | 把请求交给下一个 handler,执行完回来继续 | **"过安检,过了继续走"** | +| `c.Abort()` | 直接拦截,不再往下传递 | **"拦下来,不能进"** | +| `c.AbortWithStatus(401)` | 终止 + 返回指定状态码 | **"不让你进,还给你个理由"** | +| `c.AbortWithError(500, err)` | 终止 + 返回状态码 + 附带错误信息 | **"不让你进,还告诉你出了什么错"** | + +> [!abstract] c.Next() 底层实现 +> +> ```go +> // gin/context.go 简化版 +> func (c *Context) Next() { +> c.index++ // 移动到下一个 handler +> for ; c.index < int8(len(c.handlers)); c.index++ { +> if c.IsAborted() { // 如果前面有调用 Abort() +> return // 循环终止 +> } +> c.handlers[c.index](c) // 执行当前 handler +> } +> } +> ``` +> +> `c.Abort()` 只是设置了一个内部标志位 `isAborted = true`,当 `Next()` 检测到这个标志就停止遍历。 + +
+💡 深入理解:中间件的"洋葱模型" + +Gin 中间件执行可以用经典的"洋葱模型"来理解: + +```mermaid +flowchart TD + A["M1 前置处理"] --> B["M1.Next()"] + B --> C["M2 前置处理"] + C --> D["M2.Next()"] + D --> E["Handler 执行"] + E --> F["M2 后置处理"] + F --> G["M1 后置处理"] + + style E fill:#FFD700 + style A fill:#90EE90 + style B fill:#90EE90 + style C fill:#90EE90 + style D fill:#90EE90 + style F fill:#FFB6C1 + style G fill:#FFB6C1 +``` + +- **进入时**:M1 → M2 → Handler(按注册顺序) +- **退出时**:Handler → M2 → M1(按逆序,栈的行为) +- 这就是为什么日志中间件既能记录请求开始时间,也能记录响应耗时。 + +
+ +### 单12 & 单17:Gin 全局中间件注册与上下文共享 + +> [!note] 这两题你答对了 ✅,放在一起巩固: + +**单12:全局注册中间件** +- 正确写法:`router.Use(middleware)` +- Gin 没有 `RegisterMiddleware`、`AttachMiddleware`、`Middleware` 这些方法 + +**单17:中间件之间共享数据** +- 正确方式:`c.Set(key, val)` / `c.Get(key)` 存入 Context 的 `Keys` map +- 绝对不要用全局变量!每个请求的 Context 是隔离的 + +```go +// 中间件1:设置用户信息 +func authMiddleware(c *gin.Context) { + userID := parseToken(c.GetHeader("Authorization")) + c.Set("userID", userID) // 存入 Context + c.Set("role", getRole(userID)) + c.Next() +} + +// 中间件2/Handler:读取用户信息 +func listUsers(c *gin.Context) { + userID := c.GetString("userID") // 安全获取 + role := c.MustGet("role").(string) // 断言获取(确保 key 存在) + _ = role +} +``` + +### 单14 & 单15 & 单27 & 单28:Gin 参数解析与绑定 + +> [!note] 这四题你全部答对了 ✅,汇总关键 API: + +| 场景 | Gin API | 示例 | +|------|---------|------| +| JSON body → Struct | `c.ShouldBindJSON(&obj)` | `POST /api/user` 接收 JSON | +| 路径参数 `:id` | `c.Param("id")` | `GET /user/:id` → `id="123"` | +| 查询参数 `?key=` | `c.Query("key")` | `GET /search?q=hello` | +| Form 表单 | `c.PostForm("field")` | `POST` x-www-form-urlencoded | +| 动态路由定义 | `router.GET("/user/:id", h)` | Gin 用 `:` 前缀,不是 `{}` | +| JSON 响应 | `c.JSON(200, gin.H{...})` | 返回标准 JSON | +| 未认证状态码 | 401 Unauthorized | 400 是请求格式错,401 是未认证 | + +--- + +## 三、JWT 认证:解析步骤与上下文传递 + +### 单19:Golang 中解析 JWT 的正确步骤 + +| 项目 | 内容 | +|------|------| +| ❌ 你的答案 | C | +| ✅ 正确答案 | **A(解码 → 验证签名 → 检查声明)** | + +> [!error] 你的误区 +> 你可能凭直觉先"检查声明"(比如看看过期时间),但这样做是有安全隐患的:**在没有验证签名的前提下检查声明,等于信任了可能被篡改的数据。** + +#### 正确的 JWT 解析三步法 + +```go +import ( + "github.com/golang-jwt/jwt/v5" +) + +func parseJWT(tokenString string) (*jwt.Claims, error) { + // Step 1: 解码(Decoding)— 把 Base64Url 编码的三个部分拆出来 + // 不验签名,只看格式是否合法 + token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{}) + if err != nil { + return nil, fmt.Errorf("token format invalid: %w", err) + } + + // Step 2: 验证签名(Signature Verification)— 用密钥重新计算 HMAC SHA256 + // 确保 token 没有被第三方篡改 + token, err = jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return []byte("your-secret-key"), nil // 必须用同一个密钥 + }) + if err != nil || !token.Valid { + return nil, fmt.Errorf("signature invalid: %w", err) + } + + // Step 3: 检查声明(Claims Validation)— 此时才能相信数据 + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + return nil, fmt.Errorf("invalid claims type") + } + + // 检查过期时间 + if time.Unix(int64(claims["exp"].(float64)), 0).Before(time.Now()) { + return nil, fmt.Errorf("token expired") + } + + return &claims, nil +} +``` + +> [!warning] ⭐ 高频考点:为什么顺序不能乱? +> +> ``` +> 错误的做法:先读 exp → 发现过期 → 告诉客户端"过期了" +> ↑ 黑客可以伪造 exp = 未来时间来绕过检查 +> +> 正确的做法:先验签名 → 确认数据未被篡改 → 再读 exp → 确实是 2024-01-01 +> ``` +> +> 签名验证是**信任锚**——只有签名正确,里面的所有声明才可信。 + +#### JWT 结构速记 + +```mermaid +flowchart LR + A["JWT Token"] --> B["Header
Base64Url(JSON)"] + A --> C["Payload / Claims
Base64Url(JSON)"] + A --> D["Signature
HMACSHA256
base64UrlHeader.payload + secret"] + + B -->|"alg + typ"| C + C -->|"签名算法对
Header+Payload
加上 Secret"| D + + style D fill:#9f9 +``` + +三部分用 `.` 连接:`xxxxx.yyyyy.zzzzz` + +> [!memory] JWT 解析三步口诀 +> **"先拆开看格式 → 再验身保真 → 最后读内容"** +> +> 解码 → 验签名 → 查声明 + +### 单10 & 单11:JWT 解决的核心痛点与最佳实践 + +> [!note] 这两题你答对了 ✅,补充完整理解: + +**单10:JWT 解决的核心痛点** +- 传统 Session:服务器内存/Redis 存储会话,集群环境下需要同步共享 +- JWT:**无状态**,服务端无需存储会话记录即可验证有效性 +- 注意:JWT 并没有加密用户密码,字符串也比 SessionID **更长**(多了 Header + Payload) + +**单11:JWT 验证后的最佳实践** +- 中间件就像"查票员",查验通过后通过 `c.Set()` 把用户信息存入当前请求的 Context +- 这样后续 Handler 能安全、隔离地拿到用户信息,不会互相干扰 + +```go +func JWTAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + tokenString := c.GetHeader("Authorization") + if tokenString == "" { + c.AbortWithStatusJSON(401, gin.H{"error": "missing token"}) + return + } + + claims, err := parseJWT(tokenString) + if err != nil { + c.AbortWithStatusJSON(401, gin.H{"error": "invalid token"}) + return + } + + // ✅ 最佳实践:存入 Context,供下游使用 + c.Set("userID", claims.UserID) + c.Set("username", claims.Username) + c.Next() + } +} +``` + +--- + +## 四、Cookie 安全与 Session 管理 + +### 单7:防止 XSS 攻击的 Cookie 防护 + +| 项目 | 内容 | +|------|------| +| ✅ 正确答案 | **C(httpOnly 设置为 true)** | + +> [!note] 此题你答对了 ✅,巩固关键知识点: + +```go +// Set-Cookie 的关键属性 +http.SetCookie(w, &http.Cookie{ + Name: "session_id", + Value: sessionValue, + Path: "/", + MaxAge: 3600, // 有效期(秒),0 表示浏览器关闭即失效 + HttpOnly: true, // ✅ 防 XSS —— JS 无法 document.cookie 读取 + Secure: true, // ✅ 防窃听 —— 仅 HTTPS 传输 + SameSite: http.SameSiteStrictMode, // ✅ 防 CSRF —— 严格同源策略 +}) +``` + +| 属性 | 作用 | 防御目标 | +|------|------|---------| +| `HttpOnly` | 禁止 JavaScript 读取 | XSS 窃取 Cookie | +| `Secure` | 仅 HTTPS 传输 | 中间人窃听 | +| `SameSite` | 限制跨站携带 Cookie | CSRF 攻击 | +| `Max-Age` | 控制有效期 | 延长攻击窗口 | + +### 单9:Gin 与 Session + +| 项目 | 内容 | +|------|------| +| ✅ 正确答案 | **C(Session 本质是后端存状态,前端只保存 SessionID)** | + +> [!note] 此题你答对了 ✅。核心要点: +> - Gin 官方库**不自带** Session,需第三方中间件(如 `gorilla/sessions`) +> - Session **数据在服务端**,不在客户端(那是 Cookie 的做法) +> - 关闭浏览器不等于删除服务端 Session(取决于 `Max-Age` 或服务端清理策略) + +### 单21:Session ID 的存储方式 + +| 项目 | 内容 | +|------|------| +| ✅ 正确答案 | **B(Set-Cookie)** | + +> [!note] 此题你答对了 ✅。首次访问时服务端在 HTTP 响应的 Headers 里写入 `Set-Cookie: session_id=abc123`,浏览器自动保存,后续请求自动带上。 + +--- + +## 五、CORS 跨域问题专题 + +### 单22 ~ 单26:跨域全链路 + +> [!note] 这 5 道题你全部答对了 ✅,涵盖完整的跨域知识体系。整理如下: + +**单22:生产环境跨域的标准解决方案** +- Nginx 反向代理统一域名 / CORS 中间件精确配置 + +**单23:为什么要用 Nginx 做反向代理** +- 静态文件分发、隐藏后端 IP、负载均衡、限流 + +**单24:OPTIONS 预检请求的本质** +- CORS Preflight:复杂请求前先问后端"我的自定义头你能接受吗?" + +**单25:Vite Proxy 的开发阶段原理** +- 前端请求发给 Node.js 开发服务器 → 服务器转发给真实后端 → 原样返回 + +**单26:CORS 报错的根本原因** +- 浏览器的同源策略(Same-Origin Policy):防止恶意网站窃取敏感数据 + +> [!abstract] 同源策略判定 + +``` +同源 = 协议相同 && 域名相同 && 端口相同 + +http://localhost:3000 → http://localhost:8080 ❌ 端口不同,跨域! +https://app.example.com → https://api.example.com ❌ 域名不同,跨域! +``` + +详见 `[[DEV/跨域问题调试]]` 的详细排查流程。 + +--- + +## 六、HTTP 状态码与语义 + +### 单3、单6、单29:HTTP 基础概念 + +| 题号 | 考查点 | ✅ 答案 | 解析 | +|------|--------|---------|------| +| 单3 | "未修改"状态码 | **304** | 浏览器发 `If-Modified-Since` / `If-None-Match`,服务端返回 304 让浏览器用本地缓存 | +| 单6 | 资源未被修改的状态码 | **304** | 和单3 考同一个知识点,换了一种问法 | +| 单29 | HTTP "无状态"的含义 | **每次请求独立,服务器不记住之前请求** | 这意味着需要 Cookie/Session/JWT 来维护用户状态 | + +> [!memory] 与缓存相关的状态码 + +| 状态码 | 含义 | 触发条件 | +|--------|------|---------| +| **200** | 正常成功 | 首次请求 / 缓存失效 | +| **304** | 未修改 | 缓存命中,服务端通知浏览器直接用本地副本 | +| **206** | 部分内容 | Range 请求(视频断点续传等场景) | + +--- + +## 七、其他知识点 + +### 单30:Git 临时保存工作进度 + +| 项目 | 内容 | +|------|------| +| ✅ 正确答案 | **C(git stash)** | + +> [!note] 此题你答对了 ✅。 + +```bash +git stash # 保存当前工作区为"栈顶" +git stash pop # 恢复最近一次 stash(同时删除它) +git stash list # 查看所有 stash +git stash apply # 恢复但不删除(留在栈里) +``` + +--- + +## 错题自测表 + +> [!summary] 遮住右侧,自己检验是否真正理解了 + +| 题目 | 考查点 | ❌ 你的原答案 | ✅ 正确答案 | 关键概念 | +|------|--------|-------------|------------|---------| +| 单1 | GET vs POST 的本质区别 | A | **B** | URL 参数 vs Body,是事实差异而非设计规范 | +| 单5 | HTTP 默认端口 | D (8080) | **B (80)** | 80 是标准端口,8080 是常见替代 | +| 单13 | Gin 中间件继续执行的方法 | B (Abort) | **A (Next)** | Next() 继续执行,Abort() 中断链 | +| 单19 | JWT 解析正确顺序 | C | **A** | 解码 → 验签名 → 查声明(签名是信任锚) | + +--- + +## 高频考点速览 + +> [!tip] 考前快速过一遍 + +| 类别 | 考点 | 一句话口诀 | +|------|------|-----------| +| GET vs POST | 数据位置不同(URL vs Body) | **"GET 挂在 URL 上,POST 装在 Body 里"** | +| HTTP 默认端口 | 80 | **"HTTP=80,HTTPS=443"** | +| HTTP 无状态 | 每次请求独立 | **"喝醉就忘,下次重来"** | +| 304 状态码 | 资源未修改,走缓存 | **"没变就用老的"** | +| Gin c.Next() | 继续执行后续中间件 + handler | **"过桥费,交了才能走"** | +| Gin c.Abort() | 中断中间件链 | **"栏杆放下,不让过"** | +| Gin 全局中间件 | router.Use(middleware) | Use,不是 Register / Attach | +| Gin 共享数据 | c.Set() / c.Get() | Context 里存取,别用全局变量 | +| Gin 路径参数 | `/user/:id` | Gin 用 `:` 而不是 `{}` | +| Gin JSON 绑定 | c.ShouldBindJSON(&obj) | 从请求体映射到 Struct | +| 401 状态码 | 未认证/缺少凭证 | **"401 = 你是谁?"** | +| Cookie httpOnly | 防 XSS 读取 | **"JS 看不见,只有浏览器带"** | +| Session 本质 | 后端存状态,前端拿 SessionID | **"服务器记账,前端拿号码牌"** | +| JWT 解析三步 | 解码 → 验签名 → 查声明 | **"先看皮相,再验身份证,最后读内容"** | +| CORS 根因 | 浏览器同源策略 | **"不同源的请求,浏览器不放心"** | +| OPTIONS 预检 | 复杂请求前的"询问" | **"我先问问你能不能干"** | +| Git stash | 临时保存工作进度 | **"先收起来,干完别的再放回"** | +| Nginx 前置 | 反向代理隐藏后端、负载均衡 | **"Nginx 挡在前面,后端躲在后面"** | + +--- + +## 关联笔记 + +- [[hzh/EXAM]] +- [[GIN/3-middleware]] — 中间件完整机制(含 c.Next() / c.Abort() 详解) +- [[GIN/4-context-lifecycle]] — Context 生命周期与方法速查 +- [[GIN/3-middleware/jwt-auth-qa]] — JWT 中间件安全性 +- [[DEV/跨域问题调试]] — 跨域排查全流程 diff --git a/hzh/GO/多态.md b/hzh/GO/多态.md new file mode 100644 index 0000000..d742c67 --- /dev/null +++ b/hzh/GO/多态.md @@ -0,0 +1,211 @@ +--- +tags: [go, polymorphism, interface] +create time: 2026-05-02 14:00 +--- + +# Go 多态 + +## 概述 + +梳理 Go 中的多态机制:通过接口实现"同一份代码处理多种类型",涵盖接口定义与实现、类型断言、type switch、空接口与泛型的取舍等核心用法。 + +## 正文 + +### 一、什么是多态 + +> [!question] 思考:为什么需要多态? +> +> 假设你写了一个打印函数 `Print()`,只能接受 `User` 类型。后来来了一个 `Admin` 类型,你又得写一个 `PrintAdmin()`。如果有十个不同的类型呢?多态就是为了解决这种对不同类型做相同操作时,代码不断膨胀的问题——**让同一份代码处理多种具体类型**。 + +Go 的多态通过接口(Interface)来实现,与其他语言有本质区别: + +```go +// 其他语言(如 Java/C++)需要显式声明实现某个接口 +// Go:只要 struct 实现了接口所有方法,就自动满足该接口,无需 implements / : 关键字 + +type Writer interface { + Write([]byte) (int, error) +} + +type File struct{} +func (f File) Write(p []byte) (int, error) { return len(p), nil } +// File 自动实现 io.Writer,零额外声明 +``` + +核心原则:**接口定义行为,结构体提供实现,双方互不感知对方存在。** + +### 二、接口赋值与方法接收者 + +#### 2.1 基础用法 + +```go +var w io.Writer = file // 结构体 → 接口(隐式转换) +w.Write([]byte("hello")) // 运行时动态调用具体类型的 Write 方法 +``` + +> [!note] 指针接收者的小陷阱 +> +> | 方法接收者 | 什么变量可以赋值给接口 | +> |-----------|---------------------| +> | `func (t T) Method()` | `T` 和 `*T` 都可以 | +> | `func (t *T) Method()` | 只有 `*T` 可以 | +> +> 如果接口要求指针接收者方法,传值类型会编译报错。这是初学者最容易踩的坑之一。 + +#### 2.2 零值接口的特性 + +```go +var w io.Writer // 值为 nil,不会 panic —— 直到你真正调用它 + +w.Write([]byte("test")) // 这里才 panic: nil pointer dereference +``` + +零值接口不等于空字符串或 false,它就是 `nil`,只有在被调用时才会触发 panic。这意味着你可以安全地把未赋值的接口当作参数传递(比如依赖注入中可选的接口)。 + +### 三、类型断言与 Type Switch + +#### 3.1 类型断言 + +从接口值中提取底层具体类型: + +```go +v := interface{}(42) + +val, ok := v.(int) // 安全断言,ok=false 时 val=0,不会 panic +fmt.Println(val, ok) // 42 true + +bad := v.(string) // 不安全断言,类型不对会 panic +fmt.Println(bad) // panic: interface conversion: interface {} is int, not string +``` + +> [!tip] 惯用法:"comma ok" 是 Go 中检查类型的标准模式,类似 map 读取时的写法。几乎所有标准库都遵循这一约定。 + +#### 3.2 Type Switch + +同时处理多种可能类型: + +```go +func describe(i any) { + switch v := i.(type) { + case int: + fmt.Printf("int, value=%d\n", v) + case string: + fmt.Printf("string, value=%s\n", v) + default: + fmt.Printf("unknown type %T\n", v) + } +} +``` + +两种方式的选用建议: + +```mermaid +flowchart TD + A["interface{} 存储的值"] --> B{"确定只处理一种类型?"} + B -->|是| C["用类型断言
v, ok := i.(int)"] + B -->|否,有多种可能| D["用 type switch
switch i.(type)"] + + style C fill:#bfb,stroke:#333 + style D fill:#bbf,stroke:#333 +``` + +### 四、空接口与泛型的取舍 + +#### 4.1 空接口 `any` + +```go +// 能存储任意类型 —— JSON 反序列化正是依赖这一特性 +var x any = "hello" // 等价于: var x interface{} = "hello" +``` + +典型应用: + +```go +// encoding/json.Unmarshal +func Unmarshal(data []byte, v any) error + +// sync.Map +func (*sync.Map) LoadOrStore(key, value any) (actual any, loaded bool) +``` + +#### 4.2 空接口 vs 泛型 + +```go +// ❌ 泛型引入前:空接口做容器,取用时需反复断言 +type AnyList struct{ Items []any } +func (l *AnyList) Add(item any) { l.Items = append(l.Items, item) } + +list := &AnyList{} +list.Add("hi") +list.Add(42) // 编译期不会报错,运行时类型断言会出问题 + +// ✅ Go 1.18+ 泛型方案,类型安全有保障 +type List[T any] struct{ Items []T } +func (l *List[T]) Add(item T) { l.Items = append(l.Items, item) } + +intList := &List[int]{} +intList.Add(1) +intList.Add("hi") // 编译期直接报错 +``` + +> [!summary] 如何选择? +> +> | 场景 | 推荐方案 | +> |------|----------| +> 处理完全未知的类型(JSON 反序列化、日志、缓存) | 空接口 `any` | +> 容器类数据结构(切片、Map、列表) | 泛型 | +> 定义抽象行为接口(如 Reader、Writer) | 普通接口 | + +### 五、实战:策略模式的接口化 + +```go +type Payer interface { + Pay(amount float64) error +} + +type Alipay struct{} +func (a Alipay) Pay(amount float64) error { /* ... */ return nil } + +type WechatPay struct{} +func (w WechatPay) Pay(amount float64) error { /* ... */ return nil } + +type CreditCard struct{} +func (c CreditCard) Pay(amount float64) error { /* ... */ return nil } + +// 多态体现:同一行代码处理三种支付方式 +func Checkout(p Payer, amount float64) { + _ = p.Pay(amount) // 不关心具体是谁在付钱 +} + +Checkout(Alipay{}, 99.0) +Checkout(WechatPay{}, 99.0) +``` + +新增支付方式时无需修改 `Checkout` 本身——符合开闭原则(对扩展开放,对修改封闭)。这就是多态带来的解耦效果。 + +### 六、Type Assertion 的安全用法 + +```go +data := loadData() // returns interface{} + +// ❌ 危险链:一旦类型不对,整条链路 panic +user := data.(*User) +name := user.Name + +// ✅ 安全链:先校验再使用 +if user, ok := data.(*User); ok { + name := user.Name +} else { + log.Printf("unexpected type: %T", data) +} +``` + +> [!warning] 经验法则 +> +> 1. 优先在函数入口做一次类型断言校验,后续逻辑不再重复断言。 +> 2. 尽量避免裸类型断言 `v.(Type)` 而不用 `comma ok`——除非你确信类型必然正确,且愿意用 panic 表达"程序 BUG"。 +> 3. 空接口作为返回值时做好文档说明预期类型,否则调用方容易踩坑。 + +## 关联笔记 + +- [[Go 工程模块化]]