diff --git a/BACKEND/API 设计.md b/BACKEND/API 设计.md new file mode 100644 index 0000000..29be9cf --- /dev/null +++ b/BACKEND/API 设计.md @@ -0,0 +1,182 @@ +--- +tags: [后端, API, RESTful, 设计] +create time: 2026-04-24 18:41 +--- + +# API 设计 + +## 概述 + +API(Application Programming Interface)是前后端协作的契约。好的 API 设计让前端开发者一看就懂、一用就会。差的 API 设计会让协作效率大幅下降。 + +思考题:设计一个 API 时,是应该"让所有人都符合你的设计"还是"让设计迁就所有使用场景"?这两者的边界在哪里? + +## 正文 + +### 1. RESTful API 设计规范 + +**资源命名:** +- URL 用名词复数,不用动词 +- 层级结构表示从属关系 +- 查询参数用于筛选和排序 + +``` +GET /api/users → 获取用户列表 +GET /api/users/5 → 获取用户 5 的详情 +GET /api/users/5/posts → 获取用户 5 的所有文章 +GET /api/users?role=admin → 筛选管理员用户 +GET /api/users?sort=-created_at → 按创建时间倒序 +GET /api/users?page=2&limit=20 → 分页 +``` + +### 2. 标准响应格式 + +```json +// 成功响应(200 OK) +{ + "code": 0, + "message": "success", + "data": { + "id": 1, + "name": "Alice", + "email": "alice@example.com" + } +} + +// 成功响应(201 Created) +{ + "code": 0, + "message": "created", + "data": { "id": 5, ... } +} + +// 错误响应(4xx / 5xx) +{ + "code": 1001, + "message": "用户不存在", + "data": null +} +``` + +> **提问:** 错误码设计成数字好还是字符串好?数字更紧凑但可读性差,字符串更直观但传输量大。你怎么权衡? + +### 3. 常见错误码设计 + +| code | HTTP 状态码 | 含义 | +|------|-------------|------| +| 0 | 200 | 成功 | +| 1001 | 404 | 资源不存在 | +| 1002 | 400 | 参数校验失败 | +| 1003 | 401 | 未认证 | +| 1004 | 403 | 无权限 | +| 1005 | 429 | 请求过于频繁 | +| 2001 | 500 | 服务器内部错误 | + +### 4. 分页设计 + +``` +GET /api/users?page=1&limit=20 + +响应: +{ + "code": 0, + "data": [...], // 当前页数据 + "pagination": { + "page": 1, + "limit": 20, + "total": 100, + "total_pages": 5 + } +} +``` + +> **核心概念:** 客户端分页 vs 服务端分页。大数据量时必须服务端分页,否则性能灾难。 + +### 5. 版本控制 + +``` +方案一:URL 路径(推荐) +/api/v1/users +/api/v2/users + +方案二:请求头 +Accept: application/vnd.api.v1+json +``` + +### 6. Go 后端 API 示例 + +```go +package main + +import ( + "encoding/json" + "net/http" + "time" +) + +// 统一响应结构 +type APIResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data"` +} + +func writeJSON(w http.ResponseWriter, status int, resp APIResponse) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(resp) +} + +// 处理用户列表 +func userListHandler(w http.ResponseWriter, r *http.Request) { + // 1. 解析查询参数 + page, _ := strconv.Atoi(r.URL.Query().Get("page")) + if page < 1 { + page = 1 + } + limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) + if limit == 0 { + limit = 20 + } + + // 2. 查询数据库(省略) + users, total := getUsersFromDB(page, limit) + + // 3. 返回统一格式 + writeJSON(w, http.StatusOK, APIResponse{ + Code: 0, + Message: "success", + Data: map[string]interface{}{ + "users": users, + "pagination": map[string]int{ + "page": page, + "limit": limit, + "total": total, + "pages": (total + limit - 1) / limit, + }, + }, + }) +} +``` + +```mermaid +graph TD + A[HTTP Request] --> B{方法检查} + B -->|GET| C[解析查询参数] + B -->|POST| D[解析请求体] + C --> E[参数校验] + D --> E + E --> F{校验通过?} + F -->|否| G[返回 400 + 错误信息] + F -->|是| H[查询/操作数据库] + H --> I{操作成功?} + I -->|否| J[返回对应错误码] + I -->|是| K[返回 200/201 + 数据] +``` + +## 关联笔记 + +- [[HTTP 协议]] — API 设计的基础是理解 HTTP +- [[Go 后端基础]] — Go 实现 API 的具体方法 +- [[数据库基础]] — API 的数据来源 +- [[30.areas/finance/Investment lessons/2024.Current trading lessons.md]] diff --git a/BACKEND/Go 后端基础.md b/BACKEND/Go 后端基础.md new file mode 100644 index 0000000..5c772f1 --- /dev/null +++ b/BACKEND/Go 后端基础.md @@ -0,0 +1,255 @@ +--- +tags: [后端, Go, Gin, 基础] +create time: 2026-04-24 18:41 +--- + +# Go 后端基础 + +## 概述 + +Go 是一门适合写后端的语言:编译速度快、并发模型简单(goroutine + channel)、标准库完善。结合 Gin 框架,可以快速构建高性能的 RESTful API。 + +思考题:Go 的 goroutine 和线程有什么区别?为什么百万级 goroutine 不会撑爆内存? + +## 正文 + +### 1. 基础 HTTP 服务 + +```go +package main + +import "net/http" + +func main() { + // 最简单的 HTTP 服务 + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello, World!")) + }) + + http.ListenAndServe(":8080", nil) +} +``` + +### 2. 使用 Gin 框架 + +```go +package main + +import "github.com/gin-gonic/gin" + +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` +} + +func main() { + r := gin.Default() // 默认中间件(日志 + 恢复) + + // 路由分组:/api/users + users := r.Group("/api/users") + { + users.GET("", listUsers) + users.GET("/:id", getUser) + users.POST("", createUser) + users.PUT("/:id", updateUser) + users.DELETE("/:id", deleteUser) + } + + r.Run(":8080") +} +``` + +#### 路由分组的作用与原理 + +**思考题:如果把所有路由都写成 `r.GET("/api/users/...")` 而不是用 `.Group()`,会有什么缺点?** + +| 维度 | 说明 | +|------|------| +| **作用** | 自动为一批路由添加公共前缀(prefix),避免重复写相同的路径片段 | +| **中间件继承** | 分组上 `Use()` 的中间件只作用于该分组下的路由 | +| **路由注册** | `Group()` 返回一个新的 `IRoute`,内部维护一个前缀累加器 | + +**底层原理(Gin 源码级别):** + +```go +// gin/group.go 简化逻辑 + +func (group *routerGroup) Group(path string) RouterGroup { + // 新前缀 = 当前前缀 + 传入的路径 + newPath := group.prefix + path + return RouterGroup{ + handlers: group.handlers, // 继承已注册的中间件 + prefix: newPath, // 累加前缀 + engine: group.engine, // 共享同一个路由引擎 + } +} + +func (group *routerGroup) registerHandler(method, relativePath string, handlers ...HandlerFunc) { + // 最终路径 = 分组前缀 + 路由路径 + absolutePath := group.prefix + relativePath + group.engine.handle(method, absolutePath, handlers) +} +``` + +**路由匹配示意:** + +``` +r.Group("/api/users") +├── users.GET("") → /api/users (GET) +├── users.GET("/:id") → /api/users/:id (GET) +├── users.POST("") → /api/users (POST) +├── users.PUT("/:id") → /api/users/:id (PUT) +└── users.DELETE("/:id") → /api/users/:id (DELETE) +``` + +**嵌套分组示例:** + +```go +v1 := r.Group("/api/v1") +{ + users := v1.Group("/users") + { + users.Use(v1AuthMiddleware()) // 仅 v1 的 /users 生效 + users.GET("", listUsers) + users.GET("/:id", getUser) + } + // 嵌套后前缀是: /api/v1/users +} +``` + +> **提问:** 为什么分组中间件(`group.Use()`)只能作用于该分组及其子分组内的路由,而不能影响 `r.Group()` 之外的路由?(提示:中间件本质上是 HTTP 处理链,和路由树是正交的) + +### 3. 中间件 — 横切关注点的利器 + +```go +// 认证中间件 +func authMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + token := c.GetHeader("Authorization") + if token == "" { + c.JSON(401, gin.H{"code": 1003, "message": "未认证"}) + c.Abort() + return + } + // 验证 token... + c.Next() + } +} + +// 使用 +r.Use(authMiddleware()) // 全局中间件 +r.GET("/profile", getProfile) + +// 局部中间件 +admin := r.Group("/api/admin") +admin.Use(adminMiddleware()) +admin.GET("/stats", adminStats) +``` + +常见中间件类型: + +| 中间件 | 作用 | +|--------|------| +| CORS | 跨域处理 | +| RateLimit | 限流 | +| Logger | 请求日志 | +| Recovery | 异常恢复 | +| Auth | 身份认证 | + +### 4. 结构体标签与 JSON 序列化 + +```go +type CreateUserRequest struct { + Name string `json:"name" binding:"required,min=2,max=50"` + Email string `json:"email" binding:"required,email"` + Age int `json:"age" binding:"required,min=1,max=150"` +} + +// Gin 自动绑定和校验 +func createUser(c *gin.Context) { + var req CreateUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(400, gin.H{"code": 1002, "message": "参数校验失败", "errors": err.Error()}) + return + } + // ... +} +``` + +> **提问:** `binding:"required,email"` 中的标签是 Gin 内置的校验规则。如果需要自定义校验逻辑(比如检查用户名是否已存在),应该怎么实现? + +### 5. 错误处理 + +```go +// Go 风格的错误处理 +func getUser(c *gin.Context) { + id := c.Param("id") + + user, err := db.GetUser(id) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + c.JSON(404, gin.H{"code": 1001, "message": "用户不存在"}) + return + } + c.JSON(500, gin.H{"code": 2001, "message": "服务器错误"}) + return + } + + c.JSON(200, gin.H{"data": user}) +} +``` + +### 6. 完整 API 示例 + +```go +package main + +import ( + "log" + "net/http" + "github.com/gin-gonic/gin" +) + +// 统一响应 +type Response struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +func main() { + r := gin.Default() + + // CORS 中间件 + r.Use(func(c *gin.Context) { + c.Header("Access-Control-Allow-Origin", "*") + c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE") + c.Header("Access-Control-Allow-Headers", "Content-Type,Authorization") + if c.Request.Method == "OPTIONS" { + c.AbortWithStatus(204) + return + } + c.Next() + }) + + // 用户路由 + r.GET("/api/health", func(c *gin.Context) { + c.JSON(200, Response{Code: 0, Message: "ok"}) + }) + + r.GET("/api/users", listUsers) + r.POST("/api/users", createUser) + + log.Println("Server starting on :8080") + r.Run(":8080") +} +``` + +## 关联笔记 + +- [[HTTP 协议]] — Gin 处理 HTTP 请求的基础 +- [[API 设计]] — 按照设计的 API 规范实现 +- [[数据库基础]] — Go 操作数据库的方法 +- [[部署与运维基础]] — Go 应用部署 +- [[30.areas/finance/Investment lessons/2024.Current trading lessons.md]] diff --git a/BACKEND/HTTP 协议.md b/BACKEND/HTTP 协议.md new file mode 100644 index 0000000..5e6fd43 --- /dev/null +++ b/BACKEND/HTTP 协议.md @@ -0,0 +1,152 @@ +--- +tags: [后端, HTTP, 协议, 基础] +create time: 2026-04-24 18:41 +--- + +# HTTP 协议 + +## 概述 + +HTTP(HyperText Transfer Protocol)是浏览器和服务器之间通信的"语言"。理解 HTTP 是理解 Web 工作原理的第一步。 + +思考题:你在浏览器地址栏输入 `https://www.google.com` 后按下回车,HTTP 协议在这个过程中扮演了什么角色? + +## 正文 + +### 1. HTTP 请求结构 + +``` +请求行 → 方法 + URL + 协议版本 +请求头 → Key-Value 对,携带元信息 +空行 +请求体 → GET 请求通常为空,POST/PUT 有数据 +``` + +``` +POST /api/users HTTP/1.1 +Host: api.example.com +Content-Type: application/json +Authorization: Bearer eyJhbGciOiJIUzI1NiIs... +Content-Length: 42 + +{"name": "Alice", "age": 25} +``` + +### 2. HTTP 方法(动词) + +| 方法 | 语义 | 幂等性 | 典型场景 | +|------|------|--------|----------| +| `GET` | 读取数据 | ✅ 是 | 获取列表、详情 | +| `POST` | 创建资源 | ❌ 否 | 提交表单、创建用户 | +| `PUT` | 全量更新 | ✅ 是 | 完整替换一个资源 | +| `PATCH` | 部分更新 | ❌ 否 | 修改用户某个字段 | +| `DELETE` | 删除资源 | ✅ 是 | 删除用户、删除文章 | + +```mermaid +graph LR + A[CRUD] -->|Create| B[POST] + A -->|Read| C[GET] + A -->|Update| D[PUT / PATCH] + A -->|Delete| E[DELETE] +``` + +> **提问:** 为什么 `GET` 请求要求"幂等"和"安全"(不修改服务器状态)?如果 `GET` 请求能删除数据,会发生什么安全问题? + +### 3. 状态码分类 + +``` +1xx → 信息性(处理中) +2xx → 成功(200 OK, 201 Created) +3xx → 重定向(301 永久, 302 临时) +4xx → 客户端错误(400 参数错误, 401 未认证, 403 无权限, 404 不存在) +5xx → 服务端错误(500 服务器内部错误, 502 网关错误, 503 服务不可用) +``` + +> **实战要点:** 前端开发最常遇到的是 401(Token 过期)和 403(权限不足),理解它们的区别很重要:401 是"你是谁?",403 是"你是谁,但你不能做这件事。" + +### 4. 常用请求头 + +| Header | 说明 | +|--------|------| +| `Content-Type` | 请求体格式(`application/json` / `application/x-www-form-urlencoded`) | +| `Authorization` | 认证信息(`Bearer `) | +| `Accept` | 期望的响应格式 | +| `Cookie` | 客户端存储的会话数据 | +| `Referer` | 来源页面(用于防盗链) | +| `User-Agent` | 客户端信息(浏览器/设备) | + +### 5. RESTful 设计原则 + +RESTful 不是严格的规范,而是一组设计哲学: + +``` +资源命名用名词复数,动词用 HTTP 方法: + +GET /api/users → 获取所有用户 +GET /api/users/:id → 获取单个用户 +POST /api/users → 创建用户 +PUT /api/users/:id → 更新用户(全量) +PATCH /api/users/:id → 更新用户(部分) +DELETE /api/users/:id → 删除用户 +``` + +```mermaid +graph TD + A[RESTful 核心原则] --> B["资源用 URL 表示
(名词复数)"] + A --> C["操作用 HTTP 方法表示
(GET/POST/PUT/DELETE)"] + A --> D["状态码表示结果
(200/201/404/500)"] + A --> E["无状态
(每次请求独立)"] + A --> F["JSON 作为数据交换格式"] +``` + +> **思考:** 为什么 RESTful 要求 URL 中用名词而不是动词?`GET /api/deleteUser/5` 和 `DELETE /api/users/5` 哪个更符合 RESTful 原则?为什么? + +### 6. Go 后端处理 HTTP 请求示例 + +```go +package main + +import ( + "encoding/json" + "net/http" +) + +// 定义请求结构体 +type CreateUserRequest struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func handleCreateUser(w http.ResponseWriter, r *http.Request) { + // 只接受 POST 方法 + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // 解析请求体 + var req CreateUserRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + // 处理业务逻辑... + // 创建用户 + + // 返回 201 Created + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + json.NewEncoder(w).Encode(map[string]interface{}{ + "id": 1, + "name": req.Name, + "age": req.Age, + }) +} +``` + +## 关联笔记 + +- [[API 设计]] — 基于 HTTP 协议的 API 设计方法 +- [[数据库基础]] — HTTP 请求最终需要操作数据库 +- [[30.areas/finance/Investment lessons/2024.Current trading lessons.md]] diff --git a/BACKEND/README.md b/BACKEND/README.md new file mode 100644 index 0000000..dc02afc --- /dev/null +++ b/BACKEND/README.md @@ -0,0 +1,81 @@ +--- +tags: [后端, 全栈, 学习路线] +create time: 2026-04-24 18:41 +--- + +# 后端学习总览 + +## 概述 + +后端开发是产品的心脏——处理业务逻辑、管理数据存储、提供 API 给前端调用。这个文件夹从网络协议(HTTP)开始,经过数据库、API 设计,最终落地到 Go 语言实现,完整覆盖全栈后端的核心知识。 + +核心问题:前端和后端之间靠什么通信?HTTP 协议在整个请求-响应链路中扮演什么角色? + +## 学习路线图 + +```mermaid +graph LR + A[HTTP 协议] --> B[数据库基础] + B --> C[API 设计] + C --> D[Go 后端基础] + D --> E[部署与运维基础] + E --> F[生产环境实战] +``` + +### 阶段一:通信协议 — HTTP + +理解浏览器和服务器之间如何"对话"。HTTP 是 Web 的基石,不学 HTTP 就无法真正理解前后端交互。 + +### 阶段二:数据存储 — 数据库 + +掌握关系型数据库(SQL)的核心概念:表、关系、索引、事务。数据是后端的核心资产。 + +### 阶段三:API 设计 + +学习如何设计清晰、一致的 RESTful API。好的 API 设计能让前后端协作效率翻倍。 + +### 阶段四:Go 后端实现 + +用 Go 语言实现后端逻辑:Gin 框架、中间件、数据库操作、错误处理。 + +### 阶段五:部署与运维 + +将应用部署上线。Nginx 反向代理、Docker 容器化、环境变量管理。 + +## 笔记索引 + +| 笔记 | 状态 | 说明 | +|------|------|------| +| [[HTTP 协议]] | 基础 | 请求方法、状态码、Headers、RESTful | +| [[数据库基础]] | 基础 | SQL、关系模型、索引、事务 | +| [[API 设计]] | 进阶 | RESTful 设计、参数传递、错误处理 | +| [[Go 后端基础]] | 进阶 | Go 语法、Gin 框架、中间件 | +| [[部署与运维基础]] | 进阶 | Nginx、Docker、环境变量 | + +## 前后端协作全景 + +```mermaid +sequenceDiagram + participant C as 浏览器 (前端) + participant N as Nginx + participant S as Go 服务 (后端) + participant D as 数据库 + C->>N: HTTP 请求 (GET /api/users) + N->>S: 转发请求 + S->>D: SQL 查询 (SELECT * FROM users) + D-->>S: 返回用户数据 + S->>S: 业务逻辑处理 + S-->>N: JSON 响应 + N-->>C: 200 OK + JSON 数据 + C->>C: React 组件渲染 +``` + +## 学习建议 + +- **先理解 HTTP,再动手写代码。** 不理解 HTTP 请求/响应结构,写 API 就是在盲猜。 +- **数据库是后端的根基。** 索引、事务、连接池这些概念必须在写代码前理解清楚。 +- **API 设计要站在调用者的角度。** 前端开发者会感谢你设计得清晰的 API。 + +## 关联笔记 + +- [[30.areas/finance/Investment lessons/2024.Current trading lessons.md]] diff --git a/BACKEND/数据库基础.md b/BACKEND/数据库基础.md new file mode 100644 index 0000000..2d7d536 --- /dev/null +++ b/BACKEND/数据库基础.md @@ -0,0 +1,183 @@ +--- +tags: [后端, 数据库, SQL, 基础] +create time: 2026-04-24 18:41 +--- + +# 数据库基础 + +## 概述 + +数据库是后端的核心。几乎每个应用都需要持久化数据——用户信息、订单记录、文章内容……关系型数据库(MySQL/PostgreSQL)是后端开发的基石。 + +思考题:为什么关系型数据库能统治这么多年?JSON 数据库(如 MongoDB)出现了,为什么我们仍然需要 SQL? + +## 正文 + +### 1. 关系模型 + +关系数据库用"表"来组织数据,表与表之间通过"关系"(外键)连接。 + +```mermaid +erDiagram + USERS ||--o{ POSTS : creates + USERS { + int id PK + string username + string email + datetime created_at + } + POSTS { + int id PK + int user_id FK + string title + string content + datetime published_at + } +``` + +### 2. 基础 SQL + +```sql +-- 创建表 +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + email VARCHAR(100) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +-- 插入数据 +INSERT INTO users (username, email, password) +VALUES ('alice', 'alice@example.com', 'hashed_password'); + +-- 查询数据 +SELECT id, username, email FROM users WHERE id = 1; + +-- 更新数据 +UPDATE users SET email = 'new@example.com' WHERE id = 1; + +-- 删除数据 +DELETE FROM users WHERE id = 1; +``` + +### 3. 多表查询 + +```sql +-- JOIN 联表查询 +SELECT u.username, p.title, p.content +FROM users u +INNER JOIN posts p ON u.id = p.user_id +WHERE u.id = 1; + +-- 子查询 +SELECT * FROM users +WHERE id IN (SELECT user_id FROM posts WHERE created_at > '2026-01-01'); + +-- 聚合查询 +SELECT user_id, COUNT(*) as post_count +FROM posts +GROUP BY user_id +HAVING post_count > 10; +``` + +### 4. 索引 — 数据库性能的命脉 + +```sql +-- 创建索引 +CREATE INDEX idx_posts_user_id ON posts(user_id); +CREATE INDEX idx_posts_created_at ON posts(created_at DESC); + +-- 复合索引 +CREATE INDEX idx_user_email ON users(email, created_at); +``` + +```mermaid +graph LR + A[全表扫描 O(n)] -.->|慢| B[数据量增大] + C[索引查找 O(log n)] -.->|快| D[数据量增大] +``` + +> **核心要点:** 索引加速查询但减慢写入。每条写入(INSERT/UPDATE/DELETE)都需要更新索引。不是所有字段都需要索引,通常只给经常用于 WHERE 和 JOIN 的字段加索引。 + +> **提问:** B+ 树索引为什么比哈希索引更适合范围查询? + +### 5. 事务(Transaction)— 数据的最后一道防线 + +```sql +-- 事务保证要么全部成功,要么全部回滚 +BEGIN; + +UPDATE accounts SET balance = balance - 100 WHERE user_id = 1; +UPDATE accounts SET balance = balance + 100 WHERE user_id = 2; + +-- 检查是否有错误 +-- COMMIT; → 全部提交 +-- ROLLBACK; → 全部回滚 +``` + +ACID 四个特性: + +| 特性 | 说明 | 示例 | +|------|------|------| +| **A**tomivity(原子性) | 事务要么全做,要么全不做 | 转账时扣款和加款必须同时成功 | +| **C**onsistency(一致性) | 事务前后数据保持一致 | 转账前后总金额不变 | +| **I**solation(隔离性) | 并发事务互不干扰 | 两个用户同时转账不会出问题 | +| **D**urability(持久性) | 提交后数据永久保存 | 断电后数据不会丢失 | + +> **思考:** 如果转账过程中服务器在两条 UPDATE 之间崩溃,会发生什么?ACID 的原子性如何避免这个问题? + +### 6. Go 操作数据库示例 + +```go +package main + +import ( + "database/sql" + "fmt" + _ "github.com/lib/pq" // PostgreSQL 驱动 +) + +type User struct { + ID int + Username string + Email string +} + +func getUser(db *sql.DB, id int) (*User, error) { + var user User + err := db.QueryRow("SELECT id, username, email FROM users WHERE id = $1", id). + Scan(&user.ID, &user.Username, &user.Email) + if err != nil { + return nil, err + } + return &user, nil +} + +// 事务示例 +func transfer(db *sql.DB, fromID, toID, amount int) error { + tx, err := db.Begin() + if err != nil { + return err + } + defer tx.Rollback() // 出错时自动回滚 + + _, err = tx.Exec("UPDATE accounts SET balance = balance - $1 WHERE user_id = $2", amount, fromID) + if err != nil { + return err + } + + _, err = tx.Exec("UPDATE accounts SET balance = balance + $1 WHERE user_id = $2", amount, toID) + if err != nil { + return err + } + + return tx.Commit() +} +``` + +## 关联笔记 + +- [[API 设计]] — API 需要操作数据库 +- [[Go 后端基础]] — Go 连接数据库的实践 +- [[30.areas/finance/Investment lessons/2024.Current trading lessons.md]] diff --git a/BACKEND/部署与运维基础.md b/BACKEND/部署与运维基础.md new file mode 100644 index 0000000..c1020ca --- /dev/null +++ b/BACKEND/部署与运维基础.md @@ -0,0 +1,224 @@ +--- +tags: [后端, 部署, 运维, 基础] +create time: 2026-04-24 18:41 +--- + +# 部署与运维基础 + +## 概述 + +写完代码只是第一步,让代码在生产环境中稳定运行才是真正的工作。部署与运维涵盖了从构建、容器化、反向代理到监控的所有环节。 + +思考题:为什么本地运行正常的代码,部署到服务器就出问题?最常见的坑有哪些? + +## 正文 + +### 1. 环境变量 — 环境差异的桥梁 + +```bash +# 不同环境配置不同的变量 +# .env.production +DATABASE_HOST=db.production.com +DATABASE_PORT=5432 +DATABASE_USER=admin +DATABASE_PASSWORD=secret +JWT_SECRET=very-long-secret-key +PORT=8080 +LOG_LEVEL=warn +``` + +```go +// Go 中读取环境变量 +import "os" + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = "8080" // 默认值 + } + + logLevel := os.Getenv("LOG_LEVEL") + // ... +} +``` + +> **核心原则:** 12-Factor App 原则——配置必须存储在环境变量中,绝不能硬编码到代码里。同一份代码,换环境只需换变量。 + +> **提问:** 为什么数据库密码不能写在代码里或提交到 Git?`.env` 文件应该加入 `.gitignore` 吗? + +### 2. Nginx — 反向代理与负载均衡 + +```nginx +server { + listen 80; + server_name api.example.com; + + # 反向代理到 Go 服务 + location /api/ { + proxy_pass http://127.0.0.1:8080; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + + # 前端静态文件 + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; # SPA 路由支持 + } + + # 静态资源缓存 + location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + } +} +``` + +```mermaid +sequenceDiagram + participant C as 客户端 + participant N as Nginx:80 + participant G as Go:8080 + participant F as 静态文件 + C->>N: GET /api/users + N->>G: proxy_pass → :8080/api/users + G-->>N: JSON 响应 + N-->>C: 返回数据 + + C->>N: GET /index.html + N->>F: 读取本地文件 + F-->>N: HTML 文件 + N-->>C: 返回页面 +``` + +> **思考:** Nginx 既能做反向代理又能提供静态文件服务。为什么要把 API 和前端静态文件放在一起部署,而不是分开? + +### 3. Docker — 容器化部署 + +```dockerfile +# 多阶段构建(减小镜像体积) +FROM golang:1.22-alpine AS builder + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o server . + +# 最终镜像 +FROM alpine:3.19 +WORKDIR /app +COPY --from=builder /app/server . +EXPOSE 8080 + +# 安全:不用 root 运行 +RUN adduser -D appuser +USER appuser + +CMD ["./server"] +``` + +```bash +# 构建镜像 +docker build -t myapp:latest . + +# 运行容器 +docker run -d \ + --name myapp \ + -p 8080:8080 \ + -e DATABASE_HOST=db \ + -e JWT_SECRET=secret \ + myapp:latest +``` + +### 4. Docker Compose — 多服务编排 + +```yaml +version: '3.8' + +services: + app: + build: . + ports: + - "8080:8080" + environment: + - DATABASE_HOST=postgres + - DATABASE_PORT=5432 + - DATABASE_USER=postgres + - DATABASE_PASSWORD=secret + depends_on: + - postgres + restart: unless-stopped + + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: myapp + POSTGRES_PASSWORD: secret + volumes: + - pgdata:/var/lib/postgresql/data + restart: unless-stopped + + nginx: + image: nginx:alpine + ports: + - "80:80" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf + depends_on: + - app + restart: unless-stopped + +volumes: + pgdata: +``` + +### 5. 持续集成/持续部署(CI/CD)基础 + +```yaml +# .github/workflows/deploy.yml +name: Deploy + +on: + push: + branches: [main] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t myapp:${{ github.sha }} . + + - name: Push to registry + run: | + docker tag myapp:${{ github.sha }} registry.example.com/myapp:${{ github.sha }} + docker push registry.example.com/myapp:${{ github.sha }} + + - name: Deploy to server + run: ssh deploy@server "docker pull registry.example.com/myapp:${{ github.sha }} && docker-compose up -d" +``` + +### 6. 常用运维命令 + +```bash +# Docker 常用 +docker ps # 查看运行中的容器 +docker logs -f myapp # 查看容器日志 +docker exec -it myapp sh # 进入容器 +docker-compose up -d # 启动所有服务 + +# Go 服务 +kill -SIGTERM $(pgrep server) # 优雅停止 +go tool pprof http://localhost:6060/debug/pprof/heap # 内存分析 +``` + +## 关联笔记 + +- [[Go 后端基础]] — Go 应用的代码实现 +- [[API 设计]] — 部署的 API 需要按规范设计 +- [[30.areas/finance/Investment lessons/2024.Current trading lessons.md]] diff --git a/CS/TOOLS/skills 管理工具.md b/CS/TOOLS/skills 管理工具.md new file mode 100644 index 0000000..2ef3472 --- /dev/null +++ b/CS/TOOLS/skills 管理工具.md @@ -0,0 +1,202 @@ +--- +tags: [tools, cli, agent, skills, claude-code, cursor] +create time: 2026-04-24 10:00 +--- + +# Skills 管理工具 + +## 概述 + +`skills` 是开源智能体技能生态的 CLI 管理工具([仓库](https://github.com/YishenTu/claudian)),用于在多种 AI 编程智能体(如 Claude Code、Cursor、Codex 等 40+ 种)之间统一安装、搜索、管理和创建 Agent Skills。 + +> **什么是 Agent Skill?** 它是一段可复用的指令集(定义在 `SKILL.md` 文件中),通过 YAML frontmatter 声明名称和描述,让 AI 智能体获得特定领域的专业知识。 + +**想一想:** 为什么需要一个统一的 skills 管理工具,而不是各智能体各自管理? + +## 安装方式 + +```bash +# 从 GitHub 仓库安装 +npx skills add vercel-labs/agent-skills + +# 从完整 GitHub URL 安装 +npx skills add https://github.com/vercel-labs/agent-skills + +# 安装仓库中某个具体的 skill +npx skills add https://github.com/vercel-labs/agent-skills/tree/main/skills/web-design-guidelines + +# 从 GitLab / 任意 git URL 安装 +npx skills add https://gitlab.com/org/repo +npx skills add git@github.com:vercel-labs/agent-skills.git + +# 从本地路径安装 +npx skills add ./my-local-skills +``` + +## 核心概念 + +### 安装作用域 + +| 作用域 | 标志 | 存放位置 | 适用场景 | +|---------|------|----------|----------| +| **项目级** | (默认) | `.//skills/` | 提交到仓库,与团队共享 | +| **全局级** | `-g` | `~//skills/` | 所有项目通用 | + +### 安装方式 + +| 方式 | 说明 | +|------|------| +| **符号链接(推荐)** | 各智能体指向同一份文件,单一来源,更新方便 | +| **复制** | 每个智能体持有独立副本,适用于不支持符号链接的场景 | + +```bash +# 全局安装 +npx skills add vercel-labs/agent-skills -g + +# 复制而非符号链接 +npx skills add vercel-labs/agent-skills --copy +``` + +### 支持的智能体 + +工具支持 **40+ 种** 编程智能体,包括 Claude Code、Cursor、Codex、OpenCode、Windsurf、Copilot、Kiro CLI 等。安装时会自动检测已安装智能体,若无则提示选择。 + +> **注意:** Kiro CLI 用户需手动在 `.kiro/agents/.json` 中添加 `resources` 配置。 + +## 常用操作 + +```bash +# 列出仓库中可用技能(不安装) +npx skills add vercel-labs/agent-skills --list + +# 安装指定技能 +npx skills add vercel-labs/agent-skills --skill frontend-design --skill skill-creator + +# 安装到指定智能体 +npx skills add vercel-labs/agent-skills -a claude-code -a opencode + +# 静默安装(CI/CD 友好) +npx skills add vercel-labs/agent-skills --skill frontend-design -g -a claude-code -y + +# 一键安装所有技能到所有智能体 +npx skills add vercel-labs/agent-skills --all +``` + +### 管理已安装技能 + +```bash +# 列出所有已安装的技能 +npx skills list +npx skills ls -g # 仅全局 +npx skills ls -a cursor # 仅指定智能体 + +# 交互式搜索技能(或按关键词搜索) +npx skills find +npx skills find typescript + +# 更新技能 +npx skills update # 全部更新 +npx skills update my-skill # 单个 +npx skills update -g # 仅全局 + +# 移除技能 +npx skills remove # 交互式选择 +npx skills remove web-design-guidelines # 指定名称 +npx skills remove --all # 全部移除 +npx skills rm my-skill # rm 别名 +``` + +## 创建自定义 Skill + +一个 Skill 就是一个包含 `SKILL.md` 的目录: + +```markdown +--- +name: my-skill +description: 这个技能做什么,何时使用它 +--- + +# My Skill + +智能体遵循的操作指令。 + +## 何时使用 + +描述适用场景。 + +## 步骤 + +1. 首先,做这个 +2. 然后,做那个 +``` + +**必填字段:** `name`(唯一标识,小写+连字符)和 `description`(简短说明)。 + +**可选字段:** `metadata.internal: true` 可将技能设为隐藏,仅 `INSTALL_INTERNAL_SKILLS=1` 时可见。 + +```bash +# 在当前目录创建 SKILL.md 模板 +npx skills init + +# 在子目录创建 +npx skills init my-skill +``` + +### SKILL.md 的结构设计(教学者模式) + +编写 skill 时,建议遵循以下结构让智能体更容易理解: + +| 章节 | 目的 | 示例 | +|------|------|------| +| `name` + `description` | 告诉智能体这是什么 | "用于生成 Git 发布日志" | +| **何时使用** | 触发条件 | "在 `git log` 后使用" | +| **步骤** | 具体操作 | 分步指令 | + +> **思考:** 为什么 description 需要写得简洁但具体?因为智能体依赖它来判断何时激活该 skill。 + +## 技能发现路径 + +CLI 会在仓库中按以下路径自动搜索 `SKILL.md`: + +``` +SKILL.md # 仓库根目录 +skills/ + .curated/ # 经过审核的技能 + .experimental/ # 实验性技能 + .system/ # 系统级技能 +.agents/skills/ +.claude/skills/ +.cursor/skills/ +...(各智能体专属目录) +``` + +也支持通过 `.claude-plugin/marketplace.json` 声明插件技能,兼容 Claude Code 插件市场生态。 + +## 跨智能体兼容性 + +所有技能遵循统一的 [Agent Skills Specification](https://agentskills.io),但部分特性仅部分智能体支持: + +- **基础技能** — 所有智能体均支持 +- **`allowed-tools`** — 大多数支持,Kiro CLI 和 Zencoder 不支持 +- **`context: fork`** — 仅 Claude Code 支持 +- **Hooks** — Claude Code 和 Cline 支持 + +## 环境变量 + +```bash +# 显示隐藏的 internal 技能 +INSTALL_INTERNAL_SKILLS=1 npx skills add vercel-labs/agent-skills --list + +# 关闭遥测 +export DISABLE_TELEMETRY=1 +# 或 +export DO_NOT_TRACK=1 +``` + +> **小贴士:** 在 CI 环境中遥测会自动禁用,无需额外配置。 + +## 相关资源 + +- [技能目录](https://skills.sh) — 发现和浏览公开技能 +- [Agent Skills 规范](https://agentskills.io) — 统一规范文档 +- [Vercel Agent Skills 仓库](https://github.com/vercel-labs/agent-skills) — 官方技能合集 diff --git a/DEV/GO/Channel vs Mutex.md b/DEV/GO/Channel vs Mutex.md new file mode 100644 index 0000000..9b047e7 --- /dev/null +++ b/DEV/GO/Channel vs Mutex.md @@ -0,0 +1,85 @@ +--- +tags: [go, golang, channel, mutex, 并发, 同步] +create time: 2026-04-24 10:30 +--- + +# Channel vs Mutex:为什么 Go 发明了 Channel? + +## 概述 + +Mutex 已经能做同步了,Go 为什么还要发明 Channel?本文从抽象层次、安全性、组合性等角度分析 Channel 相比 Mutex 的核心优势,以及两者的适用场景。 + +## 核心差异:锁"状态" vs 传递"数据" + +Mutex 保护的是**共享状态(数据)**,语义是"互斥访问":拿到锁 → 读写共享变量 → 释放锁。 + +Channel 传递的是**数据本身(消息)**,语义是"同步地传递":发送 → 接收。数据的所有权随 channel 转移,而不是共享。 + +> **核心区别:Mutex 让你"共享内存",Channel 让你"传递内存"。** + +## Channel 相比 Mutex 的四大优势 + +### 1. 数据所有权转移,比"共享访问"更安全 + +```go +// Mutex 模式:数据仍然在外部被共享 +var data []int +mu.Lock() +data = append(data, x) +mu.Unlock() + +// Channel 模式:数据随 channel 转移 +ch <- x // 发送后,main goroutine 就不再持有这份数据 +``` + +Mutex 方案下,任何持有 mu 的 goroutine 都能读写 data —— 必须在每一处都正确加锁。Channel 方案下,**数据发出去就归接收方所有**,不存在"谁在什么时候访问"的问题。 + +### 2. select + 多路复用:Mutex 做不到 + +```go +select { +case v := <-ch1: + // 处理 ch1 +case v := <-ch2: + // 处理 ch2 +case <-time.After(timeout): + // 超时 +} +``` + +用 Mutex 无法实现"从多个通道中等待任意一个就绪"的模式。 + +### 3. 类型层面的方向约束 + +```go +func processor(ch <-chan int) { ... } // 只能收,不能发 +func producer(ch chan<- int) { ... } // 只能发,不能收 +``` + +在编译期就明确了数据流向。Mutex 做不到这一点 —— `*sync.Mutex` 不告诉你它保护的是哪个变量。 + +### 4. 同步 + 通信,一举两得 + +Mutex 只做同步(互斥),不传递数据。Channel 同时完成**同步**(发送阻塞到接收就绪)和**通信**(传递数据值)。 + +## 什么时候该用 Mutex? + +Channel 不是万能药,Mutex 也有它的价值: + +| 场景 | 推荐 | +|------|------| +| 保护局部共享状态(struct 多个字段被并发读写) | **Mutex** | +| 高频访问的共享计数器、缓存 | **Mutex**(性能更好) | +| goroutine 间的消息传递、管道组合 | **Channel** | +| 多路复用、超时控制、取消传播 | **Channel** | + +## 两者配合使用 + +Channel 内部实现本身就用了 mutex 来保护缓冲区操作,说明它们是**不同抽象层次的互补工具**,而非互斥关系。实际 Go 代码中经常配合使用: + +- 用 Mutex 保护 Channel 的元数据 +- 用 Channel 做 goroutine 间通信,用 Mutex 保护内部共享状态 + +## 关联笔记 + +- [[DEV/GO/Channel详解]] diff --git a/DEV/GO/Channel详解.md b/DEV/GO/Channel详解.md new file mode 100644 index 0000000..f125d04 --- /dev/null +++ b/DEV/GO/Channel详解.md @@ -0,0 +1,333 @@ +--- +tags: [go, golang, runtime, channel, 并发, sync] +create time: 2026-04-24 10:00 +--- + +# Channel 详解 + +## 概述 + +Channel 是 Go 中最核心的并发原语之一,遵循 **"不要通过共享内存来通信,而是通过通信来共享内存"** 的设计哲学。它是 goroutine 之间传递数据的同步机制,也是 Go 并发编程的灵魂。 + +> **思考**:既然有 Mutex 可以做同步,为什么还要发明 Channel?Channel 相比 Mutex 的抽象层次有什么优势? +> 详见 [[DEV/GO/Channel vs Mutex]]。 + +Channel 的本质是一个**线程安全的 FIFO 队列**,支持三种操作:**发送**、**接收**、**关闭**。 + +## Channel 的基础类型 + +```go +// 声明三种 channel +var ch1 chan int // nil channel(未初始化) +var ch2 chan int = make(chan int) // 无缓冲 channel +var ch3 chan int = make(chan int, 10) // 有缓冲 channel,容量 10 +``` + +## 无缓冲 Channel + +无缓冲 channel 的 `len() == 0`、`cap() == 0`。 + +**核心语义:发送和接收必须同时完成,是同步的。** + +```go +func main() { + ch := make(chan int) // 无缓冲 + + go func() { + ch <- 42 // 发送:如果没人接收,会阻塞 + fmt.Println("sent 42") + }() + + val := <-ch // 接收:如果没人发送,会阻塞 + fmt.Println("received:", val) // 输出: received: 42 +} +``` + +**无缓冲 channel 的行为图解**: + +```mermaid +sequenceDiagram + participant Sender as 发送方 goroutine + participant Channel as 无缓冲 channel + participant Receiver as 接收方 goroutine + + Sender->>Channel: ch <- 42 + Note over Sender,Channel: 双方同时就绪 + Channel->>Receiver: 传递 42 + Receiver->>Channel: <-ch 接收完成 + Note over Receiver,Channel: 发送和接收同时完成 + Sender->>Sender: 继续执行 + Receiver->>Receiver: 继续执行 +``` + +> **关键点**:无缓冲 channel 实现了 goroutine 之间的**同步屏障**。发送方阻塞到接收方 ready,接收方阻塞到发送方 ready。 + +## 有缓冲 Channel + +有缓冲 channel 的 `len()` 是当前元素数量,`cap()` 是缓冲区容量。 + +**核心语义:缓冲区未满时发送不阻塞,缓冲区非空时接收不阻塞。** + +```go +func main() { + ch := make(chan int, 3) // 缓冲容量 3 + + ch <- 1 // len=0→1, 不阻塞(缓冲区有空位) + ch <- 2 // len=1→2, 不阻塞 + ch <- 3 // len=2→3, 不阻塞(缓冲区满) + + fmt.Println(len(ch)) // 输出: 3 + fmt.Println(cap(ch)) // 输出: 3 + + // 缓冲区已满,下一条发送会阻塞 + // ch <- 4 // ❌ 阻塞! + + val := <-ch // len=3→2, 不阻塞 + fmt.Println("received:", val) // 输出: received: 1 + fmt.Println(len(ch)) // 输出: 2 +} +``` + +**缓冲区的内部结构**: + +```mermaid +graph LR + subgraph Buffer["channel 缓冲区 (cap=4)"] + B1[元素 1] + B2[元素 2] + B3[元素 3] + B4[空位] + end + + subgraph Send["发送方"] + S["ch <- 4"] + end + + subgraph Recv["接收方"] + R["<- ch"] + end + + S -->|"缓冲区未满,直接写入"| Buffer + Buffer -->|"缓冲区非空,直接读取"| R + + classDef buf fill:#e3f2fd,stroke:#1565c0 + classDef send fill:#fff3e0,stroke:#e65100 + classDef recv fill:#e8f5e9,stroke:#2e7d32 + class Buffer buf + class S send + class R recv +``` + +### len() vs cap() + +| 属性 | 含义 | 示例 | +|------|------|------| +| `len(ch)` | 当前缓冲区中的元素数量 | `0 ~ cap` 之间变化 | +| `cap(ch)` | 缓冲区的最大容量 | 创建时确定,不可改变 | + +> **思考**:`len()` 和 `cap()` 分别在什么时机更新?如果一个 goroutine 在发送,另一个在接收,`len()` 会正确反映当前值吗? + +## Channel 的关闭 + +```go +ch := make(chan int, 3) + +ch <- 1 +ch <- 2 + +close(ch) // 关闭 channel + +// 关闭后还能接收已发送的数据 +val, ok := <-ch // val=1, ok=true +val, ok := <-ch // val=2, ok=true +val, ok := <-ch // val=0 (零值), ok=false // 缓冲区空了,收到零值 + +// 从关闭的 channel 接收,ok 始终为 false +val, ok := <-ch // val=0, ok=false +``` + +**close() 的规则**: + +| 操作 | 合法? | 后果 | +|------|--------|------| +| `close(ch)` — 正常关闭 | ✅ | 接收方可以读完残留数据,后续接收返回零值和 `false` | +| 向已关闭的 channel 发送 | ❌ | panic: send on closed channel | +| 关闭 nil channel | ❌ | panic: close of nil channel | +| 关闭已关闭的 channel | ❌ | panic: close of closed channel | + +> **核心原则**:**发送方负责关闭 channel,接收方不负责关闭。** 多个发送方场景下,建议用 `sync.Once` 确保只关闭一次。 + +## nil Channel + +nil channel 是一个未初始化的 channel(值为 `nil`)。 + +```go +var ch chan int // nil channel + +// 向 nil channel 发送 → 永远阻塞 +// ch <- 1 // ❌ 死锁 + +// 从 nil channel 接收 → 永远阻塞 +// <-ch // ❌ 死锁 + +// 关闭 nil channel → panic +// close(ch) // ❌ panic: close of nil channel +``` + +**nil channel 的实际用途**:用于 `select` 中禁用某个分支。 + +```go +func main() { + ch1 := make(chan int) + var ch2 chan int // nil + + select { + case v := <-ch1: + fmt.Println("received from ch1:", v) + case <-ch2: + fmt.Println("from ch2 (never reaches here)") + } + // select 永远阻塞在 ch1 上,ch2 分支被静默禁用 +} +``` + +> **思考**:为什么 nil channel 是"永远阻塞"而不是"立即返回"?这体现了 Go 对 nil 的哪种设计哲学? + +## Channel 死锁场景 + +### 场景 1:向无人读的 channel 写 + +```go +func main() { + ch := make(chan int) + ch <- 42 // 阻塞:没有接收方 + // 输出: all goroutines are asleep - deadlock! +} +``` + +### 场景 2:读无人写的 channel + +```go +func main() { + ch := make(chan int) + <-ch // 阻塞:没有发送方 + // 输出: all goroutines are asleep - deadlock! +} +``` + +### 场景 3:读写 nil channel + +```go +func main() { + var ch chan int // nil + <-ch // 永远阻塞 → 死锁 +} +``` + +### 场景 4:goroutine 中读写已关闭的 channel + +```go +func main() { + ch := make(chan int) + close(ch) + + go func() { + ch <- 1 // panic: send on closed channel + }() + + <-ch // 正常接收 +} +``` + +## for-range 与 Channel + +```go +func main() { + ch := make(chan int, 3) + ch <- 1 + ch <- 2 + close(ch) // 必须关闭,for-range 才知道何时结束 + + for v := range ch { // 自动读取直到 channel 关闭且空 + fmt.Println(v) // 输出: 1, 2 + } +} +``` + +> **思考**:如果不对 channel 调用 `close()`,`for range` 会发生什么?这与无缓冲 channel 的死锁有什么区别? + +## 常见模式 + +### Worker Pool(工作池) + +```go +func worker(id int, jobs <-chan int, results chan<- int) { + for j := range jobs { + results <- j * 2 + } + fmt.Printf("worker %d done\n", id) +} + +func main() { + jobs := make(chan int, 100) + results := make(chan int, 100) + + // 启动 3 个 worker + for w := 1; w <= 3; w++ { + go worker(w, jobs, results) + } + + // 发送任务 + for j := 1; j <= 5; j++ { + jobs <- j + } + close(jobs) // 发送完毕后关闭 + + // 等待结果 + for i := 1; i <= 5; i++ { + <-results + } +} +``` + +### 超时控制 + +```go +select { +case v := <-ch: + fmt.Println("received:", v) +case <-time.After(5 * time.Second): + fmt.Println("timed out after 5s") +} +``` + +## Channel 内部实现要点 + +```go +// Go 源码简化版 channel 结构 +type hchan struct { + buf unsafe.Pointer // 缓冲区(有缓冲时) + elemsize uint16 // 每个元素的大小 + closed uint32 // 是否关闭 + elemtype *type // 元素类型 + sendx uint // 发送索引 + recvx uint // 接收索引 + recvq waitq // 等待接收的 goroutine 队列 + sendq waitq // 等待发送的 goroutine 队列 + + lock mutex // 保护所有字段的互斥锁 +} +``` + +- Channel 是**并发安全**的,内部使用 `mutex` 保护所有操作 +- 无缓冲 channel:直接 sender ↔ receiver 握手(锁粒度极小) +- 有缓冲 channel:先写入缓冲区(`buf` 循环缓冲区),满时才阻塞 +- `recvq` 和 `sendq` 是 **Goroutine 等待队列**(与 GMP 模型联动) + +> **关键理解**:Channel 的锁只保护缓冲区操作本身,不影响 goroutine 的调度——GMP 模型负责调度等待队列中的 goroutine。 + +## 关联笔记 + +- [[DEV/GO/GMP调度模型]] +- [[DEV/GO/Goroutine泄漏排查]] +- [[DEV/GO/内存分配与逃逸分析]] diff --git a/DEV/GO/GC垃圾回收.md b/DEV/GO/GC垃圾回收.md new file mode 100644 index 0000000..350e4a3 --- /dev/null +++ b/DEV/GO/GC垃圾回收.md @@ -0,0 +1,234 @@ +--- +tags: [go, golang, runtime, GC, garbage-collection, 三色标记] +create time: 2026-04-24 10:00 +--- + +# GC 垃圾回收 + +## 概述 + +Go 的垃圾回收(GC)是**自动、并发、不可移动**的标记-清除算法。它的核心目标是:**在保证正确性的前提下,尽可能减少 STW(Stop-The-World)时间**。 + +> **思考**:为什么 Go 选择"不可移动"的 GC 策略?对象不可移动对 GC 的实现和性能分别有什么影响? + +Go GC 的演进: + +| Go 版本 | 特性 | +|---------|------| +| 1.1~1.3 | 半并发 GC(并发标记,STW 终结) | +| 1.5+ | 全并发 GC(并发标记 + 并发清理,STW 仅用于启动和切换) | +| 1.8+ | 混合写屏障(Hybrid Write Barrier),消除扫描阶段的 STW | +| 1.9+ | 更精细的并发控制,STW 时间显著缩短 | + +## 三色标记法 + +三色标记是 Go GC 的核心算法。所有对象被标记为三种颜色之一: + +| 颜色 | 含义 | +|------|------| +| **白色** | 未被标记(可能可达,也可能不可达) | +| **灰色** | 已被标记,但其引用的对象尚未扫描 | +| **黑色** | 已被标记,且其引用的对象已全部扫描完毕 | + +### 标记规则 + +``` +初始:所有对象 = 白色 + +根扫描开始: + - 根对象(全局变量、栈上的引用)标记为灰色 + - 放入灰色队列 + +标记循环(并发执行): + 1. 从灰色队列取出一个灰色对象 → 标记为黑色 + 2. 扫描黑色对象引用的所有对象: + - 如果引用对象是白色 → 标记为灰色,加入灰色队列 + - 如果引用对象是灰色或黑色 → 不做操作 + +终止条件:灰色队列为空 → 标记阶段结束 +``` + +### 三色不变式 + +Go GC 依赖两个不变式来保证正确性: + +``` +强不变式(Strong Invariant): + 黑色对象不能直接引用白色对象 + +弱不变式(Weak Invariant): + 如果黑色对象引用了白色对象, + 那么那个白色对象一定是通过其他灰色对象可达的 +``` + +> **核心原理**:如果一个白色对象被黑色对象引用,但它没有通过灰色对象可达 → 说明这个白色对象**真正不可达**,可以安全回收。 + +```mermaid +flowchart LR + subgraph Roots["根对象 (灰色)"] + R1[灰色对象 R1] + R2[灰色对象 R2] + end + + subgraph Objects["所有对象"] + B1[黑色 B1] + B2[黑色 B2] + W1[白色 W1] + W2[白色 W2] + end + + R1 -->|"引用"| B1 + R1 -->|"引用"| W1 + R2 -->|"引用"| B2 + B1 -->|"引用"| W2 + B2 -->|"引用"| W2 + + classDef gray fill:#ffa726,color:#fff + classDef black fill:#616161,color:#fff + classDef white fill:#eeeeee,stroke:#333 + class R1,R2 gray + class B1,B2 black + class W1,W2 white +``` + +> **思考**:上图中 B2 直接引用了白色 W2,这违反了"黑色对象不能直接引用白色对象"的强不变式。如果此时 GC 终止,W2 就会被误回收——这就是为什么需要写屏障。 + +## 写屏障(Write Barrier) + +写屏障是并发 GC 的核心机制。在并发标记期间,如果用户代码修改了指针,GC 可能来不及追踪这些变化。写屏障确保: + +> **任何指向白色对象的指针写入,都会被记录为指向灰色对象** + +### 插入写屏障(Go 1.5+) + +在指针赋值**之前**检查: + +```go +// 伪代码:插入写屏障逻辑 +func writePointer(field, newVal) { + if newVal 是白色 { + newVal 重新标记为灰色 // 确保白色对象不被误回收 + } +} + +// 实际场景 +*p = newObj // 写屏障在这里介入 +``` + +**插入写屏障的局限**:无法处理删除引用(将指针设为 nil 或指向其他对象)的场景。 + +### 删除写屏障(Go 1.8+) + +```go +// 伪代码:删除写屏障逻辑 +func writePointer(field, newVal) { + oldVal = *field // 旧值 + *field = newVal // 新值 + + // 如果旧值是黑色,且它引用的白色对象可能只通过它可达 + if oldVal 是黑色 and 旧引用对象是白色 { + 旧引用对象重新标记为灰色 // 确保白色对象不被漏掉 + } +} +``` + +### 混合写屏障(Go 1.8+,Go 默认使用) + +**插入写屏障 + 删除写屏障**的组合: + +``` +赋值前检查:新值如果是白色 → 标灰(插入屏障) +赋值后检查:旧值如果是黑色 → 旧引用的白色对象标灰(删除屏障) + +效果:无论指针如何变化,白色对象都不会被漏标 +``` + +```mermaid +flowchart TD + A["🏁 GC 启动 (STW 极短)"] --> B["📝 并发标记阶段"] + B -->|"标记循环\n(灰色队列→黑色)"| C{"灰色队列\n是否为空?"} + C -->|"否"| B + C -->|"是"| D["🛡️ 写屏障介入\n混合写屏障: 插入+删除"] + D --> E["🏁 标记结束 (STW 极短)"] + E --> F["🗑️ 并发清理阶段"] + F --> G["🏁 GC 完成"] + + classDef start fill:#e53935,color:#fff + classDef process fill:#1e88e5,color:#fff + classDef barrier fill:#43a047,color:#fff + classDef done fill:#8e24aa,color:#fff + class A,E start + class B,F process + class D barrier + class G done +``` + +## 标记-清除 vs 标记-复制 + +Go 采用**标记-清除(Mark-Sweep)**而非标记-复制的原因: + +| 维度 | 标记-清除 | 标记-复制 | +|------|-----------|-----------| +| 内存碎片 | 会产生碎片 | 无碎片 | +| 内存利用率 | 50%(需要空半区) | 100%(碎片由整理消除) | +| GC 速度 | 与存活对象数相关 | 与总对象数相关 | +| 对象移动 | 不移动(简化指针) | 需要移动和更新指针 | +| Go 的选择 | ✅ | ❌ | + +> **思考**:既然标记-清除会产生内存碎片,Go 是如何处理的?这与 [内存分配与逃逸分析](./内存分配与逃逸分析.md) 有什么关系? + +## Go 的 GC 触发方式 + +Go 使用**比例触发(Ratelimiting)**而非固定阈值: + +``` +触发条件:距上次 GC 结束后,分配的数据量达到上次 GC 时存活数据量的 P% +P = GCPercent(可通过 GCPercent() 查询) + +Go 1.8+ 目标:单次 GC 的 STW 时间 < 100μs +``` + +```go +import ( + "runtime" + "runtime/debug" +) + +func main() { + // 查询当前 GC 比例 + p := runtime.GCPercent() + fmt.Println("GCPercent:", p) + + // 调整 GC 比例(值越小,GC 越频繁) + debug.SetGCPercent(20) // 默认 100 + + // 强制触发 GC(仅用于测试/调试) + runtime.GC() + + // 查看 GC 统计 + var m runtime.MemStats + runtime.ReadMemStats(&m) + fmt.Printf("GC 次数: %d\n", m.NumGC) + fmt.Printf("分配总量: %d bytes\n", m.Alloc) + fmt.Printf("系统分配: %d bytes\n", m.Sys) +} +``` + +> **关键理解**:`GCPercent` 不是"存活 X% 时触发 GC",而是增量比例——每次 GC 后,系统会根据当前存活量计算触发阈值。 + +## 总结 + +Go GC 的设计哲学: + +- **并发优先**:标记和清理几乎全部并发执行,STW 时间极短 +- **写屏障保障正确性**:混合写屏障确保并发标记的准确性 +- **不可移动**:简化运行时,避免指针更新开销 +- **增量回收**:GC 开销分摊到每次对象分配上 + +> **思考**:为什么 Go 不采用分代 GC(Generational GC)?在什么场景下分代 GC 会有更好的表现? + +## 关联笔记 + +- [[DEV/GO/GMP调度模型]] +- [[DEV/GO/内存分配与逃逸分析]] +- [[DEV/GO/Goroutine泄漏排查]] diff --git a/DEV/GO/GMP调度模型.md b/DEV/GO/GMP调度模型.md new file mode 100644 index 0000000..b86b09e --- /dev/null +++ b/DEV/GO/GMP调度模型.md @@ -0,0 +1,168 @@ +--- +tags: [go, golang, runtime, GMP, goroutine, scheduler] +create time: 2026-04-24 10:00 +--- + +# GMP 调度模型 + +## 概述 + +Go 的并发模型建立在 **GMP 调度架构**之上。与传统的 OS 线程 1:1 模型不同,Go 引入了 G(Goroutine)、M(Machine/Worker)、P(Processor)三层抽象,实现了高效的**用户态调度**。 + +> **思考**:如果 Go 直接使用 OS 线程(1:1 模型),同时启动百万级 goroutine 会发生什么?内存开销和上下文切换代价分别有多大? + +GMP 的核心目标是:**用最小的调度开销,最大化多核 CPU 的并行利用率**。 + +## G — Goroutine(协程) + +G 是 Go 并发执行的最小单元——协程。它的核心特征: + +- **初始栈空间 2KB**,可动态伸缩(最大可达 1GB) +- 栈的伸缩采用**分段扩展**策略(segment),每次增长时翻倍 +- 每个 G 包含:程序计数器(PC)、寄存器状态、栈指针、等待队列指针 + +```go +// 创建 goroutine 的代价极低 +go func() { + // 这个匿名函数运行在一个独立的协程中 + fmt.Println("running in goroutine") +}() + +// 对比:创建 OS 线程的代价 +// runtime/debug.SetThreadThreadsLimit() 限制线程数 +// 通常 OS 线程栈默认 1~8MB,且不可动态伸缩 +``` + +> **为什么 2KB 起步?** 大多数函数不需要很大的栈空间。小默认值让创建海量协程成为可能。 + +## M — Machine(执行器) + +M 代表**工作线程**,绑定一个 OS 线程,真正执行 G 的代码。 + +- **M : OS Thread = 1 : 1**,M 是 G 的执行载体 +- M 负责执行 G 的代码,同时参与 GMP 调度循环 +- 当 G 阻塞(如网络 IO、channel 操作)时,M 会被 P 释放去执行其他 G + +```go +// 限制 Go 使用的 OS 线程数,理解 M 的规模 +// GOPTHREADS=1000 go run main.go + +// runtime.NumGoroutine() 返回 goroutine 数量 +// runtime.NumCPU() 返回逻辑 CPU 数量 +// M 的数量默认是 GOMAXPROCS(默认 NumCPU) +``` + +## P — Processor(处理器) + +P 是**调度器本地状态**的抽象,可以理解为一个"局部调度器"。 + +- P 维护一个**本地 goroutine 队列**(runq),最多缓存 256 个待执行的 G +- P 的数量由 `GOMAXPROCS` 控制(Go 1.5+ 默认等于 CPU 逻辑核心数) +- **每个时刻,一个 P 只能绑定一个 M** 来执行代码 + +P 的核心职责: + +| 职责 | 说明 | +|------|------| +| 本地调度 | 从自己的 runq 取出 G 分配给绑定的 M 执行 | +| 工作窃取(Work Stealing)| 当自己的队列为空时,从其他 P 的队列中"偷"一半的 G | +| 全局队列 | 所有 P 共享一个全局 G 队列(最多容纳 64 个),用于负载均衡 | +| 系统调用处理 | 当绑定的 M 进入系统调用时,P 会"脱落"(handoff),找一个空闲 M 绑定 | + +## GMP 调度循环 + +```mermaid +flowchart TD + subgraph GQueue["G 队列"] + G1["G1 待执行"] + G2["G2 待执行"] + G3["G3 待执行"] + G4["G4 待执行"] + G5["G5 待执行"] + end + + subgraph GlobalQ["全局队列 (最多64个)"] + GQ["Global run queue"] + end + + subgraph LocalQ["P 本地队列 (最多256个)"] + PQL["P local runq"] + end + + subgraph MExec["M 执行 G"] + ME["执行 G 的代码"] + end + + subgraph OSys["操作系统"] + NET["网络 IO / 系统调用"] + SYS["系统调用 - 阻塞"] + end + + GQueue --> |入队| GlobalQ + GlobalQ --> |P 空闲时取| PQL + PQL --> |分配给 M| ME + ME --> |执行完 G| ME + ME --> |新建 G| PQL + ME --> |阻塞 channel/IO | NET + ME --> |系统调用| SYS + + SYS --> |P 脱落, 另找 M| PQL + NET --> |完成阻塞| PQL + + PQL --> |队列太长, 移到全局| GQ + GQ --> |工作窃取: 从其他 P 偷一半| PQL + + classDef queue fill:#e3f2fd,stroke:#1565c0 + classDef exec fill:#fff3e0,stroke:#e65100 + classDef sys fill:#fce4ec,stroke:#c62828 + class GlobalQ,PQL queue + class ME exec + class NET,SYS sys +``` + +**调度流程**: + +1. **新建 G** → 先尝试放入 P 的本地队列(`runq`),满了则放入全局队列 +2. **P 取 G** → 从本地队列取(优先),本地为空时从全局队列取,再空则"工作窃取"其他 P +3. **M 执行 G** → M 拿着 P 分配的 G 执行代码 +4. **G 阻塞** → G 进入等待队列,M 继续执行下一个 G,P 寻找新的 M 绑定 +5. **G 恢复** → G 被放回到某个 P 的队列中,等待下次被调度 + +## 为什么不用 M:N 模型? + +早期 Go 讨论过 M:N 模型(N 个 G 映射到 M 个 OS 线程),最终选择了 G:M ≈ 1:1 + P 用户态调度,原因: + +| 对比维度 | M:N 模型 | Go 的 GMP 模型 | +|----------|----------|----------------| +| 系统调用阻塞 | M 被阻塞,N 个 G 全部卡住 | G 阻塞,M 脱手,P 找新 M | +| 抢占式调度 | 需要内核协作 | 用户态即可,P 完全控制 | +| 实现复杂度 | 极高(需要内核干预) | 相对简洁 | +| Go 版本 | 1.1 之前实验过 | 1.1+ 确定方案 | + +> **关键点**:Go 1.5 引入 GOMAXPROCS = NumCPU,每个 P 绑定一个逻辑 CPU 核心,实现了真正的并行执行。 + +## 关键参数 + +```go +// 获取和设置 GOMAXPROCS +import "runtime" + +func main() { + n := runtime.GOMAXPROCS(0) // 获取当前值 + runtime.GOMAXPROCS(4) // 设置为 4 + fmt.Println("M 的最大并行度:", n) +} + +// 其他 runtime 参数 +fmt.Println("Goroutine 数量:", runtime.NumGoroutine()) +fmt.Println("逻辑 CPU 数量:", runtime.NumCPU()) +``` + +> **思考**:`GOMAXPROCS=1` 时,Go 还是并行的吗?还是只是并发? + +## 关联笔记 + +- [[DEV/GO/GC垃圾回收]] +- [[DEV/GO/Channel详解]] +- [[DEV/GO/内存分配与逃逸分析]] +- [[DEV/GO/Goroutine泄漏排查]] diff --git a/DEV/GO/Goroutine泄漏排查.md b/DEV/GO/Goroutine泄漏排查.md new file mode 100644 index 0000000..31f89c2 --- /dev/null +++ b/DEV/GO/Goroutine泄漏排查.md @@ -0,0 +1,355 @@ +--- +tags: [go, golang, runtime, goroutine, 泄漏, 并发, channel, waitgroup] +create time: 2026-04-24 10:00 +--- + +# Goroutine 泄漏排查 + +## 概述 + +Goroutine 泄漏是指 goroutine 被创建后,由于代码逻辑问题,**永远无法结束**,导致其占用的栈内存和其他资源无法回收。Goroutine 是泄漏检测的"隐形杀手"——它不会像进程那样消耗 PID,也不会像内存泄漏那样有明显的 `Out Of Memory` 错误,而是表现为 `runtime.NumGoroutine()` 持续增长。 + +> **思考**:如果一个 goroutine 泄漏了,它会一直占用内存吗?goroutine 初始栈只有 2KB,泄漏 10 万个 goroutine 会占用多少内存? + +## 泄漏场景与排查 + +### 场景 1:向无人读的 channel 写 + +```go +func producer(ch chan<- int) { + for i := 0; ; i++ { + ch <- i // 如果没有接收方,goroutine 永远阻塞 + } +} + +func main() { + ch := make(chan int) + go producer(ch) // 泄漏!没有人从 ch 读取 + time.Sleep(1 * time.Second) + fmt.Println("goroutines:", runtime.NumGoroutine()) // 至少 2 个 +} +``` + +**修复方式**:确保有对应的接收方,或者在适当的时候关闭 channel。 + +```go +func main() { + ch := make(chan int) + go func() { + for i := 0; i < 10; i++ { + ch <- i // 发送有限数量的数据 + } + close(ch) // 发送完毕后关闭 + }() + + // 用 range 消费,channel 关闭后自动退出 + for v := range ch { + fmt.Println(v) + } +} +``` + +### 场景 2:读无人写的 channel + +```go +func main() { + ch := make(chan int) + val := <-ch // 永远阻塞,goroutine 泄漏 + fmt.Println(val) +} +``` + +**修复方式**:确保有发送方,或者用 `select` 加超时。 + +```go +func main() { + ch := make(chan int) + + select { + case val := <-ch: + fmt.Println(val) + case <-time.After(5 * time.Second): + fmt.Println("timeout, no data received") + } +} +``` + +### 场景 3:读写 nil channel + +```go +func main() { + var ch chan int // nil + + // 以下任何一行都会导致永久阻塞 + // ch <- 1 // 向 nil channel 发送 → 永远阻塞 + // <-ch // 从 nil channel 接收 → 永远阻塞 + // close(ch) // 关闭 nil channel → panic +} +``` + +**修复方式**:在使用前确保 channel 已初始化。 + +```go +func main() { + var ch chan int + + if someCondition { + ch = make(chan int) + } + + select { + case ch <- 1: + fmt.Println("sent") + default: + fmt.Println("channel not initialized, skipped") + } +} +``` + +### 场景 4:WaitGroup 计数错误 + +```go +func main() { + var wg sync.WaitGroup + + // ❌ 错误:在 goroutine 外部调用 wg.Add(1) 是正确的 + // 但在 goroutine 内部调用 Add(1) 会导致 panic + // 或者忘记调用 wg.Done() + + wg.Add(1) + go func() { + defer wg.Done() // 如果 goroutine 提前 return 忘记调用 Done → 泄漏 + for { + // 没有退出条件 → goroutine 永远不会结束 + } + }() + + wg.Wait() // 永远等待 +} +``` + +**修复方式**:确保每个 `Add(1)` 都有对应的 `Done()`,且 goroutine 有明确的退出条件。 + +```go +func main() { + var wg sync.WaitGroup + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-ctx.Done(): + return // 有退出条件 + default: + // 正常处理 + } + } + }() + + time.Sleep(1 * time.Second) + cancel() // 触发退出 + wg.Wait() +} +``` + +### 场景 5:sync.Mutex 拿锁未释放 + +```go +var mu sync.Mutex + +func leaky() { + mu.Lock() + // 如果这里 panic 或者永久阻塞 + // mu.Unlock() 永远不会执行 → 所有等待 mu.Lock() 的 goroutine 泄漏 + panic("boom") // mu 永远不会被释放 +} + +func main() { + go leaky() // 拿锁后 panic,锁不释放 + go func() { + mu.Lock() // 永远等不到锁 → goroutine 泄漏 + mu.Unlock() + }() + + time.Sleep(1 * time.Second) + fmt.Println(runtime.NumGoroutine()) // 持续增加 +} +``` + +**修复方式**:确保在任何路径下都能释放锁。 + +```go +func safe() { + mu.Lock() + defer mu.Unlock() // 即使 panic,defer 也会执行 + panic("boom") +} +``` + +### 场景 6:select 中没有默认分支 + +```go +func main() { + ch := make(chan int) + + // 如果 ch 没有数据,也没有其他 case 能就绪 + // goroutine 会永远阻塞在 select 上 + select { + case <-ch: + fmt.Println("received") + // 缺少 default 和 time.After → 永远等待 + } +} +``` + +## 泄漏排查工具 + +### 1. pprof goroutine profile + +```bash +# 方法一:导入 net/http/pprof +import _ "net/http/pprof" + +# 方法二:使用 runtime/pprof +import ( + "runtime" + "runtime/pprof" + "os" +) + +func dumpGoroutines() { + f, _ := os.Create("goroutine.prof") + pprof.WriteProfile(f) + f.Close() +} +``` + +```bash +# 分析 goroutine 泄漏 +go tool pprof goroutine.prof + +# 进入交互界面后执行: +top # 查看最顶层的 goroutine +top -cum # 按累积时间排序 +list main.leaky # 查看具体函数的 goroutine 阻塞位置 +``` + +### 2. 监控 NumGoroutine + +```go +import ( + "runtime" + "time" +) + +func monitorGoroutines() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for range ticker.C { + n := runtime.NumGoroutine() + if n > 100 { // 设定阈值 + fmt.Printf("⚠️ High goroutine count: %d\n", n) + // dump stack + buf := make([]byte, 1<<20) + n := runtime.Stack(buf, true) + fmt.Printf("Goroutine stacks:\n%s\n", buf[:n]) + } + } +} +``` + +### 3. pprof 常用命令速查 + +```bash +# 启动 web UI(推荐) +go tool pprof -http=:8080 http://localhost:6060/debug/pprof/goroutine?debug=1 + +# 文本模式 +go tool pprof -text goroutine.prof + +# 查看具体函数阻塞位置 +go tool pprof -list=main.worker goroutine.prof +``` + +## 预防最佳实践 + +| 实践 | 说明 | +|------|------| +| **每个 goroutine 必须有退出条件** | 用 `context.Context` 控制生命周期 | +| **Channel 用完必须关闭** | 发送方负责关闭,防止接收方永远等待 | +| **WaitGroup 配对使用** | `Add` 和 `Done` 一一对应,用 `defer` 确保 `Done` 执行 | +| **Mutex 用 defer 释放** | `defer mu.Unlock()` 防止 panic 导致锁不释放 | +| **select 加超时** | 永远不要 `select` 中只有阻塞 channel 而无 `default`/`timeout` | +| **启动时加监控** | 定期采样 `runtime.NumGoroutine()`,异常时 dump stack | +| **context 传递取消信号** | 让所有 goroutine 都能响应取消 | + +```go +// ✅ 推荐的 goroutine 模板 +func worker(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + for { + select { + case <-ctx.Done(): + return // 正常退出 + default: + // 执行任务 + } + } +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var wg sync.WaitGroup + for i := 0; i < 10; i++ { + wg.Add(1) + go worker(ctx, &wg) + } + + wg.Wait() +} +``` + +## 排查流程图 + +```mermaid +flowchart TD + A["发现 goroutine 持续增长"] --> B{"NumGoroutine 持续增长?"} + B -->|"是"| C["go tool pprof goroutine profile"] + C --> D["top / top -cum 查看类型分布"] + D --> E{"泄漏的 goroutine 类型?"} + + E -->|"chan send / chan receive"| F["检查 channel 发送方/接收方配对"] + F --> G["确保有对应的读取/写入"] + G --> H["使用 select + timeout 防止永久等待"] + + E -->|"sync.Mutex lock"| I["检查 mutex 是否被正确释放"] + I --> J["所有路径都使用 defer Unlock"] + + E -->|"sleep / IO"| K["检查是否有阻塞的 IO 操作"] + K --> L["确保有超时和取消机制"] + + E -->|"goroutine 数量少且稳定"| M["可能是正常的工作 goroutine"] + M --> N["确认是否为预期行为"] + + B -->|"否"| O["没有泄漏,goroutine 数量正常"] + + classDef leak fill:#e53935,color:#fff + classDef fix fill:#43a047,color:#fff + classDef normal fill:#1e88e5,color:#fff + class C,D,F,I,K,M,O normal + class G,H,J,L fix + class A leak +``` + +## 关联笔记 + +- [[DEV/GO/GMP调度模型]] +- [[DEV/GO/Channel详解]] +- [[DEV/GO/GC垃圾回收]] diff --git a/DEV/GO/内存分配与逃逸分析.md b/DEV/GO/内存分配与逃逸分析.md new file mode 100644 index 0000000..38cc691 --- /dev/null +++ b/DEV/GO/内存分配与逃逸分析.md @@ -0,0 +1,325 @@ +--- +tags: [go, golang, runtime, 内存分配, 逃逸分析, 虚拟内存] +create time: 2026-04-24 10:00 +--- + +# 内存分配与逃逸分析 + +## 概述 + +Go 的内存管理分为两个层面:**运行时的内存分配**(由 Malloc 实现)和**编译期的逃逸分析**(决定变量分配在栈还是堆)。理解这两个机制对编写高性能 Go 代码至关重要。 + +> **思考**:为什么大多数语言把局部变量分配在栈上,而把 new/malloc 出来的分配在堆上?Go 的逃逸分析打破了这个规则——它如何让编译器决定分配位置? +> +> 详见 → [[DEV/GO/内存分配与逃逸分析/为什么Go打破栈堆分工规则.md]] + +## 多级存储模型 + +计算机的存储层次决定了数据的访问速度,距离 CPU 越近越快: + +```mermaid +graph LR + subgraph Fast["速度快 / 容量小 / 昂贵"] + REG[寄存器] + L1["L1 缓存 (~30ns)"] + L2["L2 缓存 (~100ns)"] + L3["L3 缓存 (~300ns)"] + end + + subgraph Slow["速度慢 / 容量大 / 便宜"] + MEM["物理内存 (~100ns)"] + DISK["磁盘 / SSD"] + SWAP["Swap 区域"] + end + + REG --> L1 --> L2 --> L3 --> MEM --> SWAP --> DISK + + classDef fast fill:#4caf50,color:#fff + classDef slow fill:#ff9800,color:#fff + class REG,SWAP fast + class L1,L2,L3,MEM slow + class DISK slow +``` + +> **核心认知**:CPU 寄存器的访问速度是纳秒级,而内存是百纳秒级,磁盘是毫秒级。三者相差百万倍。 + +### 虚拟内存的好处 + +Go 程序运行在**虚拟内存**之上,每个进程拥有独立的虚拟地址空间(64 位系统为 128TB)。 + +> **拓展**:Go 的虚拟内存使用方式和其他语言有什么不同?详见 → [[DEV/GO/内存分配与逃逸分析/虚拟内存与各语言对比.md]] + +| 特性 | 说明 | +|------|------| +| **地址隔离** | 每个进程的虚拟地址空间互不影响 | +| **页面机制** | 内存按页(4KB)管理,未使用的页面不占用物理内存 | +| **SWAP 冷热切换** | 不常用的页(冷页)换出到磁盘,常用的页(热页)留在内存 | + +> **思考**:SWAP 机制让"冷热动态切换"成为可能。这为什么是操作系统最精妙的设计之一?如果所有数据都常驻物理内存,会发生什么? + +虚拟内存的好处: + +1. **无需物理连续**:虚拟地址可以是连续的,物理页可以分散 +2. **按需分配**:`malloc` 并不真正分配物理内存,只是映射虚拟地址 +3. **冷热分页**:频繁访问的页常驻内存,低频页换出到 Swap + +## Golang 内存模型:三级缓存架构 + +Go 的内存分配器采用**三级缓存 + 中央池 + 大对象分配**的分层架构,详见 → [[DEV/GO/内存分配与逃逸分析/三级缓存架构.md]] + +```mermaid +flowchart TD + subgraph ThreadLocal["线程局部 (无锁)"] + MCACHE["mcache per-P\n每 P 一个 mcache"] + end + + subgraph CentralPool["中央池 (细粒度锁)"] + MCENTRAL["mcentral\n固定大小的对象池"] + end + + subgraph Heap["堆 (全局重锁)"] + MHEAP["mheap\n向 OS 申请内存"] + end + + subgraph OS["操作系统"] + MEM["mmap 映射虚拟内存\n默认 64MB 起始"] + end + + MCACHE -->|"缓存不够时"| MCENTRAL + MCENTRAL -->|"所有规格都空时"| MHEAP + MHEAP -->|"mmap 向 OS 申请"| MEM + + classDef local fill:#4caf50,color:#fff + classDef central fill:#2196f3,color:#fff + classDef heap fill:#ff9800,color:#fff + classDef os fill:#9e9e9e,color:#fff + class MCACHE local + class MCENTRAL central + class MHEAP heap + class MEM os +``` + +### 第一级:mcache(线程级,无锁) + +每个 P 拥有独立的 `mcache`,是 Goroutine 最近的内存分配点。 + +- **无锁设计**:因为每个 P 独占自己的 mcache,无需竞争 +- **缓存固定大小的对象**:每种大小类(size class)缓存一个 object 链表 +- Go 定义了 **67 种对象大小类别**,覆盖 0 ~ 32KB 的对象 + +``` +mcache 内部结构(简化): + mcache.span[class0] → 缓存大小为 8 字节对象的 span + mcache.span[class1] → 缓存大小为 16 字节对象的 span + ... + mcache.span[class66] → 缓存大小为 32768 字节对象的 span +``` + +### 第二级:mcentral(池级,细粒度锁) + +`mcentral` 管理多种规格(span class)的内存池,每种规格有独立的 `mspan`。 + +- **按大小分组**:同样大小的对象放在同一个 `mspan` 中 +- **细粒度锁**:每种规格的 `mspan` 有独立的锁,而非全局一把大锁 +- 当 mcache 的缓存不够时,向 mcentral 请求新的 span + +``` +mcentral 内部结构(简化): + mcentral.spans[class0] → mspan (管理大小为 8 字节的对象) + mcentral.spans[class1] → mspan (管理大小为 16 字节的对象) + ... +``` + +### 第三级:mheap(堆级,全局锁) + +`mheap` 是整个程序的"大仓库",直接向操作系统申请内存。 + +- **全局锁保护**:申请新内存时需要获取全局锁 +- **mmap 映射**:通过 `mmap` 向 OS 请求内存(初始约 64MB,可增长) +- 分配 span 给 mcentral,形成完整的分配链路 + +``` +mheap 内部结构(简化): + mheap.all[] → 管理所有 span 的数组 + mheap.pageAlloc → 位图,标记每个页面是否已分配 +``` + +### 大对象分配(>32KB) + +超过 32KB 的对象**跳过 mcache 和 mcentral**,直接从 mheap 分配: + +```go +// 超过 32KB 的对象直接走 mheap 路径 +func benchmarkLargeAlloc(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = make([]byte, 64*1024) // 64KB,直接走 mheap + } +} + +// 小于 32KB 的对象走 mcache 路径(更快) +func benchmarkSmallAlloc(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = make([]byte, 1024) // 1KB,走 mcache 路径 + } +} +``` + +> **思考**:为什么 32KB 是一个分界线?这与 CPU 缓存行(cache line,通常 64 字节)和页大小(4KB)有什么关系? + +## 锁粒度对比 + +| 层级 | 锁粒度 | 竞争程度 | 适用场景 | +|------|--------|----------|----------| +| **mcache** | 无锁(每个 P 独立) | 零竞争 | 频繁的小对象分配 | +| **mcentral** | 按大小类细粒度锁 | 低竞争 | 中等频率分配 | +| **mheap** | 全局锁 | 高竞争 | 大对象分配、扩容 | + +> **设计精妙之处**:Go 通过分层 + 无锁设计,让最常见的内存分配(小对象、同一 P 内)完全避免锁竞争。 + +## 逃逸分析 + +逃逸分析是**编译器在编译期进行的静态分析**,决定变量分配在**栈**还是**堆**上。 + +``` +变量分配策略: + - 栈:函数返回后自动释放,零 GC 开销 + - 堆:GC 管理生命周期,有 GC 开销但可超越函数作用域 + +逃逸规则(简化版): + 如果变量的生命周期在函数返回后仍然需要 → 逃逸到堆 + 否则 → 留在栈上 +``` + +### 逃逸场景 + +#### 场景 1:闭包引用局部变量 + +```go +func main() { + x := 10 // 逃逸到堆!因为匿名函数在 main 返回后仍可能运行 + f := func() int { + return x // 闭包持有对 x 的引用 + } + fmt.Println(f()) +} +``` + +编译时 `go build -gcflags="-m"` 可以看到: + +``` +go build -gcflags="-m" main.go +# 输出: +# ./main.go:4:2: x escapes to heap +``` + +#### 场景 2:返回局部变量的指针 + +```go +func newInt() *int { + x := 10 // 逃逸到堆!返回了 &x + return &x // 如果留在栈上,函数返回后 x 就被销毁了 +} +``` + +#### 场景 3:空接口(interface{}) + +```go +func main() { + x := 100 + var i interface{} = x // x 逃逸到堆! + // 因为 interface{} 是动态类型,编译器无法在编译期确定 x 的生命周期 +} +``` + +#### 场景 4:切片长度在运行时才确定 + +```go +func main() { + n := 10 + s := make([]int, n) // n 是运行时确定的 → 切片逃逸到堆 + // 如果长度是常量:make([]int, 10) → 小切片可能留在栈上 +} +``` + +#### 场景 5:接口参数传递 + +```go +func process(v interface{}) { + // 参数 v 传入时,其底层数据可能逃逸 + _ = v +} + +func main() { + x := 42 + process(x) // x 可能逃逸 +} +``` + +### 逃逸分析实战 + +```bash +# 查看逃逸分析结果 +go build -gcflags="-m" ./... + +# 更详细的输出 +go build -gcflags="-m -m" ./... +``` + +```go +package main + +func main() { + // 不逃逸 - 分配在栈上 + x := 10 + + // 逃逸 - 分配在堆上 + p := &x // 取地址 → x 逃逸 + _ = *p +} +``` + +编译输出: + +``` +./main.go:7:2: &x escaped to heap +``` + +### 栈 vs 堆对比 + +| 维度 | 栈 | 堆 | +|------|-----|-----| +| 分配速度 | 极快(只需移动栈指针) | 较慢(需要锁和内存管理) | +| 释放速度 | 自动(函数返回时栈帧弹出) | GC 回收(有延迟) | +| GC 开销 | 无 | 有 | +| 大小限制 | 有限(goroutine 栈初始 2KB) | 几乎无限(受限于虚拟内存) | +| 生命周期 | 函数作用域内 | 可超越函数作用域 | + +> **优化建议**:尽量减少逃逸到堆的变量数量,可以让 GC 负担更轻。 + +```go +// ❌ 不推荐:不必要的指针传递导致逃逸 +func getData() *Data { + return &Data{Name: "test"} // Data 逃逸到堆 +} + +// ✅ 推荐:值传递,避免不必要的指针 +func getData() Data { + return Data{Name: "test"} // 小结构体留在栈上 +} +``` + +## 总结 + +Go 内存管理的核心设计理念: + +1. **分层分配**:mcache(无锁)→ mcentral(细锁)→ mheap(全局锁),按需降级 +2. **大对象快速通道**:>32KB 的对象跳过中间层,直接走 mheap +3. **编译期逃逸分析**:尽可能让变量留在栈上,减少 GC 压力 +4. **虚拟内存抽象**:按需映射,冷热分页,让程序拥有 128TB 地址空间 + +## 关联笔记 + +- [[DEV/GO/GMP调度模型]] +- [[DEV/GO/GC垃圾回收]] +- [[DEV/GO/Channel详解]] +- [[DEV/GO/Goroutine泄漏排查]] diff --git a/DEV/GO/内存分配与逃逸分析/三级缓存架构.md b/DEV/GO/内存分配与逃逸分析/三级缓存架构.md new file mode 100644 index 0000000..536423a --- /dev/null +++ b/DEV/GO/内存分配与逃逸分析/三级缓存架构.md @@ -0,0 +1,137 @@ +--- +tags: [go, golang, runtime, 内存分配, mcache, mcentral, mheap] +create time: 2026-04-24 10:30 +--- + +# 三级缓存架构详解 + +## 概述 + +Go 的内存分配器采用 **三级缓存 + 中央池 + 大对象快速通道** 的分层架构,核心理念是"让最常见的操作最快"。本文通过生活化类比和底层实现两层视角,帮你彻底理解这个设计。 + +## 生活化类比:连锁便利店的仓储体系 + +### 🏬 个人抽屉 → mcache + +每个店长(P / Goroutine)都有自己的抽屉,里面常备着最常用的商品(固定大小的小物件)。 + +- **不需要请示任何人**,直接伸手拿,零等待 +- 抽屉空间有限,装不了太多 +- 90% 的日常需求在这里就满足了 + +### 📦 门店后仓 → mcentral + +店长的抽屉快空了,去后仓拿。 + +- 后仓按**商品类别**整齐排列(8 字节的放一起、16 字节的放一起……共 67 个类别) +- 拿的时候可能需要稍等(要拿钥匙 / 找店员,对应细粒度锁) +- 容量比抽屉大得多 + +### 🏭 区域总仓 → mheap + +门店后仓也空了?打电话给总仓库。 + +- 总仓库什么都有,容量巨大 +- 但**手续繁琐**(全局锁,要等) +- 只有实在缺货时才走这条路 + +### 🚚 超大订单(>32KB)→ 专车直送 + +如果你要订 1000 箱货(大对象),店长懒得经手中转,**直接从总仓库专车送到你店里**,跳过抽屉和后仓。 + +## 为什么这么设计? + +核心思想就是:**让最常见的操作最快**。 + +| 场景 | 占比 | 处理方式 | 等待时间 | +|------|------|---------|---------| +| 小物件、同一个 goroutine | ~90% | 抽屉直接拿 | 0 | +| 偶尔缺货 | ~9% | 去后仓拿 | 轻微 | +| 真正缺大牌货 | ~1% | 找总仓库 | 明显 | + +对比一下,如果所有东西都要去总仓库拿(比如某些语言的每次 `malloc` 都全局加锁),那整个体系就**堵死了**。 + +## 底层实现 + +### 第一级:mcache(线程级,无锁) + +每个 P 拥有独立的 `mcache`,是 Goroutine 最近的内存分配点。 + +- **无锁设计**:因为每个 P 独占自己的 mcache,无需竞争 +- **缓存固定大小的对象**:每种大小类(size class)缓存一个 object 链表 +- Go 定义了 **67 种对象大小类别**,覆盖 0 ~ 32KB 的对象 + +``` +mcache 内部结构(简化): + mcache.span[class0] → 缓存大小为 8 字节对象的 span + mcache.span[class1] → 缓存大小为 16 字节对象的 span + ... + mcache.span[class66] → 缓存大小为 32768 字节对象的 span +``` + +### 第二级:mcentral(池级,细粒度锁) + +`mcentral` 管理多种规格(span class)的内存池,每种规格有独立的 `mspan`。 + +- **按大小分组**:同样大小的对象放在同一个 `mspan` 中 +- **细粒度锁**:每种规格的 `mspan` 有独立的锁,而非全局一把大锁 +- 当 mcache 的缓存不够时,向 mcentral 请求新的 span + +``` +mcentral 内部结构(简化): + mcentral.spans[class0] → mspan (管理大小为 8 字节的对象) + mcentral.spans[class1] → mspan (管理大小为 16 字节的对象) + ... +``` + +### 第三级:mheap(堆级,全局锁) + +`mheap` 是整个程序的"大仓库",直接向操作系统申请内存。 + +- **全局锁保护**:申请新内存时需要获取全局锁 +- **mmap 映射**:通过 `mmap` 向 OS 请求内存(初始约 64MB,可增长) +- 分配 span 给 mcentral,形成完整的分配链路 + +``` +mheap 内部结构(简化): + mheap.all[] → 管理所有 span 的数组 + mheap.pageAlloc → 位图,标记每个页面是否已分配 +``` + +### 大对象分配(>32KB) + +超过 32KB 的对象**跳过 mcache 和 mcentral**,直接从 mheap 分配: + +```go +// 超过 32KB 的对象直接走 mheap 路径 +func benchmarkLargeAlloc(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = make([]byte, 64*1024) // 64KB,直接走 mheap + } +} + +// 小于 32KB 的对象走 mcache 路径(更快) +func benchmarkSmallAlloc(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = make([]byte, 1024) // 1KB,走 mcache 路径 + } +} +``` + +> **思考**:为什么 32KB 是一个分界线?这与 CPU 缓存行(cache line,通常 64 字节)和页大小(4KB)有什么关系? + +## 锁粒度对比 + +| 层级 | 锁粒度 | 竞争程度 | 适用场景 | +|------|--------|----------|----------| +| **mcache** | 无锁(每个 P 独立) | 零竞争 | 频繁的小对象分配 | +| **mcentral** | 按大小类细粒度锁 | 低竞争 | 中等频率分配 | +| **mheap** | 全局锁 | 高竞争 | 大对象分配、扩容 | + +> **设计精妙之处**:Go 通过分层 + 无锁设计,让最常见的内存分配(小对象、同一 P 内)完全避免锁竞争。 + +## 关联笔记 + +- [[DEV/GO/内存分配与逃逸分析]] +- [[DEV/GO/GMP调度模型]] +- [[DEV/GO/GC垃圾回收]] diff --git a/DEV/GO/内存分配与逃逸分析/为什么Go打破栈堆分工规则.md b/DEV/GO/内存分配与逃逸分析/为什么Go打破栈堆分工规则.md new file mode 100644 index 0000000..ae78e0b --- /dev/null +++ b/DEV/GO/内存分配与逃逸分析/为什么Go打破栈堆分工规则.md @@ -0,0 +1,97 @@ +# 为什么 Go 打破了"栈/堆分工"规则? + +## 传统语言的设计哲学:静态可知 + +C/C++/Java 等语言遵循一个简单规则:**编译期能确定生命周期的放栈上,不能确定的放堆上**。 + +``` +传统语言的直觉逻辑: +┌──────────────────────────────────────────┐ +│ 局部变量 → 编译期知道作用域 → 栈 │ +│ new/malloc → 用户显式申请 → 堆 │ +│ │ +│ 规则是"语法驱动"的:new 就是堆, │ +│ 声明就是栈。程序员一目了然。 │ +└──────────────────────────────────────────┘ +``` + +这个设计的好处是**直观**:程序员清楚地知道自己在哪分配内存,什么时候释放。 + +## Go 的突破:编译器做决定 + +Go 的逃逸分析把决策权从**程序员**转移到了**编译器**: + +``` +Go 的逻辑: +┌──────────────────────────────────────────┐ +│ 编译器问:这个变量的生命周期多长? │ +│ │ +│ · 函数返回后不再用到 → 栈(快,零GC) │ +│ · 函数返回后还要用 → 堆(慢,有GC) │ +│ │ +│ 规则是"语义驱动"的: │ +│ new/malloc 出来的也可能在栈上! │ +│ 局部变量也可能逃到堆上! │ +└──────────────────────────────────────────┘ +``` + +## 打破规则的关键机制 + +### 1. `make` / `new` 不一定是堆分配 + +```go +func f() { + // 如果这个切片在函数返回后不再使用, + // 编译器可以把它留在栈上! + s := make([]int, 1000) + _ = s[0] +} // s 在这里销毁,不需要 GC 介入 + +// 只有当它"逃逸"时才会去堆上 +func g() []int { + s := make([]int, 1000) + return s // 逃逸!调用者返回后还要用 +} +``` + +### 2. 局部变量取地址也可能逃逸 + +```go +func f() { + x := 10 + _ = &x // 取地址后,编译器必须把 x 放堆上 +} +``` + +## 为什么 Go 敢这么做? + +核心原因是 **Go 有 GC**。 + +传统语言(特别是 C/C++)没有自动 GC,一旦变量逃逸到栈外就是 UB(未定义行为)。所以只能让用户用 `malloc` 显式控制。 + +Go 有 GC 兜底,编译器可以放心地说: + +> "不管我把你放哪,你都不会被提前释放——我分析过你的生命周期了。" + +这带来了三个优势: + +| 优势 | 说明 | +|------|------| +| **自动优化** | 编译器自动选择最优位置,减少手动调优 | +| **零 GC 开销** | 大部分局部变量留在栈上,不产生 GC 压力 | +| **安全性** | 不会出现悬垂指针(dangling pointer),编译器保证不会引用已释放的栈内存 | + +## 本质区别 + +``` +传统语言:语法决定分配位置 + 局部变量声明 → 栈(默认) + new/malloc → 堆(显式) + → 程序员负责生命周期管理 + +Go 语言:语义决定分配位置 + 编译器分析生命周期 → 选最优位置 + → 编译器负责,程序员专注正确性 +``` + +这正是 Go 的设计哲学缩影:**把程序员容易出错的低层细节交给编译器,让程序员关注更高层的语义正确性。** diff --git a/DEV/GO/内存分配与逃逸分析/虚拟内存与各语言对比.md b/DEV/GO/内存分配与逃逸分析/虚拟内存与各语言对比.md new file mode 100644 index 0000000..9ad96f0 --- /dev/null +++ b/DEV/GO/内存分配与逃逸分析/虚拟内存与各语言对比.md @@ -0,0 +1,142 @@ +--- +tags: [go, golang, 虚拟内存, 内存管理, 操作系统] +create time: 2026-04-24 10:00 +--- + +# 虚拟内存:Go 与其他语言的差异 + +## 概述 + +虚拟内存本身是操作系统提供的抽象,所有语言共享同一套底层机制(页表、MMU、page fault、swap)。但不同语言在**如何与虚拟内存交互**、**谁来管理分配**上有显著差异。理解这些差异,能帮助你更好地把握 Go 内存管理的独特之处。 + +> **思考**:既然虚拟内存是 OS 的特性,那语言的"内存管理"到底在管什么?——语言管的是**从虚拟内存到对象**之间的分配策略,而虚拟内存只是舞台背景。 + +## 内存分配"控制权"的归属 + +不同语言的内存分配器层级不同: + +| 语言 | 分配链路 | 谁决定物理内存何时分配? | +|------|---------|------------------------| +| **C/C++** | 程序 → `malloc/new` → 系统库 → OS `mmap/brk` | 程序员 + malloc 库实现 | +| **Go** | 程序 → Go `malloc` → mcache→mcentral→mheap → `mmap` | Go 运行时(自己当"二道贩子") | +| **Java** | 程序 → JVM 堆管理器 → `mmap` 一大块 → 对象分配 | JVM(启动时 mmap 一大块,不主动归还) | +| **Python** | 程序 → CPython pymalloc → `malloc` | CPython(小对象 pymalloc,大对象直接 malloc) | + +### Go 的独特之处:自己当二道贩子 + +C 语言直接调系统 `malloc`(底层可能调 `mmap` 或 `brk`),而 Go 运行时**自己实现了从 OS 到对象的全套分配逻辑**: + +``` +Go 的分配链路: + 程序 malloc → Go mcache → mcentral → mheap → mmap(64MB) → OS + +C 的分配链路: + 程序 malloc → glibc/tcmalloc → mmap/brk → OS +``` + +Go 先 `mmap` 一大块虚拟内存(默认 64MB),再自己管理 span 怎么分片。好处是**不需要依赖系统 malloc 的行为**,每一块内存的分配和回收都在 Go 自己的掌控之中。 + +## 栈 vs 堆:谁来决定? + +这是 Go 和很多语言最大的区别: + +```go +// Go:编译器在编译期做逃逸分析,自动决定 +x := 10 // 可能留在栈上(零 GC 开销) +p := &x // 逃逸到堆(GC 管理) + +// Java:几乎全部对象都在堆上,栈上只有引用 +Object o = new Object(); // new 的一定在堆上 + +// C:完全由程序员决定 +int x = 10; // 栈(默认) +int *p = malloc(4); // 堆,不 free 就泄漏 +``` + +Go 打破了"局部变量在栈、动态分配在堆"的传统分工——编译器会根据**生命周期**自动决定,程序员几乎不用操心。 + +> **思考**:如果你能交给编译器做决定,为什么不是所有语言都这么做?——答案在传统语言没有 GC,一旦变量逃逸到栈外就是 UB(未定义行为),所以只能让用户显式控制。 + +## GC 与虚拟内存的协作 + +| 语言 | GC 策略 | 和虚拟内存的关系 | +|------|---------|-----------------| +| **Go** | 并发三色标记-复制 | GC 后把不用的页 **`munmap` 还给 OS**,降低 RSS | +| **Java** | CMS/G1/ZGC 等 | 通常**不主动还内存给 OS**(除非特殊配置) | +| **C/C++** | 无 GC | 程序员 `free` 后,库可能还回 OS,也可能留着下次用 | + +### Go 的"弹性"行为 + +当 GC 回收了大量内存后,Go 运行时会把空闲的页通过 `munmap` 直接还给操作系统,让 RSS 降下来: + +```go +// Go 进程的内存占用是弹性的 +func main() { + data := make([]byte, 100<<20) // 100MB + _ = data // RSS 涨 + data = nil + runtime.GC() // RSS 回落!munmap 还给了 OS +} +``` + +这意味着 Go 进程的内存占用**随 workload 动态变化**——忙的时候涨,闲的时候退。而 Java 的 JVM 通常会"拿了就不还",吃内存比较"霸道"。 + +## 内存增长模式对比 + +``` +Go: [||||||||||||][ ][||||| ] + ↑ mmap 64MB → GC 后 munmap 还一部分 → 再次增长 + +Java: [|||||||||||||||||||||||||||||||||||][ ] + ↑ 启动时 mmap 一大块,几乎只增不减 + +C: [||][ ][|||||||][ ] + ↑ malloc/free 自由穿插,取决于程序逻辑 +``` + +Go 介于 Java 的"霸道"和 C 的"随意"之间——有自动增长的策略,但也有弹性收缩的机制。 + +## 大对象处理差异 + +Go 对 **>32KB 的对象**有特殊处理:跳过 mcache/mcentral,直接从 mheap 分配。这是因为大对象不值得放进细粒度池里管理。 + +| 语言 | 大对象处理 | +|------|-----------| +| **Go** | >32KB 直接走 mheap,跳过中间层 | +| **Java** | G1 GC 把大对象放在 "humongous regions" | +| **C** | glibc 对大对象通常直接 `mmap` | + +## 按需分配的直观验证 + +```go +// 这两行并不会真正占用 1GB 物理内存 +data := make([]byte, 1<<30) // 1GB 虚拟地址空间 + +// 直到你真正写入那一刻,操作系统才开始分配物理页 +data[0] = 1 // 触发 page fault,分配第一页(4KB) +data[1024*1024] = 1 // 触发第二页 + +// ps 看 RSS 可能只有几 KB +``` + +你 `make` 了 1GB,但实际物理内存只用了几 KB。这就是虚拟内存的 **Lazy Allocation** 机制——你**承诺**要用多大,操作系统**实际**只给多少。 + +> **思考**:既然物理内存没真正分配,那为什么 `make([]byte, 1<<30)` 还是会让进程变慢?——因为建立页表条目本身是有开销的,而且一旦触发 page fault,性能会有瞬时抖动。 + +## 总结 + +| 维度 | Go 的特色 | +|------|----------| +| **分配器** | 自己实现从 mmap 到对象的全链路,不依赖系统 malloc | +| **栈/堆决策** | 编译期逃逸分析自动决定,程序员专注正确性 | +| **内存弹性** | GC 后主动 munmap 还给 OS,RSS 可回落 | +| **并发友好** | mcache 每 P 一份无锁缓存,天然配合 GMP 模型 | +| **按需分配** | 和所有语言一样利用虚拟内存,但 Go 自己的分配策略更细粒度 | + +**虚拟内存本身没变,但 Go 把它用得更"自动化"了**——你不需要像 C 那样担心泄漏,也不需要像 Java 那样忍受僵化的内存占用。 + +## 关联笔记 + +- [[DEV/GO/内存分配与逃逸分析]] +- [[DEV/GO/GMP调度模型]] +- [[DEV/GO/GC垃圾回收]] diff --git a/FRONTEND/CSS 基础.md b/FRONTEND/CSS 基础.md new file mode 100644 index 0000000..8f734fb --- /dev/null +++ b/FRONTEND/CSS 基础.md @@ -0,0 +1,148 @@ +--- +tags: [前端, CSS, 基础] +create time: 2026-04-24 18:41 +--- + +# CSS 基础 + +## 概述 + +CSS(Cascading Style Sheets)负责网页的视觉呈现。如果说 HTML 是骨架,CSS 就是皮肤和衣服。掌握 CSS 的核心是理解三个概念:**选择器**(选谁)、**盒模型**(有多大)、**布局**(怎么排)。 + +思考题:为什么 CSS 叫做"层叠"样式表?多个规则作用于同一个元素时,浏览器如何决定用哪一个? + +## 正文 + +### 1. CSS 引入方式 + +```html + +

红色文字

+ + + + + + +``` + +### 2. 选择器优先级 + +``` +!important > 行内样式 > ID选择器 > 类选择器 > 标签选择器 > 通配符 + 10000 1000 100 10 1 0 +``` + +```css +/* 优先级实战 */ +div .item { color: red; } /* 0*1000 + 1*100 + 1*10 = 110 */ +div p.item { color: blue; } /* 0*1000 + 1*100 + 1*10 + 1*1 = 111 */ +#main .item { color: green; } /* 1*1000 + 0*100 + 1*10 = 1010 */ +``` + +> **提问:** 如果一个元素同时被 `.a { color: red }` 和 `#b { color: blue }` 选中,最终颜色是什么?为什么? + +### 3. 盒模型(Box Model) + +每个元素都是一个盒子,由四部分组成: + +```mermaid +block-beta + columns 1 + margin["margin 外边距\n(透明,不影响盒子大小)"] + border["border 边框"] + padding["padding 内边距\n(盒子内部,影响背景色)"] + content["content 内容区\n(实际文字/图片)"] +``` + +```css +/* 两种盒模型的区别 */ +.box-content-box { + box-sizing: content-box; /* 默认:width = content 宽度 */ +} +.box-border-box { + box-sizing: border-box; /* 推荐:width = content + padding + border */ +} +``` + +> **核心要点:** 实际开发中建议全局使用 `box-sizing: border-box`,避免尺寸计算混乱。 + +### 4. Flexbox 布局(一维布局首选) + +```css +.container { + display: flex; + justify-content: center; /* 主轴居中 */ + align-items: center; /* 交叉轴居中 */ + gap: 16px; /* 子元素间距 */ +} + +.item { + flex: 1; /* 等宽分配 */ + /* 或者 flex: 0 0 200px; 固定宽度 */ +} +``` + +Flexbox 常用属性速查: + +| 属性 | 作用 | +|------|------| +| `display: flex` | 启用 flex 布局 | +| `justify-content` | 主轴对齐(center / space-between / space-around) | +| `align-items` | 交叉轴对齐(center / flex-start / stretch) | +| `flex-direction` | 主轴方向(row / column) | +| `gap` | 子元素间距 | + +> **提问:** 什么时候用 Flexbox,什么时候用 Grid?它们的本质区别是什么? + +### 5. Grid 布局(二维布局神器) + +```css +.grid-container { + display: grid; + grid-template-columns: 200px 1fr 1fr; /* 三列:固定 + 自适应 */ + grid-template-rows: auto 1fr auto; /* 三行:自动 + 弹性 + 自动 */ + gap: 20px; +} + +/* 响应式网格 */ +.grid-responsive { + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); +} +``` + +```mermaid +graph TD + A["display: grid"] --> B["grid-template-columns\n定义列"] + A --> C["grid-template-rows\n定义行"] + A --> D["gap\n间距"] + A --> E["justify/align\n对齐"] +``` + +### 6. 响应式设计 + +```css +/* 媒体查询:不同屏幕尺寸应用不同样式 */ +.container { + width: 100%; + padding: 0 20px; +} + +@media (max-width: 768px) { + .container { + padding: 0 10px; + } + .grid-layout { + grid-template-columns: 1fr; /* 窄屏单列 */ + } +} +``` + +> **关键概念:** 移动端优先(Mobile First)是现代响应式设计的最佳实践——先写移动端样式,再用 `min-width` 媒体查询逐步增强。 + +## 关联笔记 + +- [[HTML 基础]] — CSS 的搭档,HTML 提供结构,CSS 提供样式 +- [[JavaScript 基础]] — 通过 JS 动态修改 CSS,实现交互效果 diff --git a/FRONTEND/HTML 基础.md b/FRONTEND/HTML 基础.md new file mode 100644 index 0000000..2ad1db5 --- /dev/null +++ b/FRONTEND/HTML 基础.md @@ -0,0 +1,126 @@ +--- +tags: [前端, HTML, 基础] +create time: 2026-04-24 18:41 +--- + +# HTML 基础 + +## 概述 + +HTML(HyperText Markup Language)是构建网页的骨架。它不是一门编程语言,而是一种标记语言——用标签(tag)告诉浏览器"这里是一段标题"、"这里是一个按钮"、"这里是一张图片"。 + +思考题:为什么 HTML 被称为"标记语言"而不是"编程语言"?它有没有变量、循环或条件判断的能力? + +## 正文 + +### 1. HTML 文档结构 + +一个标准的 HTML 文档包含以下核心部分: + +```html + + + + + + 页面标题 + + +

Hello World

+

这是一段文字

+ + +``` + +### 2. 常用标签分类 + +**标题与段落:** + +| 标签 | 用途 | +|------|------| +| `

` ~ `

` | 标题(h1 最大,h6 最小) | +| `

` | 段落 | +| `
` | 换行 | +| `


` | 水平线 | + +**文本格式化:** + +| 标签 | 用途 | +|------|------| +| `` | 加粗(语义:重要) | +| `` | 斜体(语义:强调) | +| `` | 代码 | +| `` | 高亮 | + +**链接与图片:** + +```html +打开新窗口 +描述文字 +``` + +> **提问:** 为什么 `` 标签的 `alt` 属性是必填的?它除了辅助功能外,还对 SEO(搜索引擎优化)有什么影响? + +**列表:** + +```html + +
    +
  • 第一项
  • +
  • 第二项
  • +
+ + +
    +
  1. 第一步
  2. +
  3. 第二步
  4. +
+``` + +### 3. 表单 — 用户交互的核心 + +```html +
+ + + + + + + + + + + + + +
+``` + +> **思考:** `GET` 和 `POST` 方法有什么区别?为什么登录表单必须用 `POST` 而不是 `GET`? + +### 4. 语义化标签 — HTML5 的新武器 + +```html +
+