vault backup: 2026-04-22 10:10:19
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
# Claude Code 配置 - Obsidian 知识库
|
||||
|
||||
此知识库级别的配置为 Claude 操作提供额外的指导说明。
|
||||
|
||||
## 新增和完善文件
|
||||
|
||||
在初始化和完成文件时,必须**完整**读取参照 `./config/agent/DOCUMENT_OPERATION.md
|
||||
了解结构、规范和惯例。
|
||||
在初次完善后,读取文件并二次检查是否符合文档的每一条内容规范——
|
||||
- **教学者模式** 提问设计
|
||||
- **代码示例** 合理注释
|
||||
- **图表** Mermaid 格式正确性
|
||||
## 行为规范
|
||||
|
||||
- 不要读取多余的无关文件,以减少 Token 消耗,这非常重要。
|
||||
- 当你不确定某个文件应该放在什么地方时,应当优先询问用户,而不是自己探索。
|
||||
|
||||
+62
-74
@@ -244,89 +244,77 @@ graph LR
|
||||
|
||||
## 参考实现
|
||||
|
||||
### 伪代码
|
||||
### 核心实现(伪代码)
|
||||
|
||||
```python
|
||||
class HeavyKeeper:
|
||||
def __init__(self, m, L, threshold, decay_factor):
|
||||
self.m = m # 每层桶数
|
||||
self.L = L # 哈希层数
|
||||
self.threshold = threshold
|
||||
self.decay_factor = decay_factor # 衰减因子
|
||||
|
||||
# 初始化多层 sketch
|
||||
self.buckets = [[KeeperBucket() for _ in range(m)]
|
||||
for _ in range(L)]
|
||||
|
||||
# 初始化哈希函数
|
||||
self.hash_funcs = [get_hash_func(i) for i in range(L)]
|
||||
|
||||
def insert(self, item, timestamp):
|
||||
def __init__(self, m, L, decay_factor):
|
||||
self.m, self.L = m, L
|
||||
self.decay_factor = decay_factor
|
||||
self.buckets = [[Bucket() for _ in range(m)] for _ in range(L)]
|
||||
|
||||
def insert(self, item):
|
||||
for layer in range(self.L):
|
||||
idx = self.hash_funcs[layer](item) % self.m
|
||||
bucket = self.buckets[layer][idx]
|
||||
|
||||
bucket = self.buckets[layer][hash(item) % self.m]
|
||||
if bucket.item == item:
|
||||
# 守护项匹配,直接计数(大象流强化)
|
||||
bucket.count += 1
|
||||
bucket.last_seen = timestamp
|
||||
bucket.count += 1 # 守护项匹配,直接计数
|
||||
else:
|
||||
# 计算替换概率
|
||||
estimated_freq = self._estimate_freq(item)
|
||||
replace_prob = min(1, estimated_freq / bucket.count)
|
||||
|
||||
if random.random() < replace_prob:
|
||||
# 替换为新项(大象流夺权)
|
||||
bucket.item = item
|
||||
bucket.count = 1
|
||||
bucket.error = bucket.count
|
||||
bucket.last_seen = timestamp
|
||||
# 空桶直接占领;否则按概率争夺
|
||||
prob = 1 if bucket.count == 0 else min(1, self._estimate(item) / bucket.count)
|
||||
if random() < prob:
|
||||
bucket.item, bucket.count, bucket.error = item, 1, 0 # 替换为新项
|
||||
else:
|
||||
# 不替换,仅增加误差(老鼠流被阻拦)
|
||||
bucket.error += 1
|
||||
|
||||
def apply_decay(self, current_time):
|
||||
"""应用衰减机制"""
|
||||
for layer in range(self.L):
|
||||
for bucket in self.buckets[layer]:
|
||||
elapsed = current_time - bucket.last_seen
|
||||
if elapsed > DECAY_INTERVAL:
|
||||
bucket.count *= self.decay_factor
|
||||
bucket.error *= self.decay_factor
|
||||
|
||||
# 归零清理
|
||||
if bucket.count < 1:
|
||||
bucket.item = None
|
||||
bucket.count = 0
|
||||
bucket.error = 0
|
||||
|
||||
def query(self, item):
|
||||
"""查询Item的频率估计"""
|
||||
min_count = float('inf')
|
||||
for layer in range(self.L):
|
||||
idx = self.hash_funcs[layer](item) % self.m
|
||||
bucket = self.buckets[layer][idx]
|
||||
if bucket.item == item:
|
||||
min_count = min(min_count, bucket.count)
|
||||
|
||||
return min_count if min_count != float('inf') else 0
|
||||
|
||||
def get_top_k(self, k):
|
||||
"""获取Top K大象流"""
|
||||
candidates = {}
|
||||
for layer in range(self.L):
|
||||
for bucket in self.buckets[layer]:
|
||||
if bucket.count >= self.threshold and bucket.item:
|
||||
item = bucket.item
|
||||
candidates[item] = max(candidates.get(item, 0),
|
||||
bucket.count)
|
||||
|
||||
# 返回 Top-k
|
||||
return sorted(candidates.items(),
|
||||
key=lambda x: x[1],
|
||||
reverse=True)[:k]
|
||||
bucket.error += 1 # 不替换,仅增加误差
|
||||
```
|
||||
|
||||
### Go 参考实现
|
||||
|
||||
```go
|
||||
package heavykeeper
|
||||
|
||||
type Bucket struct {
|
||||
Item []byte
|
||||
Count uint32
|
||||
Error uint32
|
||||
}
|
||||
|
||||
type HeavyKeeper struct {
|
||||
m, L uint32
|
||||
decayFactor float64
|
||||
buckets [][]Bucket
|
||||
hashFuncs []func([]byte) uint64
|
||||
}
|
||||
|
||||
func (hk *HeavyKeeper) Insert(item []byte) {
|
||||
for layer := uint32(0); layer < hk.L; layer++ {
|
||||
idx := hk.hashFuncs[layer](item) % hk.m
|
||||
bucket := &hk.buckets[layer][idx]
|
||||
|
||||
if bytes.Equal(bucket.Item, item) {
|
||||
bucket.Count++
|
||||
continue
|
||||
}
|
||||
|
||||
var prob float64
|
||||
if bucket.Count == 0 {
|
||||
prob = 1 // 空桶直接占领
|
||||
} else {
|
||||
prob = float64(hk.estimate(item)) / float64(bucket.Count)
|
||||
if prob > 1 {
|
||||
prob = 1
|
||||
}
|
||||
}
|
||||
if rand.Float64() < prob {
|
||||
bucket.Item, bucket.Count, bucket.Error = item, 1, 0
|
||||
} else {
|
||||
bucket.Error++
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **说明**:`insert` 是核心逻辑——对每层哈希找桶,匹配守护项则计数,否则按概率争夺。伪代码省略了衰减、查询、Top-K 等辅助方法,实际部署时按需补充。
|
||||
|
||||
## 相关算法对比
|
||||
|
||||
| 算法 | 空间复杂度 | 大象流准确性 | 老鼠流过滤 | 衰减支持 | 适用场景 |
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
---
|
||||
tags: [CS, TOOLS, Docker, 容器化]
|
||||
create time: 2026-04-21
|
||||
---
|
||||
|
||||
# Docker 安装指南
|
||||
|
||||
## 概述
|
||||
|
||||
Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口。
|
||||
|
||||
## 系统要求
|
||||
|
||||
- Ubuntu 24.04 LTS (Jammy Jellyfish)
|
||||
- 64-bit architecture
|
||||
- 内核版本 4.15 或更高版本
|
||||
|
||||
## 安装步骤
|
||||
|
||||
### 1. 更新系统包索引
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
```
|
||||
|
||||
### 2. 安装必要的依赖包
|
||||
|
||||
```bash
|
||||
sudo apt install apt-transport-https ca-certificates curl gnupg lsb-release
|
||||
```
|
||||
|
||||
### 3. 添加 Docker 官方 GPG 密钥
|
||||
|
||||
```bash
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
```
|
||||
|
||||
### 4. 添加 Docker APT 仓库
|
||||
|
||||
```bash
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||
```
|
||||
|
||||
### 5. 更新包索引
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
```
|
||||
|
||||
### 6. 安装 Docker Engine
|
||||
|
||||
```bash
|
||||
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||
```
|
||||
|
||||
### 7. 启动并启用 Docker 服务
|
||||
|
||||
```bash
|
||||
sudo systemctl start docker
|
||||
sudo systemctl enable docker
|
||||
```
|
||||
|
||||
### 8. 将当前用户添加到 docker 组(可选,但推荐)
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
|
||||
> 请注意:执行此命令后,需要重新登录或运行 `newgrp docker` 命令使组成员身份生效。
|
||||
|
||||
## 验证安装
|
||||
|
||||
执行以下命令来验证 Docker 是否正确安装:
|
||||
|
||||
```bash
|
||||
docker --version
|
||||
```
|
||||
|
||||
运行一个简单的测试容器:
|
||||
|
||||
```bash
|
||||
docker run hello-world
|
||||
```
|
||||
|
||||
## 常用命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `docker pull [镜像名]` | 拉取镜像 |
|
||||
| `docker run [镜像名]` | 运行容器 |
|
||||
| `docker ps` | 显示正在运行的容器 |
|
||||
| `docker ps -a` | 显示所有容器 |
|
||||
| `docker images` | 显示所有镜像 |
|
||||
| `docker stop [容器名/ID]` | 停止容器 |
|
||||
| `docker rm [容器名/ID]` | 删除容器 |
|
||||
| `docker rmi [镜像名/ID]` | 删除镜像 |
|
||||
|
||||
## 后续操作
|
||||
|
||||
- **Docker Compose**:如果您需要使用 Docker Compose,在安装 Docker 时已包含,可直接使用
|
||||
- **配置 Docker**:可以修改 `/etc/docker/daemon.json` 文件进行配置
|
||||
|
||||
## 安全说明
|
||||
|
||||
- 不要使用 root 用户运行 Docker
|
||||
- 为非 root 用户添加 docker 组时要小心,因为该用户将具有 root 权限
|
||||
- 定期更新 Docker 和容器镜像
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 权限问题
|
||||
|
||||
如果遇到权限问题,确保当前用户已被添加到 docker 组中:
|
||||
|
||||
```bash
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
|
||||
然后注销并重新登录。
|
||||
|
||||
### 服务启动失败
|
||||
|
||||
如果 Docker 服务无法启动,可以检查其状态:
|
||||
|
||||
```bash
|
||||
sudo systemctl status docker
|
||||
```
|
||||
|
||||
## 参考资料
|
||||
|
||||
- [Docker 官方安装指南](https://docs.docker.com/engine/install/ubuntu/)
|
||||
- [Ubuntu 24.04 Docker 安装文档](https://ubuntu.com/server/docs/container-docker)
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
---
|
||||
tags: [DEV, GO, golang, GORM, backend, ORM, database]
|
||||
create time: 2026-04-22 10:00
|
||||
---
|
||||
|
||||
# GORM - Go 语言 ORM 框架
|
||||
|
||||
## 概述
|
||||
|
||||
GORM 是 Go 语言中最流行的 ORM(对象关系映射)框架,由 [@jinzhu](https://github.com/jinzhu) 创建。它基于 `database/sql` 构建,提供了链式 API、关联预加载、插件扩展等特性,让 Go 开发者以符合 Go 风格的方式高效操作数据库。
|
||||
|
||||
> **思考**:为什么在有了 `database/sql` 标准库之后,还需要 GORM 这样的 ORM?ORM 的价值是什么?它的代价又是什么?
|
||||
|
||||
ORM 的核心价值在于**将数据库表映射为结构体**,用面向对象的方式操作数据,减少样板 SQL 代码。代价是:理解 ORM 生成的 SQL 至关重要,复杂查询场景下仍需手写 SQL。
|
||||
|
||||
## 安装与初始化
|
||||
|
||||
```bash
|
||||
go get -u gorm.io/gorm
|
||||
go get -u gorm.io/driver/mysql # MySQL 驱动
|
||||
go get -u gorm.io/driver/postgres # PostgreSQL 驱动
|
||||
go get -u gorm.io/driver/sqlite # SQLite 驱动
|
||||
```
|
||||
|
||||
```go
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/driver/mysql"
|
||||
)
|
||||
|
||||
// MySQL 连接
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
panic("failed to connect database")
|
||||
}
|
||||
```
|
||||
|
||||
## 定义模型
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey"` // 主键,自动命名为 id
|
||||
Name string `gorm:"size:255;not null"` // 非空 + 字段长度
|
||||
Email string `gorm:"uniqueIndex"` // 唯一索引
|
||||
Age int `gorm:"default:0"` // 默认值
|
||||
Role string `gorm:"default:'user'"`
|
||||
CreatedAt time.Time // 自动管理 CreatedAt
|
||||
UpdatedAt time.Time // 自动管理 UpdatedAt
|
||||
DeletedAt gorm.DeletedAt // 软删除支持
|
||||
}
|
||||
```
|
||||
|
||||
**GORM 约定**:
|
||||
|
||||
| 约定 | 说明 |
|
||||
|------|------|
|
||||
| 表名 | 结构体名的复数形式(`User` → `users`),可通过 `TableName()` 重写 |
|
||||
| 主键 | 名为 `ID` 的字段自动作为主键,类型推断为 `INT UNIQUE PRIMARY KEY` |
|
||||
| 字段名 | Go 字段名 → snake_case 的列名(`UserName` → `user_name`) |
|
||||
| CreatedAt/UpdatedAt | 自动填充时间戳(可通过 `DisableCreateTimeIndex` / `DisableUpdateTimeIndex` 关闭) |
|
||||
|
||||
## CRUD 操作
|
||||
|
||||
### 创建
|
||||
|
||||
```go
|
||||
user := User{Name: "Alice", Age: 25, Role: "admin"}
|
||||
result := db.Create(&user) // 返回 *gorm.Result
|
||||
|
||||
// 批量创建(一次 INSERT)
|
||||
users := []User{
|
||||
{Name: "Bob", Age: 30},
|
||||
{Name: "Carol", Age: 28},
|
||||
}
|
||||
db.Create(&users) // GORM 自动优化为批量 INSERT
|
||||
```
|
||||
|
||||
### 查询
|
||||
|
||||
```go
|
||||
// 单一查询
|
||||
var user User
|
||||
db.First(&user, 10) // 主键查询
|
||||
db.First(&user, "name = ?", "Alice") // 条件查询
|
||||
db.Where("age > ?", 20).First(&user) // Where 链式
|
||||
|
||||
// 多结果查询
|
||||
var users []User
|
||||
db.Where("role = ?", "user").Find(&users)
|
||||
|
||||
// 链式条件
|
||||
db.Where("role = ?", "admin").
|
||||
Where("age >= ?", 25).
|
||||
Order("age desc").
|
||||
Limit(10).
|
||||
Offset(20).
|
||||
Find(&users)
|
||||
```
|
||||
|
||||
**GORM 查询 API 速查**:
|
||||
|
||||
| 方法 | 等价 SQL |
|
||||
|------|----------|
|
||||
| `First` / `Take` | `LIMIT 1`,找第一条 |
|
||||
| `Last` | `ORDER BY PRIMARY KEY DESC LIMIT 1` |
|
||||
| `Find` | `SELECT * FROM ...` |
|
||||
| `Where` | `WHERE ...` |
|
||||
| `Or` | `OR ...` |
|
||||
| `Not` | `NOT ...` |
|
||||
| `Order` | `ORDER BY ...` |
|
||||
| `Select` | `SELECT col1, col2` |
|
||||
| `Group` / `Having` | `GROUP BY` / `HAVING` |
|
||||
| `Joins` | `JOIN ...` |
|
||||
| `Scan` | 扫描到自定义结构体 |
|
||||
|
||||
> **注意**:`First`、`Last` 在没有数据时返回 `gorm.ErrRecordNotFound`,务必处理此错误。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["db.Model(&User{})"] --> B["Where 条件过滤"]
|
||||
B --> C["Order 排序"]
|
||||
C --> D["Select 字段选择"]
|
||||
D --> E["Limit/Offset 分页"]
|
||||
E --> F["Group/Having 分组"]
|
||||
F --> G["Preload 预加载关联"]
|
||||
G --> H["Find/First/Last 执行"]
|
||||
H --> I["Scan 扫描到结构体"]
|
||||
|
||||
classDef start fill:#4285f4,color:#fff
|
||||
classDef step fill:#f1f3f4,stroke:#333
|
||||
classDef finish fill:#34a853,color:#fff
|
||||
class A start
|
||||
class I finish
|
||||
class B,C,D,E,F,G,H step
|
||||
```
|
||||
|
||||
> **链式 API 的本质**:每个方法返回 `*gorm.DB` 对象,不断累积查询条件,最终 `Find`/`First` 时才组装并执行 SQL。
|
||||
|
||||
### 更新
|
||||
|
||||
```go
|
||||
// 更新单个字段
|
||||
db.Model(&user).Update("name", "Alice Updated")
|
||||
|
||||
// 更新多个字段
|
||||
db.Model(&user).Updates(User{Name: "Alice V2", Age: 26, Role: "superadmin"})
|
||||
|
||||
// 带条件的更新
|
||||
db.Where("age > ?", 18).Model(&user).Update("role", "adult")
|
||||
|
||||
// 用 map 更新(会跳过零值字段,慎用)
|
||||
db.Model(&user).Updates(map[string]interface{}{"name": "New", "age": 30})
|
||||
```
|
||||
|
||||
### 删除
|
||||
|
||||
```go
|
||||
// 软删除(推荐)
|
||||
db.Delete(&user) // 设置 DeletedAt = now(),而非物理删除
|
||||
|
||||
// 硬删除
|
||||
db.Unscoped().Delete(&user) // 物理删除
|
||||
|
||||
// 批量删除
|
||||
db.Where("age < ?", 18).Delete(&User{})
|
||||
```
|
||||
|
||||
## 关联(Associations)
|
||||
|
||||
GORM 支持三种关联关系,通过标签定义。
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Role string
|
||||
CreditCards []CreditCard `gorm:"foreignKey:UserID"` // HasMany
|
||||
}
|
||||
|
||||
type CreditCard struct {
|
||||
gorm.Model
|
||||
Number string
|
||||
UserID uint `gorm:"not null"` // 外键
|
||||
}
|
||||
```
|
||||
|
||||
### 三种关联类型
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph User 用户
|
||||
U1["ID\nName\nRole"]
|
||||
end
|
||||
|
||||
subgraph CreditCard 信用卡
|
||||
C1["ID\nNumber\nUserID (FK)"]
|
||||
end
|
||||
|
||||
subgraph Profile 个人资料
|
||||
P1["ID\nBio\nUserID (FK)"]
|
||||
end
|
||||
|
||||
subgraph Language 语言[关联表: user_languages]
|
||||
L1["ID\nName"]
|
||||
UM["user_id"]
|
||||
LM["language_id"]
|
||||
end
|
||||
|
||||
U1 -->|"1:N HasMany"| C1
|
||||
U1 -->|"1:1 HasOne"| P1
|
||||
U1 -.->|"N:M BelongsToMany"| L1
|
||||
|
||||
classDef user fill:#4285f4,stroke:#336,stroke-width:2px,color:#fff
|
||||
classDef card fill:#ea4335,stroke:#c33,stroke-width:2px,color:#fff
|
||||
classDef profile fill:#34a853,stroke:#264,stroke-width:2px,color:#fff
|
||||
classDef lang fill:#fbbc04,stroke:#cc8,stroke-width:2px,color:#333
|
||||
|
||||
class U1 user
|
||||
class C1 card
|
||||
class P1 profile
|
||||
class L1 lang
|
||||
```
|
||||
|
||||
> **关键理解**:标签写在**一端**,GORM 会根据外键位置自动判断关系方向。
|
||||
|
||||
- **HasOne**:外键在对方模型(`Profile.UserID`)→ 写在 `User` 上用 `hasOne`
|
||||
- **HasMany**:外键在对方模型(`CreditCard.UserID`)→ 写在 `User` 上用 `hasMany`
|
||||
- **BelongsTo**:外键在当前模型 → 写在「从属」一方
|
||||
- **BelongsToMany**:需要中间关联表(`user_languages`)
|
||||
|
||||
| 关联类型 | 标签方式 | 示例 |
|
||||
|----------|----------|------|
|
||||
| **HasOne** | 外键在对方模型 | `Profile` 包含 `UserID`,`User` 通过 `HasOne` 关联 |
|
||||
| **HasMany** | 外键在对方模型 | `CreditCards` 数组,`CreditCard` 包含 `UserID` |
|
||||
| **BelongsTo** | 外键在当前模型 | `CreditCard` 包含 `UserID`,`BelongsTo` 指向 `User` |
|
||||
| **BelongsToMany** | 需要通过关联表 | `User` ↔ `Languages`(多对多) |
|
||||
|
||||
### 预加载(Preloading)
|
||||
|
||||
```go
|
||||
// N+1 问题:一次查询 N 条关联记录
|
||||
db.Preload("CreditCards").Find(&users)
|
||||
// 生成 2 条 SQL:SELECT * FROM users; SELECT * FROM credit_cards WHERE user_id IN (...)
|
||||
|
||||
// 嵌套预加载
|
||||
db.Preload("CreditCards").
|
||||
Preload("Profile").
|
||||
Find(&users)
|
||||
|
||||
// 带条件的预加载
|
||||
db.Preload("CreditCards", "type = ?", "premium").Find(&users)
|
||||
|
||||
// 使用 Joins 预加载(生成 INNER JOIN,减少查询次数)
|
||||
db.Joins("Profile").Find(&users)
|
||||
```
|
||||
|
||||
> **核心原则**:批量查询时务必使用 `Preload`,否则会在循环中触发 N+1 查询问题。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Bad ["❌ 不使用 Preload - N+1 问题"]
|
||||
direction TB
|
||||
S1["SQL 1: SELECT * FROM users"] --> Loop["循环 N 次"]
|
||||
Loop --> S2["SQL 2: SELECT * FROM credit_cards WHERE user_id=1"]
|
||||
Loop --> S3["SQL 3: SELECT * FROM credit_cards WHERE user_id=2"]
|
||||
Loop --> S4["SQL N: SELECT * FROM credit_cards WHERE user_id=N"]
|
||||
end
|
||||
|
||||
subgraph Good ["✅ 使用 Preload - 2 条 SQL"]
|
||||
direction TB
|
||||
G1["SQL 1: SELECT * FROM users"] --> G2["SQL 2: SELECT * FROM credit_cards WHERE user_id IN (1,2,...,N)"]
|
||||
end
|
||||
```
|
||||
|
||||
## 多对多关联(BelongsToMany)
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Languages []Language `gorm:"many2many:user_languages;"`
|
||||
}
|
||||
|
||||
type Language struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
}
|
||||
|
||||
// 自动创建关联表 user_languages(字段:user_id, language_id)
|
||||
// 插入关联数据
|
||||
db.Model(&user).Association("Languages").Append(&Language{Name: "Go"})
|
||||
|
||||
// 查询关联
|
||||
db.Preload("Languages").Find(&users)
|
||||
|
||||
// 替换关联
|
||||
db.Model(&user).Association("Languages").Replace(
|
||||
&Language{Name: "Python"}, &Language{Name: "Rust"},
|
||||
)
|
||||
```
|
||||
|
||||
## 高级查询技巧
|
||||
|
||||
### Raw SQL & 条件构造
|
||||
|
||||
```go
|
||||
// 原生 SQL
|
||||
db.Raw("SELECT name, SUM(amount) FROM orders WHERE status = ? GROUP BY name", "PAID").
|
||||
Scan(&results)
|
||||
|
||||
// 条件表达式
|
||||
db.Where(
|
||||
"name LIKE ? AND age BETWEEN ? AND ?", "%jon%", 20, 30,
|
||||
).Find(&users)
|
||||
|
||||
// IN 查询
|
||||
db.Where("id IN ?", []int{1, 2, 3}).Find(&users)
|
||||
|
||||
// 子查询
|
||||
subQuery := db.Model(&Order).Select("user_id").Where("amount > ?", 100)
|
||||
db.Where("id IN (?)", subQuery).Find(&users)
|
||||
```
|
||||
|
||||
### 事务
|
||||
|
||||
```go
|
||||
tx := db.Begin() // 注意:事务中需使用 tx 而非 db
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := tx.Create(&user).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Create(&order).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
```
|
||||
|
||||
> **关键点**:事务内的所有操作必须使用事务句柄 `tx`,而非全局 `db`。
|
||||
|
||||
### 自定义表名与列名
|
||||
|
||||
```go
|
||||
func (User) TableName() string {
|
||||
return "app_users" // 不使用复数形式
|
||||
}
|
||||
|
||||
// 列名自定义
|
||||
type User struct {
|
||||
ID int `gorm:"column:uid"`
|
||||
Name string `gorm:"column:username"`
|
||||
}
|
||||
```
|
||||
|
||||
### Scope(可复用查询片段)
|
||||
|
||||
```go
|
||||
func AmountGT(amount int) func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("amount > ?", amount)
|
||||
}
|
||||
}
|
||||
|
||||
func Active() func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("status = ?", "active")
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
db.Scopes(AmountGT(100), Active()).Find(&orders)
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. Select 指定字段
|
||||
|
||||
```go
|
||||
// 只查询需要的字段,避免加载大字段(如 text/blob)
|
||||
db.Select("name", "age").Find(&users)
|
||||
```
|
||||
|
||||
### 2. 批量操作
|
||||
|
||||
```go
|
||||
// 批量写入:1次 INSERT N行,而非 N次 INSERT
|
||||
db.Create(&users)
|
||||
|
||||
// 批量更新:1次 UPDATE
|
||||
db.Model(&user).Where("id IN ?", ids).Update("status", "active")
|
||||
|
||||
// 批量删除
|
||||
db.Delete(&User{}, ids)
|
||||
```
|
||||
|
||||
### 3. 关闭预编译(极端性能场景)
|
||||
|
||||
```go
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
PrepareStmt: true, // 开启预编译,适合高并发同一 SQL
|
||||
})
|
||||
```
|
||||
|
||||
开启 `PrepareStmt` 后,GORM 会使用 `sql.DB.Prepare` + `sql.Stmt` 缓存预编译语句,适合大量相同 SQL 的执行场景。
|
||||
|
||||
## 企业级注意事项
|
||||
|
||||
### 🚫 严禁使用 AutoMigrate
|
||||
|
||||
在团队协作和企业级项目中,**禁止使用 `db.AutoMigrate()`**。原因:
|
||||
|
||||
1. **不可控的 DDL**:自动建表/改表可能导致生产数据丢失
|
||||
2. **DBA 审计要求**:所有表结构变更需经过 SQL 审查
|
||||
3. **版本管理**:数据库结构应通过迁移脚本(如 `golang-migrate`)管理
|
||||
|
||||
**推荐做法**:通过 SQL 脚本初始化数据库,与 [数据库设计](file:///home/wonder/Code/obsidian/金山办公作业/Week05/数据库设计.md) 文档保持一致。
|
||||
|
||||
### 事务与并发
|
||||
|
||||
```go
|
||||
// ❌ 错误:多个 goroutine 共享同一个 db 连接
|
||||
// db.Create(&user1) // 并发不安全
|
||||
|
||||
// ✅ 正确:事务自带连接池,每个 goroutine 开启独立事务
|
||||
func saveUser(tx *gorm.DB, user *User) error {
|
||||
return tx.Create(user).Error
|
||||
}
|
||||
```
|
||||
|
||||
GORM 底层使用 `sql.DB` 连接池,本身是并发安全的,但 `db.Begin()` 返回的事务对象不可跨 goroutine 共享。
|
||||
|
||||
### 错误处理
|
||||
|
||||
```go
|
||||
result := db.Where("id = ?", id).First(&user)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
// 记录不存在
|
||||
} else {
|
||||
// 其他数据库错误
|
||||
return result.Error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## GORM vs 原生 SQL
|
||||
|
||||
| 场景 | 推荐方案 | 原因 |
|
||||
|------|----------|------|
|
||||
| 简单 CRUD | GORM | 代码简洁,开发效率高 |
|
||||
| 复杂 JOIN | GORM `Joins` | GORM 链式写法比手写 SQL 更直观 |
|
||||
| 聚合查询(GROUP BY + HAVING) | GORM `Select/Group/Having` | 原生 SQL 也可,看团队偏好 |
|
||||
| 复杂子查询/CTE | 原生 SQL `Raw/Scan` | GORM 对复杂 SQL 支持有限 |
|
||||
| 高性能写入(大批量) | GORM 批量 | 或直接用 `sql.Batch` |
|
||||
| 数据库迁移 | SQL 脚本 / golang-migrate | GORM AutoMigrate 禁用于生产 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[DEV/GO/README]]
|
||||
- [[金山办公作业/Week05/数据库设计]]
|
||||
- [[金山办公作业/Week05/全栈开发实战:AI 智能单词本]]
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
tags: [config, claude, standards]
|
||||
---
|
||||
|
||||
# Claude Code 项目配置
|
||||
|
||||
本项目是 Obsidian vault,Claude Code 作为 AI 协作者协助知识管理。
|
||||
|
||||
## 核心原则
|
||||
|
||||
1. **Obsidian Native**: 理解 Markdown、YAML frontmatter、Wiki-links
|
||||
2. **Safety First**: 从不未经理解覆盖数据,始终使用相对路径
|
||||
3. **Proactive**: 预期潜在问题(如断链、缺失文件)
|
||||
4. **Clarity**: 精准变更,最小化笔记噪音
|
||||
|
||||
## 操作规范
|
||||
|
||||
- 详见 [[config/agent/policies/standards.md]]
|
||||
- 正面清单:[[config/agent/policies/allowlist.md]]
|
||||
- 负面清单:[[config/agent/policies/blocklist.md]]
|
||||
|
||||
## 可用 Skills
|
||||
|
||||
- [[config/agent/skills/note-creation.md]]: 笔记创建
|
||||
- [[config/agent/skills/file-edit.md]]: 文件编辑
|
||||
- [[config/agent/skills/link-management.md]]: 链接管理
|
||||
|
||||
## 集成方式
|
||||
|
||||
Claude 会在每次任务开始时自动读取此文件,遵循定义的规范和约束。
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
# 文件创建/修改规范
|
||||
|
||||
## 格式
|
||||
|
||||
```yaml
|
||||
---
|
||||
tags: []
|
||||
create time: YYYY-MM-DD HH:mm
|
||||
---
|
||||
|
||||
# <标题>
|
||||
|
||||
## 概述
|
||||
[简短描述]
|
||||
|
||||
## 正文
|
||||
[按照内容规范生成的内容]
|
||||
|
||||
## 关联笔记
|
||||
- [[相关笔记-可不填-无需链接README]]
|
||||
```
|
||||
|
||||
## 必填项
|
||||
|
||||
| 字段 | 格式 | 说明 |
|
||||
|------|------|------|
|
||||
| `tags` | 数组 | 必须存在 `[]`,智能填充相关标签 |
|
||||
| `create time` | `YYYY-MM-DD HH:mm` | 当前系统时间 |
|
||||
|
||||
## 文档结构
|
||||
|
||||
1. **## 概述**: 简短描述文档内容,不要保留占位文本
|
||||
2. **## 正文**: 主要内容,详略得当,注重拓展进阶
|
||||
3. **## 关联笔记**: 相关笔记 Wiki-links,若无可移除
|
||||
|
||||
## 内容规范【IMPORTANT】
|
||||
|
||||
- **教学者模式**: 假设你是教学者,你需要先规划如何记录这个知识点,让读者能够容易理解。你也可以穿插问题在文档中,启发学生的思考。
|
||||
- **代码示例**: 优先 Go (后端) + React/TS (前端),代码示例点到为止,不要过于冗长。可以适当通过注释省略一部分代码增强可读性,体现核心逻辑即可。当你给出代码时,一定要给出对应的文本解释。
|
||||
- **图表**: 遇到关键概念、流程,仅仅靠文字不容易清晰说明,此时应该使用 Mermaid。
|
||||
- **风格**: 详略得当,注重实用性。文风严谨但是不失“活人感”,循序渐进,深入浅出。
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
tags: [config, agent, standards]
|
||||
create time: 2026-04-17 11:29
|
||||
update time: 2026-04-18
|
||||
---
|
||||
|
||||
# 文件创建规范
|
||||
|
||||
## 核心规则
|
||||
|
||||
**所有新文件必须遵循以下结构:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
tags: []
|
||||
create time: YYYY-MM-DD HH:mm
|
||||
---
|
||||
|
||||
# <标题>
|
||||
|
||||
## 概述
|
||||
[简短描述]
|
||||
|
||||
## 正文
|
||||
[内容]
|
||||
|
||||
## 关联笔记
|
||||
- [[相关笔记]]
|
||||
```
|
||||
|
||||
## 必填项
|
||||
|
||||
| 字段 | 格式 | 说明 |
|
||||
|------|------|------|
|
||||
| `tags` | 数组 | 必须存在 `[]`,智能填充相关标签 |
|
||||
| `create time` | `YYYY-MM-DD HH:mm` | 当前系统时间 |
|
||||
|
||||
## 文档结构
|
||||
|
||||
1. **## 概述**: 简短描述文档内容,不要保留占位文本
|
||||
2. **## 正文**: 主要内容,详略得当,注重拓展进阶
|
||||
3. **## 关联笔记**: 相关笔记 Wiki-links,若无可移除
|
||||
|
||||
## 内容规范
|
||||
|
||||
- **代码示例**: 优先 Go (后端) + React/TS (前端)
|
||||
- **图表**: 选择性使用 Mermaid
|
||||
- **风格**: 详略得当,注重实用性
|
||||
|
||||
## Agent 执行步骤
|
||||
|
||||
1. 读取模板: `[[config/template/default-template.md]]`
|
||||
2. 创建文件并填充 `create time`
|
||||
3. 智能填充 `tags`
|
||||
4. 替换占位文本为实际内容
|
||||
5. 添加相关 Wiki-links
|
||||
|
||||
## 特殊情况
|
||||
|
||||
配置文件、开发文件等可灵活调整结构,但需保留:
|
||||
- `tags` 字段
|
||||
- `create time` 记录
|
||||
- 清晰的组织结构
|
||||
@@ -1,38 +0,0 @@
|
||||
---
|
||||
tags: [config, agent, policy]
|
||||
---
|
||||
|
||||
# Claude 操作正面清单
|
||||
|
||||
以下操作为明确允许的操作:
|
||||
|
||||
## 文件操作
|
||||
- ✅ 读取 vault 内任何 Markdown 文件
|
||||
- ✅ 创建新文件(遵循 [[config/agent/agent.md]] 规范)
|
||||
- ✅ 编辑现有文件(使用 Edit 工具进行精准变更)
|
||||
- ✅ 使用相对路径访问文件
|
||||
|
||||
## 内容结构
|
||||
- ✅ 添加/更新 YAML frontmatter
|
||||
- ✅ 使用 Wiki-links `[[note-name]]`
|
||||
- ✅ 使用标签 `#tag-name`
|
||||
- ✅ 保留或简化 Dataview 查询
|
||||
|
||||
## 搜索与导航
|
||||
- ✅ 使用 Grep 搜索内容
|
||||
- ✅ 使用 Glob 查找文件
|
||||
- ✅ 分析链接关系和依赖
|
||||
|
||||
## Git 操作(受限)
|
||||
- ✅ 运行 `git status` 查看状态
|
||||
- ✅ 运行 `git diff` 查看变更
|
||||
- ⚠️ 提交、推送等操作需要用户明确请求
|
||||
|
||||
## 资产管理
|
||||
- ✅ 读取本地图片(PNG, JPG, GIF, WebP)
|
||||
- ✅ 下载外部图片到 media 文件夹
|
||||
- ✅ 替换图片链接为本地 wiki-link
|
||||
|
||||
## 处理特殊情况
|
||||
- ✅ 配置文件和开发文件可灵活调整结构
|
||||
- ✅ 在用户明确要求时使用非标准结构
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
tags: [config, agent, policy]
|
||||
---
|
||||
|
||||
# Claude 操作负面清单
|
||||
|
||||
以下操作为明确禁止的操作:
|
||||
|
||||
## 文件操作
|
||||
- ❌ 未经 Read 工具直接 Edit 文件
|
||||
- ❌ 使用绝对路径访问 vault 文件
|
||||
- ❌ 覆盖 `.obsidian/` 配置(除非明确要求)
|
||||
- ❌ 删除文件(除非用户明确要求)
|
||||
|
||||
## 内容变更
|
||||
- ❌ 添加 emoji(除非用户明确要求)
|
||||
- ❌ 创建文档文件(README.md, *.md)(除非明确要求)
|
||||
- ❌ 破坏 Dataview 查询语法
|
||||
- ❌ 移除必要的 YAML frontmatter 字段
|
||||
|
||||
## 路径规范
|
||||
- ❌ 在 vault 内使用 `/path/to/file` 格式
|
||||
- ❌ 使用 `/home/user/...` 绝对路径引用 vault 文件
|
||||
- ❌ 混淆 Wiki-links 和 Markdown 链接
|
||||
|
||||
## Git 操作
|
||||
- ❌ 跳过 hooks(--no-verify)
|
||||
- ❌ 跳过签名(--no-gpg-sign)
|
||||
- ❌ 强制 push 到 main/master(警告用户)
|
||||
- ❌ 使用 --interactive 或 --amend 标志
|
||||
|
||||
## 系统命令
|
||||
- ❌ 运行 find、grep、cat(使用专用工具代替)
|
||||
- ❌ 创建空 git commit
|
||||
- ❌ 修改全局 git 配置
|
||||
- ❌ 运行破坏性命令(reset --hard, clean -f 等)
|
||||
|
||||
## 数据安全
|
||||
- ❌ 提交包含凭据的文件(.env, credentials.json)
|
||||
- ❌ 分享 API 密钥或敏感信息
|
||||
@@ -1,66 +0,0 @@
|
||||
---
|
||||
tags: [config, agent, policy]
|
||||
---
|
||||
|
||||
# Claude 操作规范
|
||||
|
||||
## 通用原则
|
||||
|
||||
1. **遵循现有结构**: 严格遵循项目中已有的文件结构和格式
|
||||
2. **安全第一**: 在修改任何文件前都必须先读取,维护现有的内容和结构
|
||||
3. **清晰性**: 所有操作必须清晰、可理解,避免混乱或不明确的变更
|
||||
|
||||
## 内容创作规范
|
||||
|
||||
1. **结构**:
|
||||
- 优先使用 Markdown 标题格式
|
||||
- 保持一致的段落结构
|
||||
- 合理使用列表来组织信息
|
||||
|
||||
2. **标签**:
|
||||
- 使用 YAML frontmatter 包含适当的标签
|
||||
- 标签必须是数组形式 `tags: [tag1, tag2]`
|
||||
- 尽可能使用相关领域的标签
|
||||
|
||||
3. **链接**:
|
||||
- 内部链接使用 Wiki-links: `[[note-name]]`
|
||||
- 外部链接使用标准 Markdown 格式: `[text](url)`
|
||||
- 确保所有内部链接都指向实际存在的文件
|
||||
|
||||
4. **内容质量**:
|
||||
- 避免使用占位文本,如 "[内容]"、"[描述]" 等
|
||||
- 保持写作风格一致和专业
|
||||
- 根据内容添加相关图表或 Mermaid 图表
|
||||
|
||||
## 文件和路径规范
|
||||
|
||||
1. **路径使用**:
|
||||
- 仅使用相对路径进行文件访问
|
||||
- 路径不应包含绝对路径(如 `/home/...`)
|
||||
|
||||
2. **文件操作**:
|
||||
- 不应在 Vault 内创建 README.md、*.md 等文档文件
|
||||
- 创建新文件时,应遵循现有的文件结构规范
|
||||
|
||||
## Git 和版本控制
|
||||
|
||||
1. **提交操作**:
|
||||
- 不应自动执行任何 Git 提交或推送操作
|
||||
- 如需执行 Git 相关操作,必须让用户明确授权
|
||||
|
||||
2. **变更检查**:
|
||||
- 检查文件变更以维护链接一致性
|
||||
- 发现断链时应记录并报告
|
||||
|
||||
## 特殊情况处理
|
||||
|
||||
1. **知识库层面的处理**:
|
||||
- 若涉及高层架构、系统设计等内容,应优先根据已有文档进行整理补充
|
||||
- 不应创造新的结构或模式,而应遵循既有规范
|
||||
|
||||
2. **代码实现**:
|
||||
- 当需要编写代码示例时,优先使用 Go 语言(后端)或 React/TS(前端)
|
||||
- 所有实现示例应在当前环境中实际可行
|
||||
|
||||
3. **数据可视化**:
|
||||
- 使用 Mermaid 图表,但需确保其正确性和可读性
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
tags: []
|
||||
create time: <% tp.date.now("YYYY-MM-DD HH:mm") %>
|
||||
---
|
||||
|
||||
# <% tp.file.title %>
|
||||
|
||||
## 概述
|
||||
|
||||
添加笔记的概述或简短描述。
|
||||
|
||||
## 正文
|
||||
|
||||
在这里写下你的笔记内容...
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- 添加关联笔记的链接
|
||||
@@ -138,7 +138,7 @@ erDiagram
|
||||
string password "密码(hash)"
|
||||
datetime created_at "创建时间"
|
||||
datetime updated_at "更新时间"
|
||||
datetime deleted_at "删除时间(软删除)"
|
||||
tinyint is_deleted "软删除标记"
|
||||
}
|
||||
WORD {
|
||||
uuid id PK "单词记录ID"
|
||||
@@ -149,7 +149,7 @@ erDiagram
|
||||
string ai_provider "AI模型来源"
|
||||
datetime created_at "创建时间"
|
||||
datetime updated_at "更新时间"
|
||||
datetime deleted_at "删除时间(软删除)"
|
||||
tinyint is_deleted "软删除标记"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -160,7 +160,7 @@ erDiagram
|
||||
- 用户名设置唯一索引,防止重复注册
|
||||
- 密码字段存储 bcrypt 哈希值,严禁明文
|
||||
- 包含标准时间戳字段
|
||||
- 支持软删除(deleted_at)
|
||||
- 支持软删除(is_deleted)
|
||||
|
||||
2. **单词表 (words)**
|
||||
- 使用 UUID 作为主键
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
# Docker 网络数据库连接指南
|
||||
|
||||
## 问题描述
|
||||
|
||||
在后端服务连接运行在 Docker 网络中的数据库时,**不能使用 `localhost`** 作为连接地址。
|
||||
|
||||
## 原因分析
|
||||
|
||||
### 为什么使用 `localhost` 是错误的?
|
||||
|
||||
在 Docker 网络中,每个容器都有**独立的网络栈**:
|
||||
|
||||
- **`localhost` 在容器内部指向容器自身**(127.0.0.1 指向容器内部)
|
||||
- **不是指向同一网络中的其他容器**
|
||||
|
||||
当你的后端容器尝试连接 `localhost:3306` 时,它实际上是在寻找**自己的 3306 端口**,而不是数据库容器的端口。
|
||||
|
||||
#### 直观理解
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Docker 网络(我的网络) │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Backend 容器 │ │ Database 容器 │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ localhost ───────┼──→ │ 自己的 8000 端口 │ │
|
||||
│ │ (127.0.0.1) │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ database ────────┼──→ │ 正确!指向 DB │ │
|
||||
│ │ (服务名) │ │ │ │
|
||||
│ └──────────────────┘ └──────────────────┘ │
|
||||
│ ↑ ↑ │
|
||||
│ └──────────┬───────────────┘ │
|
||||
│ │ │
|
||||
│ 只能通过服务名通信 │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 实际会发生什么
|
||||
|
||||
```bash
|
||||
# 在 backend 容器内执行
|
||||
# 尝试连接 localhost:3306
|
||||
# 结果:Connection refused
|
||||
# 原因:backend 自己没有在 3306 端口监听服务
|
||||
```
|
||||
|
||||
#### 类比说明
|
||||
|
||||
这就好比:
|
||||
- 你和朋友住在同一个大楼(Docker 网络)
|
||||
- 你想找朋友
|
||||
- 你大喊"我在这里"(使用 `localhost`)
|
||||
- 但实际上你需要喊的是"小明"(使用服务的名字)
|
||||
- 因为 `"我在这里"` 指的是你自己,不是小明
|
||||
|
||||
### Docker 的网络隔离机制
|
||||
|
||||
Docker 容器默认使用以下网络模式:
|
||||
|
||||
1. **Bridge 网络模式**(默认):
|
||||
- 每个容器有独立的 IP 地址
|
||||
- 容器间通过服务名互相发现
|
||||
- `localhost` 仅指向容器自身
|
||||
|
||||
2. **容器间的 DNS 解析**:
|
||||
```
|
||||
# 在 backend 容器内
|
||||
$ ping database
|
||||
PING database (172.19.0.2) // 解析到数据库容器的 IP
|
||||
|
||||
$ ping localhost
|
||||
PING localhost (127.0.0.1) // 指向自己
|
||||
```
|
||||
|
||||
3. **端口监听位置**:
|
||||
```
|
||||
Database 容器内部:MySQL 监听 0.0.0.0:3306
|
||||
Database 容器 IP:172.19.0.2
|
||||
Backend 容器不能访问:127.0.0.1:3306(自己的端口)
|
||||
Backend 容器应该访问:172.19.0.2:3306(通过服务名解析)
|
||||
```
|
||||
|
||||
### 结论
|
||||
|
||||
**使用 `localhost` 连接数据库必然失败**,因为它:
|
||||
- 永远指向调用方容器自己
|
||||
- 无法跨容器通信
|
||||
- 违反了 Docker 的网络隔离设计
|
||||
|
||||
**正确做法:使用服务名或容器名**,这样 Docker 的内置 DNS 才能正确解析到目标容器。
|
||||
|
||||
## 正确做法
|
||||
|
||||
### 1. 使用服务名(Service Name)
|
||||
|
||||
在 `docker-compose.yml` 中定义的服务名即可用作主机名:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
backend:
|
||||
build: ./backend
|
||||
ports:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
database:
|
||||
image: mysql:8.0
|
||||
ports:
|
||||
- "3306:3306"
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: rootpassword
|
||||
```
|
||||
|
||||
**连接配置:**
|
||||
```python
|
||||
# 后端连接配置
|
||||
DATABASE_URL = "mysql://user:password@database:3306/dbname"
|
||||
# ^^^^^^^^ 使用服务名 "database"
|
||||
```
|
||||
|
||||
### 2. 使用容器名
|
||||
|
||||
在 docker-compose 中为容器指定名称:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
image: mysql:8.0
|
||||
container_name: my-mysql-db # 显式指定容器名
|
||||
```
|
||||
|
||||
**连接配置:**
|
||||
```python
|
||||
DATABASE_URL = "mysql://user:password@my-mysql-db:3306/dbname"
|
||||
```
|
||||
|
||||
### 3. 网络别名(Network Alias)
|
||||
|
||||
如果有多个服务需要连接,可以设置网络别名:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
image: mysql:8.0
|
||||
networks:
|
||||
appnet:
|
||||
aliases:
|
||||
- db
|
||||
- mysql
|
||||
|
||||
networks:
|
||||
appnet:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
**连接配置:**
|
||||
```python
|
||||
# 可以使用任一别名
|
||||
DATABASE_URL = "mysql://user:password@db:3306/dbname"
|
||||
# 或
|
||||
DATABASE_URL = "mysql://user:password@mysql:3306/dbname"
|
||||
```
|
||||
|
||||
## 常见错误
|
||||
|
||||
### ❌ 错误写法
|
||||
```python
|
||||
# 这会导致连接失败
|
||||
DATABASE_URL = "mysql://user:password@localhost:3306/dbname"
|
||||
# ^^^^^^^^^ 指向 backend 自己
|
||||
```
|
||||
|
||||
### ✅ 正确写法
|
||||
```python
|
||||
# 使用 docker-compose 中的服务名
|
||||
DATABASE_URL = "mysql://user:password@database:3306/dbname"
|
||||
# ^^^^^^^^ 数据库服务名
|
||||
```
|
||||
|
||||
## 实际示例
|
||||
|
||||
### 示例 1:Spring Boot + MySQL
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3.8'
|
||||
services:
|
||||
springboot-app:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
SPRING_DATASOURCE_URL: jdbc:mysql://mysql-db:3306/mydb
|
||||
SPRING_DATASOURCE_USERNAME: user
|
||||
SPRING_DATASOURCE_PASSWORD: password
|
||||
depends_on:
|
||||
- mysql-db
|
||||
|
||||
mysql-db:
|
||||
image: mysql:8.0
|
||||
environment:
|
||||
MYSQL_DATABASE: mydb
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: password
|
||||
MYSQL_ROOT_PASSWORD: rootpassword
|
||||
ports:
|
||||
- "3306:3306"
|
||||
```
|
||||
|
||||
### 示例 2:Node.js + PostgreSQL
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3.8'
|
||||
services:
|
||||
api:
|
||||
build: ./api
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
DATABASE_HOST: postgres
|
||||
DATABASE_PORT: 5432
|
||||
DATABASE_NAME: myapp
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
postgres:
|
||||
image: postgres:14
|
||||
environment:
|
||||
POSTGRES_DB: myapp
|
||||
POSTGRES_USER: user
|
||||
POSTGRES_PASSWORD: password
|
||||
ports:
|
||||
- "5432:5432"
|
||||
```
|
||||
|
||||
```javascript
|
||||
// .env
|
||||
DATABASE_URL=postgres://user:password@postgres:5432/myapp
|
||||
// ^^^^^^^^ 服务名
|
||||
```
|
||||
|
||||
### 示例 3:Python/Django + MongoDB
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3.8'
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
MONGO_HOST: mongodb
|
||||
MONGO_PORT: 27017
|
||||
depends_on:
|
||||
- mongodb
|
||||
|
||||
mongodb:
|
||||
image: mongo:6.0
|
||||
ports:
|
||||
- "27017:27017"
|
||||
```
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
MONGO_URI = "mongodb://mongodb:27017/mydb"
|
||||
# ^^^^^^^^ 使用服务名
|
||||
```
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 1. 进入容器测试连接
|
||||
```bash
|
||||
# 进入后端容器
|
||||
docker exec -it <backend-container> bash
|
||||
|
||||
# 测试数据库连接
|
||||
ping database
|
||||
telnet database 3306
|
||||
nc -zv database 3306
|
||||
```
|
||||
|
||||
### 2. 查看网络信息
|
||||
```bash
|
||||
# 查看容器网络
|
||||
docker network inspect <network-name>
|
||||
|
||||
# 查看容器 IP 和别名
|
||||
docker inspect <database-container> --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
|
||||
```
|
||||
|
||||
### 3. 使用 docker network 连接
|
||||
```bash
|
||||
# 从宿主机通过容器 IP 连接(不推荐)
|
||||
docker inspect <database-container> | grep IPAddress
|
||||
|
||||
# 正确方式:从另一个容器连接
|
||||
docker run --rm --network <network-name> mysql:8.0 \
|
||||
mysql -h database -u user -ppassword dbname
|
||||
```
|
||||
|
||||
## 特殊情况
|
||||
|
||||
### 从宿主机连接数据库
|
||||
|
||||
如果需要从**宿主机**(非 Docker 容器)连接数据库,使用:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
image: mysql:8.0
|
||||
ports:
|
||||
- "3306:3306" # 映射到宿主机
|
||||
```
|
||||
|
||||
**连接方式:**
|
||||
```python
|
||||
# 从宿主机运行的应用
|
||||
DATABASE_URL = "mysql://user:password@localhost:3306/dbname"
|
||||
```
|
||||
|
||||
**注意:** 这是唯一可以使用 `localhost` 的情况——从宿主机连接暴露端口的容器。
|
||||
|
||||
### 开发环境 vs 生产环境
|
||||
|
||||
```python
|
||||
# 开发环境:可能直接使用本地数据库
|
||||
if os.getenv("ENVIRONMENT") == "development":
|
||||
DATABASE_URL = "mysql://user:password@localhost:3306/devdb"
|
||||
else:
|
||||
# 生产环境:使用 Docker 网络中的数据库
|
||||
DATABASE_URL = "mysql://user:password@database:3306/proddb"
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用环境变量**:将数据库连接配置为环境变量,便于在不同环境间切换
|
||||
2. **使用服务名**:优先使用 docker-compose 中的服务名,保持配置一致性
|
||||
3. **健康检查**:使用 `depends_on` + 健康检查确保数据库就绪后再启动应用
|
||||
4. **网络隔离**:为不同环境使用不同的网络
|
||||
5. **安全性**:不要在代码中硬编码密码,使用 secrets 或环境变量
|
||||
|
||||
## 故障排查清单
|
||||
|
||||
- [ ] 检查 docker-compose.yml 中数据库服务的名称
|
||||
- [ ] 确认后端使用的是服务名而非 `localhost`
|
||||
- [ ] 确认数据库服务在同一个网桥网络中
|
||||
- [ ] 检查端口映射配置(如果从宿主机连接)
|
||||
- [ ] 验证容器间的网络连通性(使用 `ping` 或 `telnet`)
|
||||
- [ ] 检查防火墙规则
|
||||
- [ ] 查看容器日志确认数据库是否正常启动
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Docker Networking](https://docs.docker.com/network/)
|
||||
- [Docker Compose Service Discovery](https://docs.docker.com/compose/networking/)
|
||||
- [Connect to MySQL from another container](https://hub.docker.com/_/mysql)
|
||||
|
||||
---
|
||||
|
||||
**最后更新:** 2026-04-21
|
||||
@@ -0,0 +1,545 @@
|
||||
---
|
||||
tags: [go, configuration, environment, viper, godotenv, assignment]
|
||||
create time: 2026-04-21
|
||||
---
|
||||
|
||||
# Viper 和 Godotenv 对比分析
|
||||
|
||||
## 概述
|
||||
|
||||
Go 语言中有两个常用的配置管理库:**Viper** 和 **Godotenv**。虽然它们都与配置管理相关,但设计目标和使用场景存在显著差异。
|
||||
|
||||
- **Viper**: 完整的配置解决方案,支持多种配置源
|
||||
- **Godotenv**: 专注于从 `.env` 文件加载环境变量
|
||||
|
||||
## 核心对比
|
||||
|
||||
| 特性 | Viper | Godotenv |
|
||||
|-----|-------|----------|
|
||||
| **主要用途** | 配置管理框架 | 环境变量加载 |
|
||||
| **配置源数量** | 多种(JSON/YAML/ENV/Flags) | 仅 `.env` 文件 |
|
||||
| **功能范围** | 完整配置系统 | 单一功能库 |
|
||||
| **代码复杂度** | 较大 | 简洁 |
|
||||
| **学习曲线** | 较复杂 | 简单 |
|
||||
| **适用场景** | 复杂应用配置 | 简单环境变量管理 |
|
||||
|
||||
## 设计理念对比
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Viper - 配置管理框架"
|
||||
A1[JSON] --> A[统一配置 API]
|
||||
B1[YAML] --> A
|
||||
C1[TOML] --> A
|
||||
D1[环境变量] --> A
|
||||
E1[命令行参数] --> A
|
||||
A --> F[配置读取/更新/监听]
|
||||
end
|
||||
|
||||
subgraph "Godotenv - 环境变量加载"
|
||||
G[.env 文件] --> H[解析]
|
||||
H --> I[设置环境变量]
|
||||
end
|
||||
|
||||
style A fill:#e1f5ff
|
||||
style I fill:#fff4e1
|
||||
```
|
||||
|
||||
## Viper 详解
|
||||
|
||||
### 功能特性
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
root((Viper))
|
||||
配置源
|
||||
JSON
|
||||
YAML
|
||||
TOML
|
||||
环境变量
|
||||
命令行参数
|
||||
功能
|
||||
自动重载
|
||||
远程配置中心
|
||||
配置合并
|
||||
结构体绑定
|
||||
优势
|
||||
多源配置
|
||||
优先级管理
|
||||
类型安全
|
||||
```
|
||||
|
||||
### 基础使用
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func viperExample() {
|
||||
// 1. 初始化
|
||||
v := viper.New()
|
||||
|
||||
// 2. 设置配置文件 (自动查找 app.json/app.yaml 等)
|
||||
v.SetConfigName("config")
|
||||
v.SetConfigType("yaml")
|
||||
v.AddConfigPath(".")
|
||||
v.AddConfigPath("./config")
|
||||
|
||||
// 3. 读取配置文件
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
panic(fmt.Errorf("读取配置失败: %w", err))
|
||||
}
|
||||
|
||||
// 4. 读取配置值
|
||||
dbHost := v.GetString("database.host")
|
||||
dbPort := v.GetInt("database.port")
|
||||
debug := v.GetBool("debug")
|
||||
|
||||
fmt.Printf("数据库: %s:%d, Debug: %v\n", dbHost, dbPort, debug)
|
||||
}
|
||||
```
|
||||
|
||||
### 结构体绑定
|
||||
|
||||
```go
|
||||
func structBindingExample() {
|
||||
type Config struct {
|
||||
AppName string `mapstructure:"app_name"`
|
||||
Debug bool `mapstructure:"debug"`
|
||||
|
||||
Database struct {
|
||||
Host string `mapstructure:"host"`
|
||||
Port int `mapstructure:"port"`
|
||||
User string `mapstructure:"user"`
|
||||
Password string `mapstructure:"password"`
|
||||
} `mapstructure:"database"`
|
||||
}
|
||||
|
||||
var config Config
|
||||
|
||||
if err := viper.Unmarshal(&config); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("应用: %s, 数据库: %s:%d\n",
|
||||
config.AppName, config.Database.Host, config.Database.Port)
|
||||
}
|
||||
```
|
||||
|
||||
### 多配置源优先级
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[默认值] --> B[配置文件]
|
||||
B --> C[环境变量]
|
||||
C --> D[命令行参数]
|
||||
D --> E[最终配置]
|
||||
|
||||
style A fill:#f9f9f9
|
||||
style E fill:#66bb6a,color:#fff
|
||||
```
|
||||
|
||||
```go
|
||||
func multiSourceExample() {
|
||||
v := viper.New()
|
||||
|
||||
// 1. 设置默认值 (最低优先级)
|
||||
v.SetDefault("server.port", 8080)
|
||||
v.SetDefault("log.level", "info")
|
||||
|
||||
// 2. 读取配置文件
|
||||
v.SetConfigFile("config.yaml")
|
||||
v.ReadInConfig()
|
||||
|
||||
// 3. 绑定环境变量 (可覆盖配置文件)
|
||||
v.SetEnvPrefix("APP") // APP_SERVER_PORT
|
||||
v.BindEnv("server.port")
|
||||
|
||||
// 4. 绑定命令行参数 (最高优先级, 使用 pflag)
|
||||
pflag.Int("port", 0, "服务器端口")
|
||||
pflag.Parse()
|
||||
v.BindPFlags(pflag.CommandLine)
|
||||
|
||||
// 最终取值: 命令行参数 > 环境变量 > 配置文件 > 默认值
|
||||
}
|
||||
```
|
||||
|
||||
### 自动重载配置
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
)
|
||||
|
||||
func watchConfig() {
|
||||
v := viper.New()
|
||||
v.SetConfigFile("config.yaml")
|
||||
v.ReadInConfig()
|
||||
|
||||
// 监听配置文件变化
|
||||
v.WatchConfig()
|
||||
v.OnConfigChange(func(e fsnotify.Event) {
|
||||
fmt.Printf("配置文件已更改: %s\n", e.Name)
|
||||
// 自动重载, 无需手动调用 ReadInConfig
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 配置文件示例 (config.yaml)
|
||||
|
||||
```yaml
|
||||
app_name: "my-app"
|
||||
debug: true
|
||||
|
||||
database:
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
user: "admin"
|
||||
password: "secret"
|
||||
|
||||
redis:
|
||||
addr: "localhost:6379"
|
||||
pool_size: 10
|
||||
|
||||
log:
|
||||
level: "debug"
|
||||
format: "json"
|
||||
```
|
||||
|
||||
## Godotenv 详解
|
||||
|
||||
### 功能特性
|
||||
|
||||
- ✅ 从 `.env` 文件加载环境变量
|
||||
- ✅ 支持注释 (# 开头)
|
||||
- ✅ 支持引号和换行
|
||||
- ❌ 不支持配置优先级
|
||||
- ❌ 不支持远程配置
|
||||
- ❌ 不支持自动重载
|
||||
|
||||
### 基础使用
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
func godotenvExample() {
|
||||
// 1. 加载 .env 文件到环境变量
|
||||
if err := godotenv.Load(); err != nil {
|
||||
log.Fatal("加载 .env 文件失败", err)
|
||||
}
|
||||
|
||||
// 2. 直接读取环境变量
|
||||
dbHost := os.Getenv("DB_HOST")
|
||||
dbPort := os.Getenv("DB_PORT")
|
||||
apiKey := os.Getenv("API_KEY")
|
||||
|
||||
fmt.Printf("数据库: %s:%s, API Key: %s\n", dbHost, dbPort, apiKey)
|
||||
}
|
||||
```
|
||||
|
||||
### 指定文件路径
|
||||
|
||||
```go
|
||||
// 加载指定路径的 .env 文件
|
||||
godotenv.Load(".env.production")
|
||||
godotenv.Load("/path/to/.env")
|
||||
|
||||
// 加载多个 .env 文件 (后加载的会覆盖先加载的)
|
||||
godotenv.Load(".env", ".env.local", ".env.secrets")
|
||||
```
|
||||
|
||||
### .env 文件示例
|
||||
|
||||
```bash
|
||||
# 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=admin
|
||||
DB_PASSWORD=secret123
|
||||
|
||||
# API 配置
|
||||
API_BASE_URL=https://api.example.com
|
||||
API_KEY=sk-1234567890abcdef
|
||||
|
||||
# 应用配置
|
||||
APP_NAME=my-app
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
```
|
||||
|
||||
### 配合 Viper 使用
|
||||
|
||||
```go
|
||||
func combinedExample() {
|
||||
// 1. 先用 godotenv 加载 .env
|
||||
godotenv.Load()
|
||||
|
||||
// 2. 再用 viper 读取环境变量
|
||||
viper.AutomaticEnv() // 自动读取环境变量
|
||||
|
||||
// 3. 可以设置默认值
|
||||
viper.SetDefault("DB_PORT", 5432)
|
||||
|
||||
// 4. 读取配置
|
||||
dbHost := viper.GetString("DB_HOST")
|
||||
dbPort := viper.GetInt("DB_PORT")
|
||||
}
|
||||
```
|
||||
|
||||
## 使用场景分析
|
||||
|
||||
### 适用 Viper 的场景
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[复杂应用配置] --> B[多配置源]
|
||||
A --> C[需要优先级管理]
|
||||
A --> D[结构化配置文件]
|
||||
A --> E[配置热更新]
|
||||
|
||||
B --> B1[JSON/YAML/TOML]
|
||||
C --> C1[默认值 < 文件 < ENV < CLI]
|
||||
D --> D1[嵌套结构]
|
||||
E --> E1[监听文件变化]
|
||||
```
|
||||
|
||||
**典型应用:**
|
||||
- 微服务配置管理
|
||||
- 需要多种配置来源的应用
|
||||
- 需要配置优先级和覆盖机制
|
||||
- 需要远程配置中心集成的应用
|
||||
|
||||
### 适用 Godotenv 的场景
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[简单环境变量管理] --> B[12-Factor App]
|
||||
A --> C[Docker 容器化]
|
||||
A --> D[开发环境隔离]
|
||||
|
||||
B --> B1[配置即代码]
|
||||
C --> C1[容器环境变量]
|
||||
D --> D1[.env.dev / .env.prod]
|
||||
```
|
||||
|
||||
**典型应用:**
|
||||
- 12-Factor 应用
|
||||
- Docker/Kubernetes 部署
|
||||
- 简单脚本或小型应用
|
||||
- 仅需要环境变量的项目
|
||||
|
||||
## 代码对比示例
|
||||
|
||||
### 场景: 从配置文件读取数据库配置
|
||||
|
||||
#### 使用 Viper
|
||||
|
||||
```go
|
||||
// config.yaml
|
||||
/*
|
||||
database:
|
||||
host: localhost
|
||||
port: 5432
|
||||
user: admin
|
||||
password: secret123
|
||||
*/
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
|
||||
// 代码
|
||||
viper.SetConfigFile("config.yaml")
|
||||
viper.ReadInConfig()
|
||||
|
||||
var config DatabaseConfig
|
||||
viper.UnmarshalKey("database", &config)
|
||||
|
||||
db, err := sql.Open("postgres",
|
||||
fmt.Sprintf("%s:%s@%s:%d/mydb",
|
||||
config.User, config.Password, config.Host, config.Port))
|
||||
```
|
||||
|
||||
#### 使用 Godotenv
|
||||
|
||||
```go
|
||||
// .env
|
||||
/*
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=admin
|
||||
DB_PASSWORD=secret123
|
||||
*/
|
||||
|
||||
// 代码
|
||||
godotenv.Load()
|
||||
|
||||
db, err := sql.Open("postgres",
|
||||
fmt.Sprintf("%s:%s@%s:%s/mydb",
|
||||
os.Getenv("DB_USER"),
|
||||
os.Getenv("DB_PASSWORD"),
|
||||
os.Getenv("DB_HOST"),
|
||||
os.Getenv("DB_PORT")))
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### Viper 最佳实践
|
||||
|
||||
```go
|
||||
// 1. 封装配置加载
|
||||
func LoadConfig() (*Config, error) {
|
||||
v := viper.New()
|
||||
|
||||
// 设置配置文件默认路径
|
||||
v.SetConfigFile("config/config.yaml")
|
||||
|
||||
// 允许环境变量覆盖
|
||||
v.SetEnvPrefix("APP")
|
||||
v.AutomaticEnv()
|
||||
|
||||
if err := v.ReadInConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var cfg Config
|
||||
if err := v.Unmarshal(&cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// 2. 配置验证
|
||||
func (c *Config) Validate() error {
|
||||
if c.Database.Host == "" {
|
||||
return errors.New("数据库主机不能为空")
|
||||
}
|
||||
if c.Database.Port <= 0 || c.Database.Port > 65535 {
|
||||
return errors.New("端口号无效")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Godotenv 最佳实践
|
||||
|
||||
```bash
|
||||
# .env.example (提交到版本控制)
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_USER=admin
|
||||
DB_PASSWORD=
|
||||
|
||||
# .env (不提交,加入 .gitignore)
|
||||
DB_HOST=production-db.example.com
|
||||
DB_PORT=5432
|
||||
DB_USER=admin
|
||||
DB_PASSWORD=real_secret_password
|
||||
```
|
||||
|
||||
```go
|
||||
// 1. 提供默认值
|
||||
func getEnv(key, defaultValue string) string {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// 2. 使用
|
||||
dbHost := getEnv("DB_HOST", "localhost")
|
||||
dbPort := getEnv("DB_PORT", "5432")
|
||||
```
|
||||
|
||||
### 混合使用策略
|
||||
|
||||
```go
|
||||
// config.yaml (结构化配置)
|
||||
app:
|
||||
name: "my-app"
|
||||
version: "1.0.0"
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
// .env (敏感信息)
|
||||
DB_PASSWORD=secret123
|
||||
API_SECRET=xyz789
|
||||
|
||||
// 代码
|
||||
func main() {
|
||||
// 1. 加载环境变量 (敏感配置)
|
||||
godotenv.Load()
|
||||
|
||||
// 2. 加载配置文件 (结构化配置)
|
||||
viper.SetConfigFile("config.yaml")
|
||||
viper.ReadInConfig()
|
||||
|
||||
// 3. 合并配置
|
||||
viper.BindEnv("database.password", "DB_PASSWORD")
|
||||
viper.BindEnv("api.secret", "API_SECRET")
|
||||
|
||||
// 4. 读取最终配置
|
||||
config := loadConfig()
|
||||
|
||||
// 在代码中,结构化配置在 YAML,敏感配置在 .env
|
||||
}
|
||||
```
|
||||
|
||||
## 项目选择建议
|
||||
|
||||
### 选择 Viper,如果你需要:
|
||||
|
||||
- ✅ 支持多种配置格式 (JSON/YAML/TOML)
|
||||
- ✅ 配置优先级管理
|
||||
- ✅ 结构体绑定和类型安全
|
||||
- ✅ 配置热更新
|
||||
- ✅ 远程配置中心集成
|
||||
- ✅ 命令行参数绑定
|
||||
|
||||
### 选择 Godotenv,如果你需要:
|
||||
|
||||
- ✅ 12-Factor 应用规范
|
||||
- ✅ Docker/Kubernetes 部署
|
||||
- ✅ 极简配置管理
|
||||
- ✅ 环境变量即可满足需求
|
||||
- ✅ 团队熟悉 `.env` 工作流
|
||||
- ✅ 依赖最小化
|
||||
|
||||
## 学习资源
|
||||
|
||||
### Viper
|
||||
|
||||
- [Viper 官方文档](https://github.com/spf13/viper)
|
||||
- [Viper 使用指南](https://www.sohamkamani.com/golang/viper-guide/)
|
||||
- [Go 配置管理最佳实践](https://sagikazarmark.hu/blog/2022/03/02/manage-go-configuration/)
|
||||
|
||||
### Godotenv
|
||||
|
||||
- [Godotenv 官方文档](https://github.com/joho/godotenv)
|
||||
- [12-Factor App - Config](https://12factor.net/config)
|
||||
- [.env 文件规范](https://dotenv.org/)
|
||||
|
||||
## 相关笔记
|
||||
|
||||
- [[CS/LANG/Go/标准库/标准库]] - Go 语言标准库
|
||||
- [[CS/DevOps/Docker/基础]] - Docker 容器化
|
||||
- [[CS/NET/Auth/密钥管理]] - 配置安全最佳实践
|
||||
- [[金山办公作业/Week05/数据库设计]] - 数据库配置详解
|
||||
+23
-23
@@ -20,7 +20,7 @@ erDiagram
|
||||
string password "密码(hash)"
|
||||
datetime created_at "创建时间"
|
||||
datetime updated_at "更新时间"
|
||||
datetime deleted_at "删除时间(软删除)"
|
||||
tinyint is_deleted "软删除标记"
|
||||
}
|
||||
WORD {
|
||||
uuid id PK "单词记录ID"
|
||||
@@ -31,7 +31,7 @@ erDiagram
|
||||
string ai_provider "AI模型来源"
|
||||
datetime created_at "创建时间"
|
||||
datetime updated_at "更新时间"
|
||||
datetime deleted_at "删除时间(软删除)"
|
||||
tinyint is_deleted "软删除标记"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -49,7 +49,7 @@ erDiagram
|
||||
| `password` | VARCHAR(255) | NOT NULL | - | 用户密码,存储 bcrypt 哈希值,严禁明文存储 |
|
||||
| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | - | 账户创建时间 |
|
||||
| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | - | 账户最后更新时间 |
|
||||
| `deleted_at` | DATETIME | DEFAULT NULL | INDEX | 软删除标记,NULL 表示未删除 |
|
||||
| `is_deleted` | TINYINT | DEFAULT 0 | INDEX | 软删除标记,0 表示未删除,1 表示已删除 |
|
||||
|
||||
**索引设计:**
|
||||
- 主键索引:`id`
|
||||
@@ -60,23 +60,23 @@ erDiagram
|
||||
| 单词表名 | `words` |
|
||||
|-----------|---------|
|
||||
|
||||
| 字段名 | 数据类型 | 约束 | 索引 | 说明 |
|
||||
| ------------- | ------------ | --------------------------- | ------------------------- | ------------------------------------------ |
|
||||
| `id` | CHAR(36) | PRIMARY KEY | - | 单词记录唯一标识符(UUID 格式) |
|
||||
| `user_id` | CHAR(36) | NOT NULL, FOREIGN KEY | - | 所属用户 ID,关联 users.id |
|
||||
| `word` | VARCHAR(100) | NOT NULL | INDEX idx_word_search(20) | 单词文本,支持前缀搜索优化 |
|
||||
| `definition` | TEXT | NOT NULL | - | AI 生成的单词释义 |
|
||||
| `examples` | JSON | NOT NULL | - | 例句列表,存储 JSON 数组格式 |
|
||||
| `ai_provider` | ENUM | NOT NULL | - | AI 模型来源标识(`deepseek`=DeepSeek,`qwen`=通义千问) |
|
||||
| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | - | 单词记录创建时间 |
|
||||
| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | - | 单词记录最后更新时间 |
|
||||
| `deleted_at` | DATETIME | DEFAULT NULL | INDEX idx_user_word | 软删除标记 |
|
||||
| 字段名 | 数据类型 | 约束 | 索引 | 说明 |
|
||||
|--------|----------|------|------|------|
|
||||
| `id` | CHAR(36) | PRIMARY KEY | - | 单词记录唯一标识符(UUID 格式) |
|
||||
| `user_id` | CHAR(36) | NOT NULL, FOREIGN KEY | - | 所属用户 ID,关联 users.id |
|
||||
| `word` | VARCHAR(100) | NOT NULL | INDEX idx_word_search(20) | 单词文本,支持前缀搜索优化 |
|
||||
| `definition` | TEXT | NOT NULL | - | AI 生成的单词释义 |
|
||||
| `examples` | JSON | NOT NULL | - | 例句列表,存储 JSON 数组格式 |
|
||||
| `ai_provider` | ENUM | NOT NULL | - | AI 模型来源标识(`deepseek`=DeepSeek,`qwen`=通义千问) |
|
||||
| `created_at` | DATETIME | DEFAULT CURRENT_TIMESTAMP | - | 单词记录创建时间 |
|
||||
| `updated_at` | DATETIME | ON UPDATE CURRENT_TIMESTAMP | - | 单词记录最后更新时间 |
|
||||
| `is_deleted` | TINYINT | DEFAULT 0 | INDEX idx_user_word | 软删除标记 |
|
||||
|
||||
**索引设计:**
|
||||
- 主键索引:`id`
|
||||
- 外键索引:`user_id`(自动创建)
|
||||
- 唯一索引:`uk_user_word` (user_id, word(50)) - 防止同一用户保存重复单词
|
||||
- 复合索引:`idx_user_word` (user_id, deleted_at) - 优化用户单词列表分页查询
|
||||
- 复合索引:`idx_user_word` (user_id, is_deleted) - 优化用户单词列表分页查询
|
||||
- 前缀索引:`idx_word_search` (word(20)) - 支持单词前缀搜索优化
|
||||
|
||||
## 四、表关联关系
|
||||
@@ -97,15 +97,15 @@ erDiagram
|
||||
```sql
|
||||
-- 查询某用户的所有单词(排除已删除)
|
||||
SELECT * FROM words
|
||||
WHERE user_id = ? AND deleted_at IS NULL
|
||||
WHERE user_id = ? AND is_deleted = 0
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- 联表查询用户信息及单词数量
|
||||
SELECT u.id, u.username, COUNT(w.id) as word_count
|
||||
FROM users u
|
||||
LEFT JOIN words w ON u.id = w.user_id AND w.deleted_at IS NULL
|
||||
WHERE u.deleted_at IS NULL
|
||||
LEFT JOIN words w ON u.id = w.user_id AND w.is_deleted = 0
|
||||
WHERE u.is_deleted = 0
|
||||
GROUP BY u.id;
|
||||
```
|
||||
|
||||
@@ -126,7 +126,7 @@ GROUP BY u.id;
|
||||
-- 查询:检查用户是否已保存该单词
|
||||
SELECT id, word, definition, examples, ai_provider
|
||||
FROM words
|
||||
WHERE user_id = ? AND word = ? AND deleted_at IS NULL;
|
||||
WHERE user_id = ? AND word = ? AND is_deleted = 0;
|
||||
```
|
||||
|
||||
### 5.2 手动保存单词
|
||||
@@ -147,14 +147,14 @@ VALUES (?, ?, ?, ?, ?, ?);
|
||||
-- 查询:分页获取单词列表(按创建时间倒序)
|
||||
SELECT id, word, definition, examples, ai_provider, created_at
|
||||
FROM words
|
||||
WHERE user_id = ? AND deleted_at IS NULL
|
||||
WHERE user_id = ? AND is_deleted = 0
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?;
|
||||
|
||||
-- 计算总数(用于分页器)
|
||||
SELECT COUNT(*) as total
|
||||
FROM words
|
||||
WHERE user_id = ? AND deleted_at IS NULL;
|
||||
WHERE user_id = ? AND is_deleted = 0;
|
||||
```
|
||||
|
||||
### 5.4 删除单词(软删除)
|
||||
@@ -164,8 +164,8 @@ WHERE user_id = ? AND deleted_at IS NULL;
|
||||
```sql
|
||||
-- 更新:软删除单词记录
|
||||
UPDATE words
|
||||
SET deleted_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND user_id = ? AND deleted_at IS NULL;
|
||||
SET is_deleted = 1, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND user_id = ? AND is_deleted = 0;
|
||||
```
|
||||
|
||||
## 六、索引优化说明
|
||||
|
||||
Reference in New Issue
Block a user