vault backup: 2026-05-02 10:18:49
This commit is contained in:
+356
@@ -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 <token>
|
||||||
|
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 配置与跨域场景
|
||||||
@@ -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,传入的是值拷贝,不影响真正返回值
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>延伸思考:多个 defer 叠加有名返回值</summary>
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、下划线 `_` 的含义
|
||||||
|
|
||||||
|
### 单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)) // ?
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>点击查看答案解析</summary>
|
||||||
|
|
||||||
|
两个都输出 `true`。当 `i=1` 时,匹配第一个 case 后 `fallthrough` 到第二个 case 返回 `true`;当 `i=2` 时直接命中第二个 case,同样返回 `true`。
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、函数声明语法
|
||||||
|
|
||||||
|
### 多选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]]
|
||||||
@@ -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]]
|
||||||
@@ -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()` 检测到这个标志就停止遍历。
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>💡 深入理解:中间件的"洋葱模型"</summary>
|
||||||
|
|
||||||
|
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(按逆序,栈的行为)
|
||||||
|
- 这就是为什么日志中间件既能记录请求开始时间,也能记录响应耗时。
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### 单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<br/>Base64Url(JSON)"]
|
||||||
|
A --> C["Payload / Claims<br/>Base64Url(JSON)"]
|
||||||
|
A --> D["Signature<br/>HMACSHA256<br/>base64UrlHeader.payload + secret"]
|
||||||
|
|
||||||
|
B -->|"alg + typ"| C
|
||||||
|
C -->|"签名算法对<br/>Header+Payload<br/>加上 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/跨域问题调试]] — 跨域排查全流程
|
||||||
+211
@@ -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["用类型断言<br/>v, ok := i.(int)"]
|
||||||
|
B -->|否,有多种可能| D["用 type switch<br/>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 工程模块化]]
|
||||||
Reference in New Issue
Block a user