Repository navigation
Expand file tree
/
Copy pathalgolia.json
More file actions
157 lines (157 loc) · 50.4 KB
/
Copy pathalgolia.json
File metadata and controls
157 lines (157 loc) · 50.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
[
{
"objectID": "64d0d9816d4af5a37395fb0cec4c1afd81cee086",
"permalink": "/post/k8s/nginx-ingress/tke%E5%8D%87%E7%BA%A7%E8%AE%B0%E5%BD%95/",
"title": "TKE 集群 Nginx Ingress 升级部署全记录:从远程连接到 Auth 鉴权配置","content": "记录一次在腾讯云 TKE 集群上,通过 Helm 升级部署 Nginx Ingress Controller 的完整过程,包括远程连接集群、Helm Values 配置要点,以及利用 auth_request 实现统一鉴权的 Ingress 配置方案。\n1. 远程连接 TKE 集群 首先需要在 TKE 控制台开启公网访问,下载集群的 kubeconfig 凭证文件,然后在本地配置:\n# 将 TKE 凭证加入 KUBECONFIG export KUBECONFIG=$KUBECONFIG:/home/akf/下载/tke/cls-g9mon6gq-config # 查看可用的 context kubectl config --kubeconfig=/home/akf/下载/tke/cls-g9mon6gq-config get-contexts # 切换到目标集群 context kubectl config --kubeconfig=/home/akf/下载/tke/cls-g9mon6gq-config use-context cls-g9mon6gq-100045431505-context-default 连接成功后,就可以在本地通过 kubectl 操作远程 TKE 集群了。\n2. 通过 Helm 部署 Nginx Ingress Controller 2.1 添加 Helm 仓库 helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update # 查看已添加的仓库 helm repo list # 搜索可用的 chart 版本 helm search repo ingress-nginx 2.2 安装/升级 Nginx Ingress helm upgrade --install prod-nginx ingress-nginx/ingress-nginx \\ --namespace ingress-nginx --create-namespace \\ --set controller.ingressClass=ingress-new \\ --set …","date": "2026-02-24 00:00:00",
"updated": "2026-02-24 00:00:00"
},
{
"objectID": "1cbe67ce390bd2f9a9b3300e383e38f93d36bb9f",
"permalink": "/post/golang/tencent-sms-service/",
"title": "Go 腾讯云短信服务封装:验证码发送完整指南","content": "在用户认证场景中,短信验证码是常见的身份验证方式。本文介绍如何基于腾讯云 SMS SDK 封装一个简洁易用的短信发送服务,支持多种业务场景的验证码发送。\n概述 这个短信服务封装了腾讯云 SMS SDK,主要用于发送验证码短信,支持以下场景:\n用户注册 用户登录 重置密码 绑定/修改手机号 账号注销 依赖安装 go get github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111 YAML 配置 在配置文件中添加腾讯云短信相关配置:\nTencentSMS: SecretId: \u0026amp;#34;your-secret-id\u0026amp;#34; # 腾讯云 API 密钥 ID SecretKey: \u0026amp;#34;your-secret-key\u0026amp;#34; # 腾讯云 API 密钥 Key AppID: \u0026amp;#34;1400xxxxxx\u0026amp;#34; # 短信应用 ID SignName: \u0026amp;#34;你的签名\u0026amp;#34; # 短信签名 (需腾讯云审核通过) TemplateIdBind: \u0026amp;#34;xxxxxxx\u0026amp;#34; # 绑定手机模板 ID TemplateIdRegister: \u0026amp;#34;xxxxxxx\u0026amp;#34; # 注册验证码模板 ID TemplateIdLogin: \u0026amp;#34;xxxxxxx\u0026amp;#34; # 登录验证码模板 ID TemplateIdResetPassword: \u0026amp;#34;xxxxxxx\u0026amp;#34; # 重置密码模板 ID TemplateIdModifyPhone: \u0026amp;#34;xxxxxxx\u0026amp;#34; # 修改手机号模板 ID TemplateIdDeletion: \u0026amp;#34;xxxxxxx\u0026amp;#34; # 账号注销模板 ID 配置项说明 配置项 类型 必填 说明 SecretId string 是 腾讯云 API 密钥 ID SecretKey string 是 腾讯云 API 密钥 Key AppID string 是 短信应用 ID SignName string 是 短信签名内容,需审核通过 TemplateId* string 否 各场景的模板 ID 获取配置值 SecretId / SecretKey: 腾讯云控制台 -\u0026amp;gt; 访问管理 -\u0026amp;gt; API密钥管理 AppID: 腾 …","date": "2026-02-03 00:00:00",
"updated": "2026-02-03 00:00:00"
},
{
"objectID": "6454bf1d3818e13ed23cfb6b0cca1cda896cecd5",
"permalink": "/post/devenv/k3s-install-kubectl/",
"title": "在本地单机快速搭建 Kubernetes:k3s 从安装到彻底用 kubectl 命令","content": "最近在本地机器上折腾轻量级 Kubernetes 时,发现 k3s 仍然是目前最省心、最省资源的单机/边缘/开发环境选择。\n但很多人(包括我一开始)都会遇到一个很烦人的问题:\n为什么每次都要打 sudo k3s kubectl get pods 这么长一串?有没有办法像正常 k8s 集群一样,直接敲 kubectl 就行?\n本文就完整记录一次从 零开始安装 k3s 到 彻底改成只用 kubectl 的全过程,适用于 Ubuntu/Debian/CentOS 等主流 Linux 系统。\n1. 安装 k3s(最简单一行命令) 官方推荐的安装方式超级简洁:\ncurl -sfL https://get.k3s.io | sh - 这条命令会做这些事:\n下载最新稳定版 k3s 二进制 使用 containerd 作为容器运行时(不会干扰已有的 Docker) 安装成 systemd 服务(开机自启) 自动创建一个单节点集群(server + agent 合一) kubeconfig 文件放在 /etc/rancher/k3s/k3s.yaml 安装大概 1~3 分钟,完成后会看到类似输出:\nCreated symlink /etc/systemd/system/multi-user.target.wants/k3s.service → /etc/systemd/system/k3s.service. 推荐轻量版(关掉不必要的内置组件,省 200~400MB 内存) curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC=\u0026amp;#34;--disable=traefik --disable=servicelb --disable=metrics-server\u0026amp;#34; sh - 这样默认不装 Traefik Ingress 和 servicelb,后续需要可以自己用 Helm 装更灵活的版本。\n2. 验证安装是否成功 # 查看服务状态 sudo systemctl status k3s # 查看节点(最简单验证方式) sudo k3s kubectl get nodes 正常输出示例:\nNAME STATUS ROLES AGE VERSION tmp Ready control-plane 2m v1.34.3+k3s1 如果看到 …","date": "2026-01-22 00:00:00",
"updated": "2026-01-22 00:00:00"
},
{
"objectID": "991621e3de9ee9b554ce2dd1651b44039a4926ec",
"permalink": "/post/golang/robfig-cron-guide/",
"title": "Go 定时任务完全指南:robfig/cron 库详解","content": "在后端开发中,定时任务是常见的需求:定期清理过期数据、发送定时通知、定时同步数据等。Go 的 robfig/cron 库提供了一个简洁而强大的解决方案。\n这篇文章通过实际例子,详细讲解如何使用 cron 库来实现各种定时任务。\n基础概念 robfig/cron 是一个 Go 的定时任务库,模仿 Unix cron 的工作方式。它允许你用 cron 表达式来定义任务的执行时间。\n安装 go get github.com/robfig/cron/v3 基本用法 package main import ( \u0026amp;#34;fmt\u0026amp;#34; \u0026amp;#34;github.com/robfig/cron/v3\u0026amp;#34; ) func main() { // 创建一个新的 cron 实例 c := cron.New() // 添加定时任务 c.AddFunc(\u0026amp;#34;0 */20 * * * *\u0026amp;#34;, func() { fmt.Println(\u0026amp;#34;每 20 分钟执行一次\u0026amp;#34;) }) // 启动 cron c.Start() // 保持程序运行 select {} } Cron 表达式详解 Cron 表达式由 6 个字段组成(包括秒),从左到右分别是:\n秒 分 时 日 月 周 * * * * * * 字段说明 字段 范围 说明 秒 0-59 秒数 分 0-59 分钟 时 0-23 小时(24小时制) 日 1-31 月份中的日期 月 1-12 月份 周 0-6 星期(0=周日,6=周六) 特殊符号 符号 说明 例子 * 任意值 * * * * * * 每秒执行一次 , 列表 0,30 * * * * * 每分钟的 0 秒和 30 秒 - 范围 0 9-17 * * * * 9 点到 17 点每分钟 / 步长 0 */20 * * * * 每 20 分钟 ? 不指定 用于日期或周 常见表达式示例 按时间间隔 // 每秒执行一次 c.AddFunc(\u0026amp;#34;* * * * * *\u0026amp;#34;, func() {}) // 每 5 秒执行一次 c.AddFunc(\u0026amp;#34;*/5 * * * * *\u0026amp;#34;, func() {}) // 每分钟执行一次 c.AddFunc(\u0026amp;#34;0 * * * * *\u0026amp;#34;, func() {}) // 每 20 分钟执行一次(你的例子 …","date": "2026-01-08 00:00:00",
"updated": "2026-01-08 00:00:00"
},
{
"objectID": "5831f3b7b1590b07a909c2ff2dbe284af2ea884c",
"permalink": "/post/golang/order-cache-ttl-design/",
"title": "订单支付链接缓存设计:为什么是 9 分 30 秒?","content": "在支付系统中,生成支付链接是一个常见的操作。如果用户频繁点击\u0026amp;quot;支付\u0026amp;quot;按钮,每次都调用支付平台 API 创建新订单,不仅浪费资源,还可能触发风控。所以我们通常会缓存支付链接。\n但缓存时间设置多少才合理?这篇文章通过分析一个真实的代码片段,来讲解这背后的设计思路。\n问题代码 ttl := (constants.OrderExpireTime - 30*time.Second).Seconds() 其中常量定义为:\nconst OrderExpireTime = time.Minute * 10 // 10 分钟 缓存时间计算 让我们先算一下实际的 TTL:\nOrderExpireTime = 10 分钟 ttl = 10 分钟 - 30 秒 = 9 分 30 秒 这个设计看似简单,但背后有深层的考量。\n为什么这个设计是合理的? 1. 与支付平台订单有效期同步 支付平台(微信、支付宝等)的订单有效期通常是 10 分钟。缓存时间略短于订单有效期,确保我们不会返回已过期的支付链接给用户。\n2. 30 秒缓冲时间的作用 这是最关键的设计点。让我们看一个时间轴:\n订单创建时间轴: 0 分钟 ────────────── 9 分 30 秒 ────── 10 分钟 ↑ ↑ ↑ 创建订单 缓存过期 订单过期 (缓存失效) 缓存提前 30 秒过期的好处:\n如果用户在第 9 分 40 秒请求,缓存已失效,系统会创建新订单 避免了边界情况:如果缓存和订单同时过期,可能返回无效的支付链接 给系统留出了反应时间 3. 业务场景匹配 这个缓存主要用于:\n防止重复下单:用户快速重复点击(几秒内)时,返回同一支付链接 提升用户体验:用户在 10 分钟内多次获取支付链接,无需重新创建 自动失效:超过 10 分钟后,订单自动失效,需要重新下单 时间轴示例 假设用户的操作流程:\n时间点 用户操作 系统行为 ───────────────────────────────────────────────── 0 秒 点击支付 创建订单,缓存 9 分 30 秒 5 秒 再次点击支付 返回缓存的支付链接 9 分 20 秒 再次点击支付 返回缓存的支付链接 9 分 40 秒 再次点击支付 缓存已失效,创建新订单 10 分钟 用户未支付 原订单过期,需要重新下单 潜在的优化方案 虽然当前设计已经很 …","date": "2026-01-08 00:00:00",
"updated": "2026-01-08 00:00:00"
},
{
"objectID": "8ac53ef0bd7704eec86af5b58562eedd0f91cb53",
"permalink": "/post/git/usegit/",
"title": "Git Cherry-Pick 使用指南","content": " 什么是 Cherry-Pick? Cherry-pick 是 Git 中一个强大的功能,它允许你从一个分支中选择特定的提交(commit),并将其应用到另一个分支上。这在需要将某些特定的修改从一个分支移植到另一个分支时非常有用。\n使用场景 最常见的使用场景是:\n在测试分支(test/dev)上开发并测试新功能 测试通过后,将特定的提交应用到发布分支(release/main) 避免合并整个分支带来的不必要的提交 基本用法 1. 查看提交历史 首先,在测试分支上查看你想要 cherry-pick 的提交:\ngit log --oneline 记录下你想要应用的提交的 hash 值(例如:abc1234)\n2. 切换到目标分支 切换到你想要应用提交的分支(如 release 分支):\ngit checkout release # 或者使用新的命令 git switch release 3. 执行 Cherry-Pick 将指定的提交应用到当前分支:\ngit cherry-pick abc1234 4. Cherry-Pick 多个提交 如果需要应用多个连续的提交:\n# 应用从 commit1 到 commit2 之间的所有提交(不包括 commit1) git cherry-pick commit1..commit2 # 应用从 commit1 到 commit2 之间的所有提交(包括 commit1) git cherry-pick commit1^..commit2 应用多个不连续的提交:\ngit cherry-pick abc1234 def5678 ghi9012 处理冲突 如果 cherry-pick 过程中出现冲突:\n# 1. 查看冲突文件 git status # 2. 手动解决冲突后,添加文件 git add \u0026amp;lt;冲突文件\u0026amp;gt; # 3. 继续 cherry-pick git cherry-pick --continue # 或者放弃本次 cherry-pick git cherry-pick --abort 常用选项 # 只应用更改,不自动提交(可以修改后再提交) git cherry-pick -n abc1234 # 或 git cherry-pick --no-commit abc1234 # 在提交信息中添加原始提交的引用 git …","date": "2025-12-15 00:00:00",
"updated": "2025-12-15 00:00:00"
},
{
"objectID": "3b40e3b06ed62cba04206f32b6a970fc5d890182",
"permalink": "/post/git/usegit_grpah/",
"title": "Git Cherry-Pick 图形化使用指南","content": " 前言 相比命令行操作,使用图形化 Git 工具进行 cherry-pick 更加直观和便捷。本文将通过实际截图演示如何使用图形化工具(如 GitKraken、SourceTree、VS Code Git Graph 等)完成 cherry-pick 操作。1\n使用场景 在实际开发中,我们经常遇到这样的情况:\n在测试分支开发了新功能 测试通过后,只想将这个功能的提交应用到生产分支 不想合并整个测试分支(可能包含其他未完成的功能) 这时候 cherry-pick 就是最佳选择!\n图形化操作步骤 步骤 1:在测试分支添加新功能 首先,在测试分支(test/dev)上进行开发:\n完成新功能的代码编写 提交更改(commit) 在测试环境中验证功能正常 关键点:\n确保提交信息清晰明了,方便后续识别 每个功能尽量独立提交,避免一个提交包含多个不相关的更改 充分测试后再进行 cherry-pick 步骤 2:选择要 Cherry-Pick 的提交 在图形化工具中:\n找到你想要应用的提交记录 右键点击该提交 选择 \u0026ldquo;Cherry Pick Commit\u0026rdquo; 选项 ⚠️ 重要提醒:\n不要直接点击 \u0026ldquo;Push to main\u0026rdquo;! 此时只是选择提交,还没有应用到目标分支 需要先切换到目标分支再执行 cherry-pick 步骤 3:切换到目标分支并应用提交 接下来的操作:\n切换到目标分支(如 release、main 或其他生产分支) 执行 cherry-pick 操作,将选中的提交应用到当前分支 图形化工具会自动创建一个新的提交 Cherry-Pick 的优势:\n✅ 只接入特定的更改,产生一个新的提交 ✅ 不会合并整个分支的所有提交 ✅ 保持目标分支的提交历史清晰 ✅ 避免引入未完成或未测试的代码 与 Merge 的区别:\nMerge(合并分支):会将源分支的所有提交都合并过来 Cherry-Pick:只选择特定的提交应用到目标分支 步骤 4:查看最终效果 Cherry-pick 完成后,你可以看到:\n目标分支上出现了一个新的提交 这个提交包含了你选择的更改 提交的 hash 值与原提交不同(这是正常的) 分支历史保持清晰,没有不必要的合并记录 验证步骤:\n检查提交历史,确认新提交已经出现 查看代码更改,确保内容正确 在目标分支上重新测试功能 确认无误后,推送到远程仓库 处理冲突 如果 cherry-pick 过程中出现冲突,图形化工具通常会:\n高亮显示冲突文件 提供冲突解决界面 可以选择保留哪一方的更改 或者手动编辑合并 解决冲突后继续 标记冲突已解决 点击 \u0026ldquo;Continue Cherry-pick\u0026rdquo; 或者放弃操作 点击 \u0026ldquo;Abort Cherry-pick\u0026rdquo; 恢复到操作前的状态 ","date": "2025-12-15 00:00:00",
"updated": "2025-12-15 00:00:00"
},
{
"objectID": "c00071561afba14435aebdb5dfd71a1e2496460c",
"permalink": "/post/git/usegit_squash/",
"title": "Git squash 使用指南","content": " Git Cherry-Pick 使用指南 ","date": "2025-12-15 00:00:00",
"updated": "2025-12-15 00:00:00"
},
{
"objectID": "e6c8b3af7793b89b88aa6826d239b295a52b9a62",
"permalink": "/post/git/uesegit_squash_graph/",
"title": "Git squash 图形化使用指南","content": " Git squash 使用指南 ","date": "2025-12-15 00:00:00",
"updated": "2025-12-15 00:00:00"
},
{
"objectID": "6c77a754c2d349bbc7c240fdba490a6be2701a22",
"permalink": "/post/sdktools/aws-ecr-push-tool/",
"title": "Go 实现 Docker 镜像自动推送到 AWS ECR","content": " 背景 在微服务架构中,我们经常需要将构建好的 Docker 镜像推送到容器镜像仓库。AWS ECR (Elastic Container Registry) 是 AWS 提供的托管式 Docker 镜像仓库服务。本文将介绍如何使用 Go 编写一个自动化工具,实现 Docker 镜像到 AWS ECR 的推送。\n工具概述 这个工具主要完成以下几个步骤:\n通过 AWS SDK 获取 ECR 认证凭证 为本地 Docker 镜像打标签 登录到 ECR 仓库 推送镜像到 ECR 核心实现 1. 配置结构定义 首先定义配置结构体,包含 ECR 和 EKS 相关配置:\ntype Config struct { // ECR配置 Region string ImageName string ImageTag string RepositoryName string // EKS配置 KubeconfigPath string Namespace string DeploymentName string ContainerName string } 2. 命令行参数解析 使用 Go 标准库的 flag 包解析命令行参数:\nfunc main() { // 命令行参数 region := flag.String(\u0026amp;#34;region\u0026amp;#34;, \u0026amp;#34;us-east-1\u0026amp;#34;, \u0026amp;#34;AWS region\u0026amp;#34;) imageName := flag.String(\u0026amp;#34;image\u0026amp;#34;, \u0026amp;#34;\u0026amp;#34;, \u0026amp;#34;Local image name\u0026amp;#34;) imageTag := flag.String(\u0026amp;#34;tag\u0026amp;#34;, fmt.Sprintf(\u0026amp;#34;%d\u0026amp;#34;, time.Now().Unix()), \u0026amp;#34;Image tag (default: unix timestamp)\u0026amp;#34;) repoName := flag.String(\u0026amp;#34;repo\u0026amp;#34;, \u0026amp;#34;your-org/your-service\u0026amp;#34;, \u0026amp;#34;ECR repository name\u0026amp;#34;) flag.Parse() config := \u0026amp;amp;Config{ Region: *region, ImageName: …","date": "2025-12-14 00:00:00",
"updated": "2025-12-14 00:00:00"
},
{
"objectID": "d4948b8f0811db4f292cbd7c89ee73ebee304f67",
"permalink": "/post/golang/go-build-tag/",
"title": "Go Build Tag:条件编译的正确姿势","content": "Build Tag 是 Go 的条件编译机制,让你控制哪些文件参与编译。简单说就是给文件打个\u0026quot;标签\u0026quot;,编译时可以选择性地包含或排除这些文件。\n基本语法 在文件开头添加 //go:build 指令:\n//go:build tagname package main 注意://go:build 必须在 package 声明之前,且与 package 之间要有空行。\n常见使用场景 1. 平台特定代码 Go 标准库大量使用这种方式处理跨平台代码:\n//go:build linux package main // 这个文件只在 Linux 下编译 func platformSpecificFunc() { // Linux 特定实现 } //go:build windows package main // 这个文件只在 Windows 下编译 func platformSpecificFunc() { // Windows 特定实现 } 2. 独立工具脚本 当你在同一个 cmd 目录下有多个入口文件时:\n//go:build gen package main // 只有显式指定 -tags gen 才编译 func main() { // 代码生成逻辑 } 3. 测试/调试代码 //go:build debug package main // 只在 debug 模式下编译 func debugLog(msg string) { fmt.Println(\u0026#34;[DEBUG]\u0026#34;, msg) } 运行方式 # 不带 tag,gen.go 不参与编译 go build ./app/account/cmd/ # 带 tag,gen.go 参与编译 go run -tags gen ./app/account/cmd/gen.go 组合条件 Build Tag 支持逻辑运算:\n//go:build linux \u0026amp;\u0026amp; amd64 // Linux 且 amd64 架构 //go:build linux || darwin // Linux 或 macOS //go:build !windows // 非 Windows 平台 //go:build (linux || darwin) \u0026amp;\u0026amp; !cgo // (Linux 或 macOS) 且不使用 cgo 更好的替代方案:子目录 对于多入口文件的场景,其实更常见的做法是直接放到不同子目录:\napp/account/cmd/ ├── gen/ │ └── main.go └── push/ └── main.go 这样更清晰,运行时也不需要记 tag 名字:\n# 直接运行对应目录 go run ./app/account/cmd/gen/ go run ./app/account/cmd/push/ 什么时候用 Build Tag? 场景 推荐方案 平台特定代码 Build Tag(linux、windows、darwin) 多个独立入口 子目录更清晰 调试/测试代码 Build Tag(debug、integration) 可选功能模块 Build Tag 总结 Build Tag 是 Go 条件编译的标准方式,适合处理平台差异和可选功能。但对于多入口文件的场景,子目录组织往往是更简洁的选择——不用记 tag 名,IDE 支持也更好。\n","date": "2025-12-13 00:00:00",
"updated": "2025-12-13 00:00:00"
},
{
"objectID": "df5b2343d1d8a1df97d073f6d2a2ec7cf024399f",
"permalink": "/post/usefeat/go-zero-jwt-limiter/",
"title": "鉴权登录限制设置","content": " 前言 在实际业务中,我们经常需要限制用户的同时登录设备数量。比如视频会员只允许同时在 2 台设备上登录,第 3 台设备登录时会把最早的那个踢下线。\n本文介绍如何在 Go-Zero 框架中实现这个功能。\n实现思路 使用 Redis 的有序集合(Sorted Set)存储用户的 JWT 令牌:\nmember: JWT 令牌 score: 登录时间戳 每次登录时,添加新令牌并移除超出限制的旧令牌。利用 Lua 脚本保证操作的原子性。\n核心代码 Lua 脚本 -- limiter.lua local key = KEYS[1] local jwt = ARGV[1] local score = tonumber(ARGV[2]) local size = tonumber(ARGV[3]) -- 添加或更新令牌 redis.call(\u0026amp;#39;ZADD\u0026amp;#39;, key, score, jwt) -- 移除超出限制的旧令牌 redis.call(\u0026amp;#39;ZREMRANGEBYRANK\u0026amp;#39;, key, 0, -size-1) -- 检查令牌是否存在(可能刚刚被移除) local exists = redis.call(\u0026amp;#39;ZSCORE\u0026amp;#39;, key, jwt) if exists then return 1 else return 0 end 脚本逻辑:\nZADD 将新令牌加入集合,score 为时间戳 ZREMRANGEBYRANK 按 score 排序后,删除排名靠前(最旧)的令牌,只保留最新的 size 个 检查当前令牌是否还在集合中,返回 1 表示有效,0 表示被踢出 Go 封装 package jwtLimiter import ( \u0026amp;#34;context\u0026amp;#34; _ \u0026amp;#34;embed\u0026amp;#34; \u0026amp;#34;fmt\u0026amp;#34; \u0026amp;#34;creaibo/common/utils\u0026amp;#34; \u0026amp;#34;github.com/zeromicro/go-zero/core/stores/redis\u0026amp;#34; ) //go:embed limiter.lua var luaScript string type JwtLimiter struct { redis *redis.Redis size int64 // 允许同时登录数量 } 关于 …","date": "2025-12-10 00:00:00",
"updated": "2025-12-10 00:00:00"
},
{
"objectID": "0f95627a443862ea35164114ed5f950721e35dab",
"permalink": "/post/microservice/go-zero/go-zero%E5%B8%B8%E7%94%A8%E5%8A%9F%E8%83%BD/",
"title": "Go-Zero 鉴权","content": " 1.1 go-zero jwt 上一节我们提到了,在desc/api文件中我们定义了go-zero自带的jwt中间件,生成代码后我们可以看到定义了jwt的api服务的路由,这里我们以usercenter服务举例,我们可以看下 go-zero-looklook/app/usercenter/cmd/api/internal/handler/routes.go 代码如下\n// Code generated by goctl. DO NOT EDIT. package handler ........ server.AddRoutes( []rest.Route{ { Method: http.MethodPost, Path: \u0026#34;/user/detail\u0026#34;, Handler: user.DetailHandler(serverCtx), }, { Method: http.MethodPost, Path: \u0026#34;/user/wxMiniAuth\u0026#34;, Handler: user.WxMiniAuthHandler(serverCtx), }, }, rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), rest.WithPrefix(\u0026#34;/usercenter/v1\u0026#34;), ) } 我们可以看到在路由中给我们使用了rest.WithJwt , 就是针对一组路由使用了jwt鉴权,具体实现可以去看一下rest.WithJwt源码。\n","date": "2025-12-06 00:00:00",
"updated": "2025-12-06 00:00:00"
},
{
"objectID": "fd1cf7e47b29b436043d948841d7fa89ad5c7ce8",
"permalink": "/post/microservice/go-zero/go-zero_nginx/",
"title": "Go-Zero 微服务网关:Nginx 统一入口实践","content": "理解 go-zero 中 API 与网关的关系,以及如何使用 Nginx 作为统一流量入口。\nAPI 不是网关 很多人把 go-zero 的 API 服务理解成网关,这其实是个误区。\n如果把单个 API 当网关用,会导致:\n一个 API 对应多个 RPC 改任何业务都要重新构建整个 API 效率低,维护麻烦 正确做法是:API 只是聚合服务,每个业务有自己的 API + RPC:\n用户服务:usercenter-api + usercenter-rpc 订单服务:order-api + order-rpc 支付服务:payment-api + payment-rpc 这样改用户服务只需更新用户相关的代码,互不影响。\n真正的网关 那多个 API 服务怎么统一入口?这才需要真正的网关:\n┌─────────────────┐ │ Nginx │ ← 真正的网关 │ (8888端口) │ └────────┬────────┘ │ ┌────────────────────┼────────────────────┐ │ │ │ ▼ ▼ ▼ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ usercenter-api│ │ order-api │ │ payment-api │ │ :1004 │ │ :1001 │ │ :1002 │ └───────────────┘ └───────────────┘ └───────────────┘ 可选方案:Nginx、Kong、APISIX 等,本质都是统一流量入口。\nNginx 网关配置 server { listen 8081; access_log /var/log/nginx/looklook.com_access.log; error_log /var/log/nginx/looklook.com_error.log; # 订单服务 location /order/ { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://looklook:1001; } # 支付服务 location /payment/ { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://looklook:1002; } # 旅游服务 location /travel/ { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://looklook:1003; } # 用户中心 location /usercenter/ { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header REMOTE-HOST $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_pass http://looklook:1004; } } Docker 映射:外部 8888 → 容器内 8081\n请求流程示例 访问 http://127.0.0.1:8888/usercenter/v1/user/detail:\n客户端请求 :8888 ↓ Nginx 匹配 /usercenter/ ↓ 转发到 usercenter-api :1004 ↓ go-zero JWT 鉴权 ↓ 业务逻辑处理 JWT 鉴权 在 API 定义文件中配置需要鉴权的路由:\n// 需要登录的接口 @server( prefix: usercenter/v1 group: user jwt: JwtAuth ) service usercenter { @doc \u0026#34;get user info\u0026#34; @handler detail post /user/detail (UserInfoReq) returns (UserInfoResp) } 在 Logic 中获取用户 ID:\nfunc (l *DetailLogic) Detail(req types.UserInfoReq) (*types.UserInfoResp, error) { // JWT 鉴权通过后,从 ctx 中获取 userId userId := ctxdata.GetUidFromCtx(l.ctx) // ... } 总结 组件 职责 Nginx 统一入口、路由分发、日志收集 API 聚合 RPC、HTTP 协议转换、JWT 鉴权 RPC 内部业务逻辑、服务间通信 这套架构的好处:\n统一入口,便于日志收集和行为分析 各服务独立部署,互不影响 鉴权在 API 层统一处理,RPC 层专注业务 参考资料 go-zero-looklook - go-zero 微服务最佳实践项目 ","date": "2025-12-06 00:00:00",
"updated": "2025-12-06 00:00:00"
},
{
"objectID": "88fe1cb9f3562fd74cf132075ebda14e670aa9a1",
"permalink": "/post/study/modd%E7%83%AD%E5%8A%A0%E8%BD%BD/",
"title": "使用 modd 实现 Go 项目热加载","content": "modd 是一个轻量级的文件监听工具,可以在代码变更时自动重新编译并重启服务,提升开发效率。\n安装 modd go install github.com/cortesi/modd/cmd/modd@latest 配置文件 在项目根目录创建 modd.conf:\n# usercenter rpc 服务 app/usercenter/cmd/rpc/**/*.go { prep: go build -o data/server/usercenter-rpc -v app/usercenter/cmd/rpc/usercenter.go daemon +sigkill: ./data/server/usercenter-rpc -f app/usercenter/cmd/rpc/etc/usercenter.yaml } 配置解析 1. 文件监听路径 app/usercenter/cmd/rpc/**/*.go ** 表示递归匹配所有子目录,只要这些 .go 文件有任何改动,就触发本条规则。\n2. prep 指令(准备阶段) prep: go build -o data/server/usercenter-rpc -v app/usercenter/cmd/rpc/usercenter.go 把 usercenter.go 及其依赖编译成二进制文件 -o 指定输出路径 -v 显示详细编译过程,方便排错 3. daemon +sigkill(守护进程) daemon +sigkill: ./data/server/usercenter-rpc -f app/usercenter/cmd/rpc/etc/usercenter.yaml 编译成功后,以守护进程方式启动二进制 -f 指定配置文件路径 +sigkill:文件再次变更时,先用 SIGKILL 强制杀掉旧进程,再重新编译启动 启动热加载 modd 工作流程 代码修改 → modd 检测到变更 → 执行 prep 编译 → 杀掉旧进程 → 启动新进程 多服务配置示例 # usercenter rpc app/usercenter/cmd/rpc/**/*.go { prep: go build -o data/server/usercenter-rpc -v app/usercenter/cmd/rpc/usercenter.go daemon +sigkill: ./data/server/usercenter-rpc -f app/usercenter/cmd/rpc/etc/usercenter.yaml } # order rpc app/order/cmd/rpc/**/*.go { prep: go build -o data/server/order-rpc -v app/order/cmd/rpc/order.go daemon +sigkill: ./data/server/order-rpc -f app/order/cmd/rpc/etc/order.yaml } # api gateway app/gateway/cmd/api/**/*.go { prep: go build -o data/server/gateway-api -v app/gateway/cmd/api/gateway.go daemon +sigkill: ./data/server/gateway-api -f app/gateway/cmd/api/etc/gateway.yaml } 这样每个服务独立监听,改哪个重启哪个,互不影响。\n","date": "2025-12-06 00:00:00",
"updated": "2025-12-06 00:00:00"
},
{
"objectID": "529a9df05fba3e26cc0d5a9ec3dd3c629927134c",
"permalink": "/post/microservice/go-zero/go-zero%E6%9C%8D%E5%8A%A1%E8%AE%BE%E8%AE%A1/",
"title": "Go-Zero 微服务最佳实践架构","content": "基于 go-zero 的微服务项目目录结构设计,参考 looklook 项目的最佳实践。\n整体架构 app/ ├── usercenter/ # 用户中心服务 │ ├── cmd/ │ │ ├── api/ # HTTP API 接口 │ │ └── rpc/ # gRPC 服务 │ └── model/ # 数据模型 (user, userAuth) │ ├── travel/ # 民宿/旅游服务 │ ├── cmd/ │ │ ├── api/ # HTTP API 接口 │ │ └── rpc/ # gRPC 服务 │ └── model/ # 数据模型 (homestay, business, activity, comment) │ ├── order/ # 订单服务 │ ├── cmd/ │ │ ├── api/ # HTTP API 接口 │ │ ├── rpc/ # gRPC 服务 │ │ └── mq/ # 消息队列消费者 │ └── model/ # 数据模型 (homestayOrder) │ ├── payment/ # 支付服务 │ ├── cmd/ │ │ ├── api/ # HTTP API 接口 │ │ └── rpc/ # gRPC 服务 │ └── model/ # 数据模型 (thirdPayment) │ └── mqueue/ # 消息队列服务 └── cmd/ ├── job/ # 延迟任务处理 (如订单超时取消) └── scheduler/ # 定时任务调度 (如定时统计) 服务内部分层 目录 作用 说明 cmd/api/ HTTP API 接口 对外暴露的 RESTful 接口,供前端/客户端调用 cmd/rpc/ gRPC 服务 内部服务间通信,供其他微服务调用 cmd/mq/ 消息队列消费者 处理异步消息(仅 order 服务有) cmd/job/ 延迟任务处理 消费延迟队列中的任务(仅 mqueue 服务) cmd/scheduler/ 定时任务调度 执行周期性定时任务(仅 mqueue 服务) model/ 数据模型层 数据库表映射、CRUD 操作封装 单个服务详细结构 以 usercenter 为例:\nusercenter/ ├── cmd/ │ ├── api/ │ │ ├── desc/ # API 定 …","date": "2025-12-05 00:00:00",
"updated": "2025-12-05 00:00:00"
},
{
"objectID": "f823d360be438e5af93a7eb6219e83b06c906e24",
"permalink": "/post/study/study_01/",
"title": "微服务日志采集链路:从文件到 Kibana","content": "理解微服务架构中日志从产生到可搜索的完整流转过程。\n整体链路 微服务 → 本地日志文件 → Filebeat → Kafka → go-stash → Elasticsearch → Kibana 各环节职责 1. 微服务写日志 各个微服务将日志写入本地文件:\n/var/log/looklook/user.log /var/log/looklook/order.log /var/log/looklook/payment.log ... 为什么不直接发到 ES?\n解耦:服务只管写文件,不关心日志去哪 可靠:即使下游挂了,日志也不丢 2. Filebeat 采集 Filebeat 是轻量级的日志采集器,守着日志文件:\n# filebeat.yml filebeat.inputs: - type: log paths: - /var/log/looklook/*.log output.kafka: hosts: [\u0026#34;kafka:9092\u0026#34;] topic: \u0026#34;looklook-log\u0026#34; 工作方式:\n监听文件变化,有新行立即读取 记录读取位置(offset),重启不重复 批量发送,减少网络开销 3. Kafka 缓冲 Kafka 作为中间缓冲层:\nProducer(Filebeat) → Topic(looklook-log) → Consumer(go-stash) 为什么加这层?\n削峰:日志量突增时不会压垮 ES 解耦:采集和处理可以独立扩展 可靠:消息持久化,消费失败可重试 4. go-stash 处理 go-stash(或 Logstash)从 Kafka 拉取日志,做清洗转换:\n# 典型处理 Input: {\u0026#34;time\u0026#34;:\u0026#34;2025-12-06\u0026#34;,\u0026#34;level\u0026#34;:\u0026#34;info\u0026#34;,\u0026#34;msg\u0026#34;:\u0026#34;user login\u0026#34;,\u0026#34;user_id\u0026#34;:123} 处理: - 解析 JSON 字段 - 时间格式转换 - 过滤无用字段 - 添加索引名 Output: 写入 ES 的 looklook-log-2025.12.06 索引 5. Elasticsearch 存储 ES 负责存储和索引,支持全文检索:\n索引: looklook-log-2025.12.06 文档: { time, level, msg, user_id, service, ... } 6. Kibana 查询 最终在 Kibana 上:\n搜索:level:error AND service:order 可视化:错误趋势图、服务调用量 告警:错误数超阈值通知 一图总结 ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌──────────┐ ┌────┐ ┌────────┐ │ 微服务 │───▶│ 日志文件 │───▶│Filebeat │───▶│ Kafka │───▶│Stash│───▶│ ES │ └─────────┘ └─────────┘ └─────────┘ └──────────┘ └────┘ └────────┘ 写入 存储 采集 缓冲 处理 存储/索引 │ ▼ ┌────────┐ │ Kibana │ └────────┘ 查询 这套架构的核心思想:每个组件只做一件事,通过消息队列解耦,任何一环出问题都不会导致日志丢失。\n","date": "2025-12-05 00:00:00",
"updated": "2025-12-05 00:00:00"
},
{
"objectID": "f4d3c62d839106eb24caa105028dee1c82d82690",
"permalink": "/post/microservice/go-zero/zero_start/",
"title": "Go-Zero 快速入门指南","content": "Go-Zero 是一个集成了各种工程实践的 Web 和 RPC 框架,本文记录从零开始搭建 Go-Zero 开发环境的完整流程。\n1. 初始化项目 首先创建一个新的 Go 模块:\ngo mod init your-project-name 2. 安装 goctl 工具 goctl 是 go-zero 的代码生成工具,可以通过以下两种方式安装:\n# 方式一:直接安装(推荐) go install github.com/zeromicro/go-zero/tools/goctl@latest # 方式二:作为依赖引入 go get -u github.com/zeromicro/go-zero/tools/goctl 安装完成后验证:\n$ goctl --version goctl version 1.7.3 linux/amd64 3. 安装依赖环境 goctl 提供了一键安装所需依赖的命令:\n$ goctl env check --install --verbose --force 这个命令会检查并安装:\nprotoc:Protocol Buffers 编译器 protoc-gen-go:Go 语言的 protobuf 插件 protoc-gen-go-grpc:gRPC 的 Go 插件 4. 安装 Go-Zero 框架 go get -u github.com/zeromicro/go-zero@latest 5. 常用命令 API 服务生成 根据 .api 文件生成 HTTP 服务代码:\ngoctl api go -api user.api -dir ./user 根据 api 文件生成 Swagger 文档\ngoctl api swagger -api ./api/http/ping.api -dir ./swaggerdoc 参数说明:\n-api:指定 api 定义文件 -dir:指定输出目录 RPC 服务生成 根据 .proto 文件生成 gRPC 服务代码:\ngoctl rpc protoc user.proto --go_out=./types --go-grpc_out=./types --zrpc_out=. Model 生成 根据数据库表生成 CRUD 代码:\n# 从 MySQL 连接生成 goctl model mysql datasource -url=\u0026#34;user:password@tcp(127.0.0.1:3306)/dbname\u0026#34; -table=\u0026#34;user\u0026#34; -dir=\u0026#34;./model\u0026#34; # 从 DDL 文件生成 goctl model mysql ddl -src=\u0026#34;user.sql\u0026#34; -dir=\u0026#34;./model\u0026#34; 6. 项目结构示例 使用 goctl 生成的典型项目结构:\n. ├── etc │ └── user-api.yaml # 配置文件 ├── internal │ ├── config │ │ └── config.go # 配置结构 │ ├── handler # HTTP handlers │ ├── logic # 业务逻辑 │ ├── svc │ │ └── servicecontext.go │ └── types │ └── types.go # 请求/响应结构 ├── user.api # API 定义文件 └── user.go # 入口文件 参考资料 Go-Zero 官方文档 Go-Zero GitHub ","date": "2025-12-04 00:00:00",
"updated": "2025-12-04 00:00:00"
},
{
"objectID": "067bc5ef62cd3cb0468c229916204423259c3a09",
"permalink": "/post/golang/proto.pitfalls/",
"title": "Golang 后端踩坑:Protobuf 枚举命名与全局唯一性","content": " 背景 在 Go 项目中使用 Protobuf 定义枚举时,由于 protoc-gen-go 会把枚举值编译成 包级常量,所有枚举值默认放在 同一命名空间。一旦同名,即使分属不同枚举,也会触发 全局唯一性冲突,protoc 直接报错。\n踩坑示例 以下写法在同一份 .proto 文件里会直接编译失败:\nenum Status1 { OK = 0; NOT-OK = 1; // 带连字符 } enum Status2 { OK = 0; NOT-OK = 1; // 带连字符 } 报错信息\n\u0026#34;OK\u0026#34; is already defined in \u0026#34;your.proto\u0026#34;. \u0026#34;NOT_OK\u0026#34; is already defined in \u0026#34;your.proto\u0026#34;. 原因两点:\n枚举值在 Go 中生成 const OK = 0,包级可见,同名即冲突。 Protobuf 标识符仅支持字母、数字和下划线,连字符 - 会被自动映射成下划线 _,于是 NOT-OK → NOT_OK,再次撞名。 正确姿势 给每个枚举值加 统一前缀,保证全文件唯一:\nenum Status1 { STATUS1_OK = 0; STATUS1_NOT_OK = 1; } enum Status2 { STATUS2_OK = 0; STATUS2_NOT_OK = 1; } 编译后 Go 代码示例:\nconst ( Status1_OK Status1 = 0 Status1_NOT_OK Status1 = 1 Status2_OK Status2 = 0 Status2_NOT_OK Status2 = 1 ) 彼此独立,不会冲突。\n前后端协议解耦 如果前后端共用同一份 .proto,务必把对外接口的枚举单独抽到一个公共文件(如 common.proto),后端私有枚举放到内部文件,不暴露给前端。\n这样既能保证兼容性,也避免后续新增字段时误伤客户端。\n存储与通讯分离 新起项目建议 存储结构体与通讯协议彻底分离:\n通讯层(DTO)——用 .proto 定义,只关心序列化。 存储层(DAO)——用 gorm 结构体,按需加索引、标签,避免被协议绑架。 示例:\n// 通讯层 type OrderStatusResp struct { Status common_pb.OrderStatus `json:\u0026#34;status\u0026#34;` } // 存储层 type Order struct { ID uint Status int `gorm:\u0026#34;type:tinyint;index\u0026#34;` // 自定义映射 } 小结 枚举值必须 全局唯一,加前缀是最简单有效的办法。 不要用连字符 -,Protobuf 会自动转 _,容易踩坑。 前后端共用协议时,公共枚举单独文件维护,后端私有枚举对外隐藏。 存储与通讯分离,让 .proto 只干序列化的活,别让数据库字段被协议绑死。 ","date": "2025-12-04 00:00:00",
"updated": "2025-12-04 00:00:00"
},
{
"objectID": "74528a6fdda3033cf1475eca7368c360f9854773",
"permalink": "/post/devenv/gvm-install/",
"title": "GVM 安装与使用指南","content": "GVM (Go Version Manager) 是一个管理多个 Go 版本的工具,方便在不同项目间切换 Go 版本。\n1. 清理旧版本 Go(可选) 如果系统之前使用install安装过 Go,建议先清理干净,避免版本冲突:\nsudo rm -rf /usr/local/go sudo apt-get remove golang sudo apt-get remove golang-go sudo apt-get autoremove 如果是全新环境,可跳过此步骤。\n2. 安装 GVM 安装go版本之前呢 , 有些unbantu版本会需要安装前置的条件\nsudo apt-get install gcc make 执行以下命令安装 GVM:\nbash \u0026lt; \u0026lt;(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer) 安装完成后,重启终端,输入 gvm 验证是否安装成功。\n如果提示命令未找到,需要手动在 ~/.bashrc 底部添加:\n[[ -s \u0026#34;$HOME/.gvm/scripts/gvm\u0026#34; ]] \u0026amp;\u0026amp; source \u0026#34;$HOME/.gvm/scripts/gvm\u0026#34; 然后执行 source ~/.bashrc 使配置生效。\n3. 安装 Go 版本 使用 GVM 安装指定版本的 Go:\n# 方式一:使用预编译二进制(推荐,速度快) gvm install go1.18 -B # 方式二:从源码编译(指定镜像源) gvm install go1.18 --source=https://github.com/golang/go 4. 切换 Go 版本 # 临时切换(仅当前终端生效) gvm use go1.18 # 设为默认版本(每次打开终端自动使用) gvm use go1.18 --default 验证安装:\ngo version go env 常用命令速查 命令 说明 gvm listall 查看所有可安装的版本 gvm list 查看已安装的版本 gvm install go1.x 安装指定版本 gvm use go1.x 切换版本 gvm uninstall go1.x 卸载指定版本 常见问题 GVM 安装后 cd 命令失效 参考 GVM GitHub ","date": "2025-12-04 00:00:00",
"updated": "2025-12-04 00:00:00"
},
{
"objectID": "282ce7cb960016e50e1d9ce9e046088616732090",
"permalink": "/post/devenv/gvm-cd-bug/",
"title": "解决 GVM 安装后 cd 命令失效问题","content": "在 Ubuntu 上安装 GVM 后,可能会遇到 cd 命令失效的问题。本文记录问题原因及解决方案。\n问题现象 安装 GVM 后,终端中 cd 命令无法正常工作。\n问题分析 安装 GVM 后,.bashrc 末尾会添加:\n[[ -s \u0026#34;$HOME/.gvm/scripts/gvm\u0026#34; ]] \u0026amp;\u0026amp; source \u0026#34;$HOME/.gvm/scripts/gvm\u0026#34; 这个脚本会加载 gvm-default:\n# ~/.gvm/scripts/gvm export GVM_ROOT=$HOME/.gvm . $GVM_ROOT/scripts/gvm-default 问题出在 gvm-default 脚本的最后一行:\n. \u0026#34;$GVM_ROOT/scripts/env/cd\u0026#34; \u0026amp;\u0026amp; cd . GVM 重写了 cd 命令(用于自动切换 Go 版本),但这会导致系统默认的 cd 命令被覆盖失效。\n解决方案 在 ~/.gvm/scripts/gvm-default 末尾添加 unset cd,取消 GVM 对 cd 的覆盖:\necho \u0026#39;unset cd\u0026#39; \u0026gt;\u0026gt; ~/.gvm/scripts/gvm-default 添加后文件末尾应该是:\n. \u0026#34;$GVM_ROOT/scripts/env/cd\u0026#34; \u0026amp;\u0026amp; cd . unset cd 重新加载配置:\nsource ~/.bashrc 验证修复:\ncd /tmp \u0026amp;\u0026amp; pwd # 输出 /tmp 说明修复成功 总结 GVM 的 cd 覆盖功能本意是在切换目录时自动检测 .go-version 文件并切换 Go 版本,但实现上有 bug。通过 unset cd 可以恢复默认行为,同时不影响 GVM 的其他功能。\n","date": "2025-12-04 00:00:00",
"updated": "2025-12-04 00:00:00"
},
{
"objectID": "02fe9ddc1c8206ac7313718d31be2714fc4f79b5",
"permalink": "/post/microservice/kitex_hertz/hertz-kitex-quickstart/",
"title": "Hertz + Kitex 微服务项目搭建指南","content": "使用 Hertz 作为 HTTP 网关 + Kitex 作为 RPC 微服务的项目搭建流程。\n项目结构 . ├── go.mod ├── go.sum └── server ├── cmd │ ├── api # Hertz HTTP 网关 │ └── user # Kitex RPC 服务 ├── idl │ ├── base │ ├── http # Hertz 用的 thrift │ └── rpc # Kitex 用的 thrift └── shared ├── consts └── kitex_gen # 共享的 model 库 环境准备 # 安装 Hertz 脚手架 go install github.com/cloudwego/hertz/cmd/hz@latest # 安装 Kitex 脚手架 go install github.com/cloudwego/kitex/tool/cmd/kitex@latest # 高版本 Go 需要降级 thrift(解决脚手架生成问题) go get github.com/apache/thrift@v0.13.0 Step 1:生成共享 Model 库 在 shared 目录下生成 Kitex model:\ncd server/shared kitex -module haha ./../../idl/rpc/user.thrift 生成后会在 kitex_gen 目录下产生对应的 model 文件。\nStep 2:创建 Kitex RPC 服务 进入 server/cmd/user 目录,指定使用共享的 model 库:\ncd server/cmd/user kitex -service user \\ -module haha \\ -use haha/server/shared/kitex_gen \\ ./../../idl/rpc/user.thrift 生成结构:\nuser/ ├── build.sh ├── handler.go # 在这里编写业务逻辑 ├── kitex_info.yaml ├── main.go └── script/ └── bootstrap.sh 注意:-service 是增量覆盖模式,不会覆盖你修改过的代码。\nStep 3:创建 Hertz HTTP 网关 进入 server/cmd/api 目录:\ncd server/cmd/api hz new -idl ./../../idl/http/user.thrift -module haha/server/cmd/api 生成结构:\napi/ ├── biz/ │ ├── handler/ │ │ ├── ping.go │ │ └── user/ │ │ └── user_service.go # 业务逻辑 │ ├── model/ │ │ ├── base/ │ │ └── user/ │ └── router/ │ ├── register.go │ └── user/ │ ├── middleware.go │ └── user.go ├── main.go ├── router.go ├── router_gen.go └── ... 在 handler 和 router 中编写业务逻辑,然后在当前目录执行服务即可。\n","date": "2025-12-03 00:00:00",
"updated": "2025-12-03 00:00:00"
},
{
"objectID": "45a1b24c940c43fe839922eb2201392c1cd5caa4",
"permalink": "/post/devenv/wsl2-disk-compact/",
"title": "WSL2 磁盘空间释放:使用 DiskPart 压缩虚拟磁盘","content": "WSL2 删除文件后磁盘空间不释放?用 DiskPart 压缩虚拟磁盘解决。\n问题 WSL2 中删除文件或清理 Docker 镜像后,虚拟磁盘文件(ext4.vhdx)大小不会自动减少,导致宿主机磁盘空间被持续占用。\n解决步骤 1. 关闭 WSL2 wsl --shutdown 2. 打开 DiskPart 按 Win + R,输入 diskpart,以管理员身份运行。\n3. 找到虚拟磁盘文件 虚拟磁盘文件路径一般在:\nC:\\Users\\你的用户名\\AppData\\Local\\Packages\\ 在该目录下搜索 ext4.vhdx 文件,记下完整路径。\n4. 选择并压缩磁盘 在 DiskPart 中执行:\nselect vdisk file=\u0026#34;C:\\Users\\你的用户名\\AppData\\Local\\Packages\\...\\ext4.vhdx\u0026#34; compact vdisk 等待进度到 100% 完成,关闭窗口即可。\n","date": "2025-12-03 00:00:00",
"updated": "2025-12-03 00:00:00"
},
{
"objectID": "d06f1d318d274aa0fcc8c200429b22b3c9f80eb1",
"permalink": "/post/golang/rand-pitfalls/",
"title": "Golang Randder线性同余的坑","content": "记录 Golang math/rand 使用线性同余算法(LCG)带来的问题。\n问题背景 Go 的 math/rand 包默认使用线性同余生成器(Linear Congruential Generator),这会导致几个常见的坑。\n坑 1:默认种子固定,每次运行结果相同 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; ) func main() { fmt.Println(rand.Intn(100)) // 每次运行都输出 81 fmt.Println(rand.Intn(100)) // 每次运行都输出 87 } 原因:默认种子是 1,不设置种子每次程序启动生成的序列完全一样。\n解决方案:\n// Go 1.20 之前 rand.Seed(time.Now().UnixNano()) // Go 1.20+ 推荐使用 rand.New(rand.NewSource(time.Now().UnixNano())) 坑 2:并发不安全 // 错误示例:多个 goroutine 共享全局 rand for i := 0; i \u0026lt; 10; i++ { go func() { fmt.Println(rand.Intn(100)) // 可能 panic 或产生重复值 }() } 错误的解决方案 1:在 goroutine 内部创建 rand\nfor i := 0; i \u0026lt; 10; i++ { go func() { r := rand.New(rand.NewSource(time.Now().UnixNano())) fmt.Println(r.Intn(100)) }() } 这样写有问题!goroutine 启动太快,time.Now().UnixNano() 可能返回相同的值,导致多个 goroutine 使用相同的种子,生成相同的随机数。\n错误的解决方案 2:循环内创建但加偏移\nfor i := 0; i \u0026lt; 10; i++ { r := rand.New(rand.NewSource(time.Now().UnixNano() + int64(i))) go func(rng *rand.Rand) { fmt.Println(rng.Intn(100)) }(r) } 虽然加了 i 偏移,但循环执行太快时 time.Now().UnixNano() 本身可能没变化,还是会有重复种子的风险。\n正确的解决方案:在外部创建一个 rand,传入 goroutine 使用\n// 方案 1:外部创建单个 rand,通过锁保护(简单场景) var ( rng = rand.New(rand.NewSource(time.Now().UnixNano())) rngMu sync.Mutex ) for i := 0; i \u0026lt; 10; i++ { go func() { rngMu.Lock() n := rng.Intn(100) rngMu.Unlock() fmt.Println(n) }() } // 方案 2:为每个 goroutine 预先创建独立的 rand rngs := make([]*rand.Rand, 10) for i := 0; i \u0026lt; 10; i++ { rngs[i] = rand.New(rand.NewSource(time.Now().UnixNano() + int64(i)*1000)) } for i := 0; i \u0026lt; 10; i++ { go func(r *rand.Rand) { fmt.Println(r.Intn(100)) }(rngs[i]) } // 方案 3:Go 1.20+ 直接用全局 rand(已自动处理并发安全) for i := 0; i \u0026lt; 10; i++ { go func() { fmt.Println(rand.Intn(100)) }() } 坑 3:线性同余可预测,不能用于安全场景 LCG 算法是可预测的,知道几个连续输出就能推算后续值。\n错误用法:\n// 千万别这样生成 token! token := fmt.Sprintf(\u0026#34;%d\u0026#34;, rand.Int63()) 正确做法:安全场景使用 crypto/rand\nimport \u0026#34;crypto/rand\u0026#34; func generateToken() string { b := make([]byte, 32) crypto_rand.Read(b) return hex.EncodeToString(b) } 坑 4:Go 1.20 的行为变化 Go 1.20 开始,全局 rand 函数自动使用随机种子,不再需要手动 rand.Seed()。但如果你显式调用 rand.Seed(),行为会回退到旧模式。\n// Go 1.20+ 这样写反而有问题 rand.Seed(time.Now().UnixNano()) // 不要这样做了 // 直接用就行 rand.Intn(100) 总结 场景 推荐方案 普通随机数 (Go 1.20+) 直接用 rand.Intn() 普通随机数 (Go 1.20 前) rand.Seed(time.Now().UnixNano()) 并发场景 每个 goroutine 独立 rand.New() 安全场景 (token/密码) crypto/rand ","date": "2025-12-02 00:00:00",
"updated": "2025-12-02 00:00:00"
},
{
"objectID": "98768b67cc8c76f9c9b5f3e039900e4adf95fb35",
"permalink": "/post/docker/cnm%E5%AE%B9%E5%99%A8%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/",
"title": "","content": " 桥接网络 其实本质上是通过一个虚拟网卡虚拟一个独立的内网ip,然后通过这个内网ip和宿主机之间的一个端口的映射维护一个nat表\n在进行网络交互的时候,通过这个nat表进行内网和外网之间的一个交流\n","date": "0001-01-01 00:00:00",
"updated": "0001-01-01 00:00:00"
},
{
"objectID": "f5d7244f7c74ffa90780eaa827c78082f8c2477c",
"permalink": "/post/vscode/vscode1/",
"title": "","content": "","date": "0001-01-01 00:00:00",
"updated": "0001-01-01 00:00:00"
}]