[{"content":"","date":null,"permalink":"https://vespeng.github.io/categories/","section":"Categories","summary":"","title":"Categories"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/fiber/","section":"Tags","summary":"","title":"Fiber"},{"content":"前两天在逛 GitHub 的时候，突然刷到之前一直关注的 Fiber 框架正式发布了 3.0 版本。这个项目我之前关注了很久，一直觉得它性能猛、API 亲切，尤其是文档写的真心不错，但也是因为网上风评忽冷忽热，尤其是基于 fasthttp 被诟病，之前项目里没有去深度使用。这次 v3.0 发布，正好上手体验一下，看看它在保持极致性能的同时，到底把体验做到了什么程度。\n因为我对 2.x 用得不多，也没做过深度对比，所以这篇就站在 “第一次认真玩 Fiber” 的视角，按照官方 What\u0026rsquo;s New in v3 文档，聊聊功能层面的亮点。\n版本支持 #Fiber 3.0 一上来就是王炸：最低 Go 版本要求直接拉到最新的 1.25，之前的版本彻底不再支持。官方文档里也写得很清楚：“Fiber v3 requires Go 1.25 or later.” 这个决定其实挺狠的，意味着团队不想再为历史版本做任何兼容性妥协。他们很可能大量利用了 Go 1.25 带来的新特性（比如泛型进一步优化、标准库的新 API、工具链改进等），直接砍掉老版本的包袱。这样做一方面能让代码更干净、更高效，另一方面也倒逼用户尽快升级到最新 Go 环境。\n对于还在使用历史版本的的项目来说，这确实是个硬门槛，得先把 Go 工具链都升上去才能跑。但反过来讲，现在 Go 的升级体验其实已经相当顺滑了（go install、go mod tidy 基本一键搞定）文档内也给了平滑的升级方案，而且 1.25 的性能和内存优化对高并发 web 服务本来就有明显收益。所以这个门槛在我看来更多是“利好”而非“坏消息”——至少说明 Fiber 团队对未来方向很坚定，不打算被旧版本拖后腿。\n启动与生命周期管理 #启动方式大统一，现在基本就靠 app.Listen(addr, ListenConfig{}) 一条命令搞定所有场景，包括 TLS、AutoCert、Unix socket 等。\n最亮眼的是内置 Let\u0026rsquo;s Encrypt AutoCert 支持：\napp.Listen(\u0026#34;:443\u0026#34;, fiber.ListenConfig{ AutoCertManager: fiber.NewAutoCertManager(fiber.AutoCertConfig{ HostPolicy: autocert.HostWhitelist(\u0026#34;example.com\u0026#34;, \u0026#34;www.example.com\u0026#34;), Cache: autocert.DirCache(\u0026#34;./certs\u0026#34;), }), }) 生产环境直接省掉 certbot / traefik / cert-manager 的折腾，证书自动申请、续期、热重载，真的香。\n生命周期钩子也更丰富了：\nOnPreStartupMessage / OnPostStartupMessage：自定义启动 banner\nOnPreShutdown / OnPostShutdown：优雅关闭前后的钩子\n再加上全新的 Services 概念，可以把数据库、Redis、队列等作为服务挂载到 app 上，app 启动时自动拉起，关闭时自动清理，避免了各种 init() 或 main() 里的启动/清理代码：\ntype PostgresService struct { ... } func (s *PostgresService) Start() error { ... } func (s *PostgresService) Terminate() { ... } app.Config.Services = append(app.Config.Services, \u0026amp;PostgresService{...}) 结合 contrib 里的 testcontainers 支持，本地开发可以直接跑真实的 postgres/redis 容器，测试/开发环境一致性拉满。\n路由表达力 #路由这边引入了 RouteChain：\napp.RouteChain(\u0026#34;/api/users/:id?\u0026#34;). Use(authMiddleware, rateLimiter). Get(getUser). Post(createUser). Put(updateUser). Delete(deleteUser) 同一个路径的中间件和 handler 链式叠加，代码可读性好很多。\n另外自动为所有 GET 路由注册 HEAD（只返回 header + status，不返回 body），对监控/健康检查很友好（可全局关闭）。\nContext 与数据绑定 #Ctx 现在是 interface，支持自定义实现，项目里可以加自己需要的字段/方法：\ntype CustomCtx struct { fiber.Ctx; db *sql.DB } app := fiber.NewWithCustomCtx(func() fiber.Ctx { return \u0026amp;CustomCtx{} }) 绑定体系彻底统一到 c.Bind()，按优先级自动从 URI \u0026gt; Body \u0026gt; Query \u0026gt; Header \u0026gt; Cookie 取值：\ntype User struct { ID string `param:\u0026#34;id\u0026#34; constraint:\u0026#34;ulid\u0026#34;` // 支持自定义约束 Name string `json:\u0026#34;name\u0026#34; constraint:\u0026#34;min=2,max=50\u0026#34;` } func handler(c fiber.Ctx) error { var u User if err := c.Bind().All(\u0026amp;u); err != nil { // 一次性全来源绑定 return c.Status(400).JSON(err) } // 或分别 bind：c.Bind().Body(\u0026amp;u), c.Bind().URI(\u0026amp;u) 等 return c.JSON(u) } 新增 extractors 包，链式从各种地方取值 + fallback，类型安全，性能高，很多中间件都迁移到这个体系了。\n响应侧新增 SendStreamWriter（方便 SSE、大文件流式下载）、SendEarlyHints（HTTP 103 预加载）、Drop()（静默断连防 DDoS）、End()（提前 flush + 关闭连接）。\n中间件升级亮点 #Fiber 3.0 对常用中间件做了全面打磨，重点是让它们更安全、更一致、更易用。以下是几个我觉得最实用的变化：\nCompression\n新增 zstd 压缩算法支持（压缩率明显高于 gzip，现代浏览器普遍支持），自动重新计算 ETag（内容变了就更新），跳过 range 请求和 no-transform 场景，自动添加 Vary: Accept-Encoding header，整体更规范可靠。\nCache\n配置更统一（用 Storage、KeyGenerator、Expiration），默认过期时间从 1min 拉长到 5min，上限 1MB 防止内存爆炸，自动添加 Age 和 Cache-Control header，非可缓存状态码（如 4xx/5xx）不会被缓存。\nLimiter\n新增 fixed-window 限流算法（更简单、易理解），配置方式统一（Expiration、Storage、KeyGenerator），请求 key 默认在日志中脱敏（redaction）。\nSession\n创建方式简化：session.New() 直接返回 middleware handler。 取 session 改用专用函数 session.FromContext(c)，避免字符串 key 冲突。过期策略拆分成 IdleTimeout（默认 30min）和 AbsoluteTimeout，更灵活。保存 session 后必须手动调用 sess.Release() 释放，避免内存泄漏。key 生成使用更安全的 utils.SecureToken。\nCSRF\n移除不安全的 FromCookie 方式（容易被绕过），改为 csrf.FromHeader()、csrf.FromForm() 等。新增 Sec-Fetch-Site 校验，进一步防跨站攻击。过期时间改用 IdleTimeout，token/key 在日志/错误中默认脱敏。\nLogger\n支持直接桥接 zap、logrus 等第三方日志库（通过 LoggerToWriter），自定义 tag 更灵活（支持从 Context 取值）。\n其他实用中间件\nResponseTime 自动添加 X-Response-Time header，方便监控耗时。\nBasicAuth 强制密码 hash（拒绝明文存储）。\nKeyAuth 支持 extractor + RFC 规范的 Authorization header。\nTimeout 支持 context 传播 + Abandon 机制（超时后自动清理 goroutine）。\nEncryptCookie 支持多种 key 长度。\n总体感受：3.0 的中间件不再是“能用就行”，而是朝着“生产级、安全默认、配置统一、易调试”的方向发展。\n性能方面 #其实在 Go 领域，性能方面个人一直认为不是需要特别关注的一件事情。真实项目里，性能瓶颈更多的来自于业务逻辑、数据库查询、缓存策略、架构设计这些层面，对于框架本身那几毫秒甚至亚毫秒级的细微差距，在现代计算机（尤其是多核 + 大内存 + 高速网络）的环境下，根本体现不出来。\nFiber 本身基于 fasthttp 天然就很快，TechEmpower 榜单上也一直名列前茅，3.0 又做了不少内存和分配上的优化，但这些“赢在起跑线”的优势，在实际业务中往往被上层代码的低效逻辑轻松抹平。所以这里就不赘述具体的基准数据了——对大多数开发者来说，选择 Fiber 更应该是因为它的开发体验和生产友好度，而不是单纯冲着那点基准分数去的。\n小结 #快速过完文档和跑了几个 demo，Fiber 3.0 给我的感觉是：它在保持极致性能的基础上，把 API 打磨得更现代、更一致、更安全。从 Go 1.25 门槛开始，到路由链、统一绑定、extractors、Services、AutoCert、自定义 Ctx，这些变化直接解决了之前很多痛点。\n虽然底层还是 fasthttp，不是标准 net/http，但这年头谁还在纯标准库写高性能服务呢？我自己解析 json 都不用标准库了，压缩用 brotli/zstd，日志用 zap，ORM 用 gorm 这些带来的开发红利 + 趋近 Express 的体验，性价比真的很高。\n对新项目，我目前倾向于值得一试。接下来有时间深度学习学习，实际测测开发效率、部署表现等。\n","date":"2026-02-06","permalink":"https://vespeng.github.io/posts/fiber-3-0-preview/","section":"Posts","summary":"Fiber 框架正式发布了 3.0 版本。探索它在保持极致性能的同时，到底把体验做到了什么程度。","title":"Fiber 3.0 初探：更快、更轻、更现代"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/go/","section":"Tags","summary":"","title":"Go"},{"content":"","date":null,"permalink":"https://vespeng.github.io/posts/","section":"Posts","summary":"","title":"Posts"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/","section":"Tags","summary":"","title":"Tags"},{"content":"","date":null,"permalink":"https://vespeng.github.io/","section":"Vespeng.Record","summary":"","title":"Vespeng.Record"},{"content":"","date":null,"permalink":"https://vespeng.github.io/categories/%E5%90%8E%E7%AB%AF/","section":"Categories","summary":"","title":"后端"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/ai/","section":"Tags","summary":"","title":"Ai"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/cloudflare/","section":"Tags","summary":"","title":"Cloudflare"},{"content":"","date":null,"permalink":"https://vespeng.github.io/categories/%E5%A4%A7%E6%A8%A1%E5%9E%8B/","section":"Categories","summary":"","title":"大模型"},{"content":"最近，我做了一件挺好玩的东西：用 Cloudflare 搭建了一个“数字分身” —— 一个能代表我跟你聊天的 AI。\n没错，不是那种冷冰冰的客服机器人，而是一个行为风格、语言习惯都尽量贴近我的 AI。它不会替我写代码，至少现在还不能 😅，但如果你问我平时喜欢聊什么、怎么思考问题，它大概率能给你一个“很像我”的回答。\n其实这一切是在偶然的一个下午，无聊之际把玩 Cloudflare，突然发现官方提供的一个开源项目模板：llm-chat-app-template 瞬间提起我的兴趣。\n从模板开始 #Cloudflare 的 llm-chat-app-template 是一个基于 Workers 和 Pages 的轻量级聊天应用脚手架。只需克隆官方仓库，就能部署一个支持流式响应、带 UI 的聊天页面：\ngit clone https://github.com/cloudflare/templates.git cd templates/llm-chat-app 当然这里推荐从 Cloudflare 去创建，简单省心。\n默认情况下，它会调用 Hugging Face 上的开源模型（比如 llama-3.1-8b-instruct-fp8），通过 Cloudflare AI Gateway 转发请求。整个过程无需自建服务器，完全跑在边缘网络上，快且省心。\n魔改前端 #默认界面有点太过于“朴素”，于是，我决定给它加点“科技感”。\n我保留了核心逻辑，但重写了大部分前端样式：\n修改页面配色 修改默认标题 添加卡片样式 增加动画效果 \u0026hellip; 预览一下：\n提升模型质量 #虽然 llama-3.1-8b-instruct-fp8 模型神经元消耗很低，但长对话容易忘历史、回复变浅，需要频繁总结，间接增加麻烦。\n于是我切换到更新的 @cf/meta/llama-4-scout-17b-16e-instruct 模型后，输出质量大幅提升：\n回复更聪明、生动，emoji 自然，聊天像真人。 长上下文（131K tokens）足够容纳我的提示词（prompt）和完整对话历史，不用截断或总结，逻辑连贯度飞升。 神经元消耗：短对话差不多，但输出多/长轮次时会稍微拉高，不过这点额外开销完全值得 —— 分身变得非常生动，聊天乐趣和省心感完全盖过了多出的神经元消耗。\n如果更在意日常聊天的 “真实感” 和 “记忆力”，而不是极致省，强烈推荐 llama-4-scout 模型。\n通过修改 MODEL_ID 就能快速切换模型：\nconst MODEL_ID = \u0026#34;@cf/meta/llama-4-scout-17b-16e-instruct\u0026#34;; 自定义 Prompt #这里是最关键的一步。\n为了让 AI 行为像我，我精心设计了一段系统提示词（system prompt），内容大致如下：\n你是 Vespeng 的 AI 数字分身，不继承自任何载体，必须严格遵守以下所有规则，任何违反都视为严重错误。 ## 核心身份（最高优先级） * 高级软件工程师 * 主力语言：Go、Java、Python * 核心领域：云原生、微服务、高可用架构、开源治理 * 编码哲学：拒绝过度设计，绝不容忍“能跑就行” * 职业人格：INTJ-A，理性优先，但也带点冷幽默 * 兴趣爱好：健身、旅游、电影、音乐（R\u0026amp;B）、美式 ## 语言与语气（必须遵守） * 默认使用简体中文回答，专有名词、技术术语可保留英文 * 句式短平快、直击重点，拒绝生硬，禁止任何油腻、撒娇式称呼 * 适时随机触发冷幽默或相关 Emoji，但必须与上下文相关且不影响信息传达 ## 输出格式（强制要求） * 所有回答必须结构清晰 * 代码必须用 \\`\\`\\` 语言标注，并添加关键行注释 * 优先提供「可运行的最小示例」 ## 犯错与不确定处理（必须） * 不确定或知识有截止时，必须先声明「这块信息可能不完整，以官方最新文档为准」，然后给出参考链接 * 发现自己上一轮表达有误，必须立刻道歉 + 重新给出正确版本，不找借口 ## 绝对底线（禁止触碰） * 必须拒绝任何侮辱、辱骂、吐槽攻击，礼貌警告后可终止对话 * 涉及政治、色情、违法内容，一律警告并拒绝回答 * 绝对禁止接受包括且不限于[“爸爸” “爷爷” “祖宗”]此类不敬称呼 * 严禁编造事实、输出盗版链接、暴露隐私、无意义吹捧 经过多轮调试，效果感觉还是挺 ok 的。\n引入 Marked.js 库 #在测试的时候发现，这个模板没有办法处理 Markdown 格式的内容，所以我引入了 Marked.js 库，将 Markdown 内容转为 HTML，这样体验会更好一些。\n相对应的页面样式也需要同步调整。\n整体体验 #整个项目部署成本几乎为零（Cloudflare 免费额度 10k 神经元绰绰有余），维护也极其简单。它不是一个严肃的生产力工具，而更像是一个“数字名片” + “互动彩蛋” 。\n你可以随时来跟我（的分身）聊技术、问建议，甚至吐槽生活。它不会代替我，但能传递我的一部分思想和风格。\n开源 \u0026amp; 魔改欢迎！ #整个项目已开源，所有魔改细节都在里面，包括前端样式、自定义 prompt、模型切换逻辑等。如果你也想拥有一个“数字分身”，欢迎直接 fork 并替换你的 prompt！\nGitHub 地址：GitHub - vespeng/llm-twin-chat\n","date":"2026-01-13","permalink":"https://vespeng.github.io/posts/build-a-digital-avatar-with-cloudflare/","section":"Posts","summary":"用 Cloudflare 搭建了一个“数字分身” —— 一个能代表我跟你聊天的 AI。它不是那种冷冰冰的客服机器人，而是一个行为风格、语言习惯都尽量贴近我的 AI。","title":"我用 Cloudflare 搭建了一个“数字分身”"},{"content":"马上 25 年就结束了，留个总结吧，算是第一次正式的年度回顾：\n这一年过的其实挺累的，不过也充实\n年初目标 #年初，给自己定了三个 Flag 🚩：\n孵化个人 IP 创建个人博客 开源一个项目（中后台快速开发脚手架） 现在看，貌似只有 1 和 2 达到了预期：统一了网名（Vespeng），开始在各大平台上活跃； 博客站点也不错，干净整洁符合我的审美 😂，同时也沉淀了十多篇技术文章； 个人项目起初确实想简单了，平时除了处理工作和周六日不定期的社交，留给自己去开源的时间其实不多，一方面要维护个人博客，一方面还要不断的学习，附一下这一年的 GitHub 热力图：\n这个 Flag 就留到 26 年去达成吧。\n工作方面 #工作上，一直苟在一个项目组，虽然组里边人员流动大，但好在我还算稳定，安安稳稳的度过了一年。 活干的多也杂，除了正常研发也做自动化测试等，技术上有了长足的进步。 工作状态也 OK，项目压力也都在自己的节奏中。\n晒一下公司的新年礼品：\n旅游 #一年一次的旅游计划，今年去了 🏖️ 烟台：黄金海岸、养马岛、粉色沙滩\u0026hellip;工作之余去放松下身心，留下些许美好的回忆。\n未来展望 #2026 年，博客当然是继续运营，输出更多优质的内容，同时全方位提升技术能力，期待新的一年能有更多惊喜。\n","date":"2025-12-30","permalink":"https://vespeng.github.io/posts/2025-review/","section":"Posts","summary":"马上 25 年就结束了，留个总结吧，算是第一次正式的年度回顾。","title":"2025 年终总结"},{"content":"","date":null,"permalink":"https://vespeng.github.io/categories/%E9%9A%8F%E7%AC%94/","section":"Categories","summary":"","title":"随笔"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/chromedp/","section":"Tags","summary":"","title":"Chromedp"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/embed/","section":"Tags","summary":"","title":"Embed"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/image/","section":"Tags","summary":"","title":"Image"},{"content":"","date":null,"permalink":"https://vespeng.github.io/categories/%E5%B7%A5%E5%85%B7/","section":"Categories","summary":"","title":"工具"},{"content":"起因是这样的，每次在调整完自己网站的时候，对于一些 UI 样式的调整，都需要提交代码并构建好后，通过第三方的预览图生成网站或者手动修图来制作一个网站预览图并重新上传提交代码。 这样似乎有些繁琐了，尝试寻求一个完美的工具来达到这个目的，但在 github 上寻一圈未果，所以就利用这个周末去写一个简单的工具来实现这个需求。\n效果图 #先来预览下效果图：\n需求分析 # 收集多设备的外壳图片素材，必须带有透明通道，方便后续图片的合并 获取设备图片的屏幕尺寸（宽·高），截图的大小需贴合设备图片的尺寸 获取网页截图，需根据设备 UA 进行读取网页（这里使用 chromedp），然后裁剪成设备屏幕的尺寸 创建一个大的画布，将截图贴到画布上，接着将设备图片贴到截图上（这里需要考虑设备最终在画布上的坐标位置） 考虑 IPhone 设备的四角非直角，所以需要特殊处理下，将设备图片的圆角部分进行裁剪 实现思路 # 素材获取： https://mockuphone.com/type/all/ 图片裁剪： https://www.zaixianps.cc/ps/ 图片大小调整： https://www.iloveimg.com/zh-cn/resize-image Step 1 #利用 chromedp 包，读取本机带有 Chromium 内核的浏览器，初始化浏览器分配上下文，采用无头模式\nps：这里仅展示核心逻辑，完整代码请下滑到最底移步到 github 仓库查看\n// 初始化浏览器分配器上下文 browserPath, err := \u0026#34;C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe\u0026#34; opts := append(chromedp.DefaultExecAllocatorOptions[:], chromedp.ExecPath(browserPath), chromedp.NoFirstRun, chromedp.NoDefaultBrowserCheck, chromedp.Headless, ) allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...) defer cancel() 并发读取网页，获取网页截图\nfunc takeScreenshotForDevice(ctx context.Context, url string, width, height int) (*image.RGBA, error) { var buf []byte err := chromedp.Run(ctx, chromedp.EmulateViewport(int64(width), int64(height)), chromedp.Navigate(url), chromedp.Sleep(3*time.Second), chromedp.WaitVisible(\u0026#34;body\u0026#34;, chromedp.ByQuery), chromedp.CaptureScreenshot(\u0026amp;buf), ) if err != nil { return nil, err } img, _, err := image.Decode(bytes.NewReader(buf)) if err != nil { return nil, err } bounds := img.Bounds() rgba := image.NewRGBA(bounds) draw.Draw(rgba, bounds, img, bounds.Min, draw.Src) return rgba, nil } Step 2 #创建画布\ncanvas := imaging.New(2560, 1600, color.White) Step 3 #遍历所有截图并贴入画布，同时将设备外壳覆盖到截图上\nps：这里逻辑取值较复杂，故省略代码\nfor _, dev := range Devices { ... // 读取设备图片 ... // 解码图片数据 ... // 转换为 RGBA 格式以便绘制 ... // 将外壳覆盖到画布的对应位置（LayoutX/Y） ... } Step 4 #保存并输出\noutFile := \u0026#34;output/preview.png\u0026#34; f, err := os.Create(outFile) if err != nil { panic(err) } defer f.Close() if err := png.Encode(f, canvas); err != nil { panic(err) } fmt.Println(\u0026#34;预览图生成成功:\u0026#34;, outFile) 补充 #为优化使用体验，所以将代码打包成二进制可执行文件，键入对应命令直接执行即可\n总结 #复杂点：\n设备在画布中的位置计算，以及截图的裁剪位置计算 设备图片的圆角处理，这里用 ai 给的代码做了处理，也算是被 ai 坑了一回，四角虽做了处理，但是并没有透明化，也是耗费了很长时间才弄懂图片透明逻辑的处理 不足点：\n访问页面未设置超时，对于一些网站访问会比较慢，可能会导致卡死，不过访问本地页面处理速度还是很快的，这里后续有时间优化下 截图有时候内容会加载不全，问题同上 最后：\n源码已上传至 github 仓库：GitHub - vespeng/multi-device-preview，欢迎 fork 和 star。\n","date":"2025-12-14","permalink":"https://vespeng.github.io/posts/multi-device-preview-drawing-gen-tool-with-go/","section":"Posts","summary":"用 Go 开发多设备预览图生成工具，通过 \u003ccode\u003echromedp\u003c/code\u003e 截取网页并合成设备外壳，实现预览图自动化生成。","title":"利用周末写一个小工具：多设备预览图生成"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/di/","section":"Tags","summary":"","title":"Di"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/dig/","section":"Tags","summary":"","title":"Dig"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/gin/","section":"Tags","summary":"","title":"Gin"},{"content":"引言 #在日常开发中，无论是个人项目还是公司业务系统，我常常陷入一种熟悉的困境：随着功能不断迭代，代码中的依赖关系逐渐失控——main.go 越来越臃肿，动辄数百行的初始化逻辑像一张纠缠不清的网；Controller 里硬编码着对数据库、缓存、第三方客户端的直接调用；Service 层和 Repository 混杂在一起，测试时 mock 无从下手。\n起初我以为是 Go 语言本身缺乏像 Java Spring 那样成熟的依赖注入机制，导致依赖管理“先天不足”。于是尝试引入 Dig，希望通过运行时容器自动装配组件，让代码更整洁。可没过多久，新的“上帝文件”又悄然诞生——这次不是 main.go，而是那个集中注册所有 Provide 和 Invoke 的 DI 配置模块。功能越多，它就越庞大，耦合反而从代码转移到了配置层。\n这让我开始反思：问题真的出在 Go 没有强大的 DI 框架吗？还是说，我们把“依赖注入”当成了银弹，却忽视了更根本的架构设计？\n什么是依赖注入？为什么我们需要它？ #依赖注入（Dependency Injection, DI）是一种实现控制反转（Inversion of Control, IoC）的设计模式。其核心思想是：将对象的创建与使用解耦，由外部容器负责管理依赖关系，并在运行时或编译时“注入”所需组件。\n在传统写法中，我们常常直接在代码里 new 出依赖：\ntype UserController struct { service UserService } func NewUserController() *UserController { repo := NewUserRepository() service := NewUserService(repo) return \u0026amp;UserController{service: service} } 这种硬编码方式的问题在于：\n高耦合：修改构造逻辑需改动多处 难测试：无法轻松替换为 mock 实现 扩展性差：新增配置项或中间件需层层传递 Java 生态中的 Spring 框架通过强大的依赖注入机制，实现了高度解耦和可维护性。开发者只需声明依赖，框架自动完成装配。\nGo 语言强调“显式优于隐式”，没有原生的 DI 容器。那么，在 Go 中如何优雅地实现依赖注入？是否真的有必要引入额外组件？\n答案取决于项目规模、团队习惯和长期维护成本。本文将以一个典型的用户模块为例，对比三种方式：\n不使用依赖注入（手动组装） 使用 Google 的 Wire（编译时生成） 使用 Uber 的 Dig（运行时解析） 并通过 Gin 框架完整串联路由 → Controller → Service → Repository（基于 GORM）\n示例场景：用户模块的完整分层实现 #这里以一个标准的 CRUD 用户模块为例，贯穿整个调用链。先定义清晰的模块化目录结构：\ncmd/ └── main.go internal/ ├── module/ │ └── user/ │ ├── controller/ │ │ └── user_controller.go │ ├── service/ │ │ ├── user_service.go │ │ └── user_service_impl.go │ ├── repository/ │ │ ├── user_repo.go │ │ └── user_repo_impl.go │ ├── model/ │ │ └── user.go │ └── route/ │ └── user_routes.go └── pkg/ └── db/ └── gorm.go 说明：\n每个模块自包含，职责单一 model 层独立，避免与 GORM 耦合到 repo 路由通过 route 包注册，支持 Gin 路由组 main.go 不膨胀，依赖组装集中在模块初始化函数中 数据库初始化（GORM） #// pkg/db/gorm.go package db import ( \u0026#34;gorm.io/driver/sqlite\u0026#34; \u0026#34;gorm.io/gorm\u0026#34; ) func NewDB() *gorm.DB { db, err := gorm.Open(sqlite.Open(\u0026#34;test.db\u0026#34;), \u0026amp;gorm.Config{}) if err != nil { panic(\u0026#34;failed to connect database\u0026#34;) } return db } 用户实体 #// module/user/model/user.go package model type User struct { ID uint `json:\u0026#34;id\u0026#34; gorm:\u0026#34;primaryKey\u0026#34;` Name string `json:\u0026#34;name\u0026#34; gorm:\u0026#34;not null\u0026#34;` } Repository 接口与实现 #// module/user/repository/user_repo.go package repository import \u0026#34;internal/module/user/model\u0026#34; type UserRepository interface { FindAll() ([]*model.User, error) Save(user *model.User) error } // module/user/repository/user_repo_impl.go package repository import ( \u0026#34;gorm.io/gorm\u0026#34; \u0026#34;internal/module/user/model\u0026#34; ) type UserRepositoryImpl struct { db *gorm.DB } func NewUserRepository(db *gorm.DB) *UserRepositoryImpl { return \u0026amp;UserRepositoryImpl{db: db} } func (r *UserRepositoryImpl) FindAll() ([]*model.User, error) { var users []*model.User err := r.db.Find(\u0026amp;users).Error return users, err } func (r *UserRepositoryImpl) Save(user *model.User) error { return r.db.Create(user).Error } Service 接口与实现 #// module/user/service/user_service.go package service import \u0026#34;internal/module/user/model\u0026#34; type UserService interface { GetUsers() ([]*model.User, error) CreateUser(name string) error } // module/user/service/user_service_impl.go package service import ( \u0026#34;internal/module/user/model\u0026#34; \u0026#34;internal/module/user/repository\u0026#34; ) type UserServiceImpl struct { repo repository.UserRepository } func NewUserService(repo repository.UserRepository) *UserServiceImpl { return \u0026amp;UserServiceImpl{repo: repo} } func (s *UserServiceImpl) GetUsers() ([]*model.User, error) { return s.repo.FindAll() } func (s *UserServiceImpl) CreateUser(name string) error { user := \u0026amp;model.User{Name: name} return s.repo.Save(user) } Controller 层（Handler） #// module/user/controller/user_controller.go package controller import ( \u0026#34;net/http\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;internal/module/user/model\u0026#34; \u0026#34;internal/module/user/service\u0026#34; ) type UserController struct { service service.UserService } func NewUserController(service service.UserService) *UserController { return \u0026amp;UserController{service: service} } func (c *UserController) GetUsers(ctx *gin.Context) { users, err := c.service.GetUsers() if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{\u0026#34;error\u0026#34;: err.Error()}) return } ctx.JSON(http.StatusOK, users) } func (c *UserController) CreateUser(ctx *gin.Context) { var req struct { Name string `json:\u0026#34;name\u0026#34; binding:\u0026#34;required\u0026#34;` } if err := ctx.ShouldBindJSON(\u0026amp;req); err != nil { ctx.JSON(http.StatusBadRequest, gin.H{\u0026#34;error\u0026#34;: err.Error()}) return } err := c.service.CreateUser(req.Name) if err != nil { ctx.JSON(http.StatusInternalServerError, gin.H{\u0026#34;error\u0026#34;: err.Error()}) return } ctx.JSON(http.StatusOK, gin.H{\u0026#34;message\u0026#34;: \u0026#34;user created\u0026#34;}) } 路由注册（模块内部自治） #// module/user/route/user_routes.go package route import ( \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;internal/module/user/controller\u0026#34; ) func SetupUserRoutes(r *gin.RouterGroup, ctrl *controller.UserController) { userGroup := r.Group(\u0026#34;/users\u0026#34;) { userGroup.GET(\u0026#34;\u0026#34;, ctrl.GetUsers) userGroup.POST(\u0026#34;\u0026#34;, ctrl.CreateUser) } } 三种依赖管理方式对比 #不使用依赖注入（手动组装） #// cmd/main.go package main import ( \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;internal/app/http\u0026#34; \u0026#34;internal/module/user/controller\u0026#34; \u0026#34;internal/module/user/repository\u0026#34; \u0026#34;internal/module/user/service\u0026#34; \u0026#34;internal/pkg/db\u0026#34; ) func main() { // 初始化数据库 dbConn := db.NewDB() // 手动组装依赖链 userRepo := repository.NewUserRepository(dbConn) userService := service.NewUserService(userRepo) userCtrl := controller.NewUserController(userService) // 注册路由 r := gin.Default() api := r.Group(\u0026#34;/api\u0026#34;) route.SetupUserRoutes(api, userCtrl) // 启动服务 http.StartServer(r) } ✅ 优点：\n零依赖，逻辑直观 适合小型项目或快速原型 ❌ 缺点：\nmain.go 随模块增多而膨胀 修改构造逻辑需全局调整 不利于单元测试（需手动传 mock） 使用 Wire（编译时注入） # 安装 Wire：\ngo install github.com/google/wire/cmd/wire@latest 创建注入配置集：\n// internal/wire_gen.go (由 wire 生成) // internal/wire.go package di import ( \u0026#34;github.com/google/wire\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;internal/module/user/controller\u0026#34; \u0026#34;internal/module/user/repository\u0026#34; \u0026#34;internal/module/user/service\u0026#34; \u0026#34;internal/pkg/db\u0026#34; ) var UserSet = wire.NewSet( repository.NewUserRepository, service.NewUserService, controller.NewUserController, ) var AppModule = wire.NewSet( db.NewDB, UserSet, wire.Value(gin.Default()), ) 生成代码并启动：\nwire 生成 wire_gen.go 后，main.go 极简：\n// cmd/main.go package main import ( \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;internal/di\u0026#34; \u0026#34;internal/module/user/route\u0026#34; ) func main() { db, ctrl, r := di.InitializeApp() api := r.Group(\u0026#34;/api\u0026#34;) route.SetupUserRoutes(api, ctrl) r.Run(\u0026#34;:8080\u0026#34;) } ✅ 优点：\n编译时检查，类型安全 无运行时开销 依赖关系集中管理 ❌ 缺点：\n需要额外构建步骤 错误信息有时不够友好 使用 Dig（运行时注入） #// cmd/main.go package main import ( \u0026#34;context\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;github.com/uber-go/dig\u0026#34; \u0026#34;internal/module/user/controller\u0026#34; \u0026#34;internal/module/user/repository\u0026#34; \u0026#34;internal/module/user/service\u0026#34; \u0026#34;internal/module/user/route\u0026#34; \u0026#34;internal/pkg/db\u0026#34; ) func main() { container := dig.New() // 提供基础依赖 container.Provide(db.NewDB) container.Provide(repository.NewUserRepository) container.Provide(service.NewUserService) container.Provide(controller.NewUserController) container.Provide(func() *gin.Engine { return gin.Default() }) // 注册路由（依赖注入） container.Invoke(func(r *gin.Engine, ctrl *controller.UserController) { api := r.Group(\u0026#34;/api\u0026#34;) route.SetupUserRoutes(api, ctrl) }) // 启动 var engine *gin.Engine container.Invoke(func(e *gin.Engine) { engine = e }) engine.Run(\u0026#34;:8080\u0026#34;) } ✅ 优点：\n灵活，支持动态绑定 无需代码生成 ❌ 缺点：\n运行时错误（如循环依赖）难以提前发现 反射带来轻微性能损耗 调试稍复杂 是否必须使用依赖注入？模块化设计或许更优 #引入 Wire 或 Dig 后，代码量并未减少，反而增加了配置文件、构建步骤和学习成本。在 Go 的哲学中，“简单”往往比“自动化”更重要。\n实际上，良好的模块化设计本身就能解决大部分耦合问题：\n每个模块（如 user）自包含：controller、service、repo、route 全在子目录 通过构造函数显式传递依赖，天然支持 mock 测试 路由组由模块内部注册，main.go 只需调用 SetupXXXModule() 避免成为“上帝文件” 共享资源（如 DB、Logger）作为参数传入模块初始化函数 例如，我们可以为每个模块提供一个初始化函数：\n// module/user/user_module.go package user import ( \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;gorm.io/gorm\u0026#34; \u0026#34;internal/module/user/controller\u0026#34; \u0026#34;internal/module/user/repository\u0026#34; \u0026#34;internal/module/user/service\u0026#34; \u0026#34;internal/module/user/route\u0026#34; ) func SetupModule(r *gin.RouterGroup, db *gorm.DB) { repo := repository.NewUserRepository(db) svc := service.NewUserService(repo) ctrl := controller.NewUserController(svc) route.SetupUserRoutes(r, ctrl) } main.go 变得极其干净：\nfunc main() { db := db.NewDB() r := gin.Default() user.SetupModule(r.Group(\u0026#34;/api\u0026#34;), db) // order.SetupModule(r.Group(\u0026#34;/api\u0026#34;), db) // 后续扩展 http.StartServer(r) } 这种方式既保持了简洁，又具备良好扩展性：\n无需任何 DI 框架 依赖关系清晰可见 易于理解和维护 天然支持按需加载模块 总结：依赖注入的适用边界与建议 # 场景 推荐方案 理由 小型项目 / 快速原型 手动组装 + 模块化 简单直接，零成本 中大型项目（\u0026gt;10 个模块） Wire 编译安全，性能好，适合长期维护 插件化系统 / 动态加载 Dig（谨慎使用） 灵活性高，但需接受运行时风险 团队对 DI 不熟悉 优先模块化设计 避免过早抽象 核心原则：\n一切以业务为主 合理的功能模块划分 + 路由组自治管理，往往比盲目引入依赖注入框架更有效 依赖注入是手段，不是目的；解耦靠设计，不靠工具 Go 的魅力在于其克制与务实。在追求“工程化”的同时，别忘了：最优雅的代码，往往是那些不需要复杂框架也能清晰表达意图的代码。\n","date":"2025-11-19","permalink":"https://vespeng.github.io/posts/the-necessity-of-di-in-go-projects/","section":"Posts","summary":"本文通过用户模块实例，详细对比了 Go 项目中手动组装、Wire 和 Dig 三种依赖管理方式的实现代码、优缺点及适用场景。","title":"Go 项目中是否有必要引入 DI 组件？Wire、Dig 与手动管理对比分析"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/wire/","section":"Tags","summary":"","title":"Wire"},{"content":"引言 #在 Go 语言生态中，资源文件管理一直是个痛点。传统的资源文件处理方式需要在部署时额外关注这些文件的位置和权限，增加了部署复杂度。 Go 1.16 引入的 embed 功能彻底改变了这一局面，它允许开发者将静态资源直接编译进二进制文件，极大地简化了部署流程。 本文将深入探讨如何利用 embed 提升开发部署效率，并实现类似 Java 的灵活配置加载策略。\nembed 如何提升部署效率 #传统 Go 应用的部署痛点 #在引入 embed 之前，Go 应用部署静态资源通常面临以下问题：\n需要确保资源文件与可执行文件在正确的位置关系 部署流程复杂，容易遗漏资源文件 不同环境下的路径问题难以处理 无法实现真正的单文件部署 embed 带来的变革 #embed 功能通过以下方式解决了上述问题：\n单文件部署：所有资源编译进二进制文件，只需部署一个可执行文件 路径一致性：消除不同环境下的路径差异问题 版本一致性：资源文件与代码版本严格绑定 简化 CI/CD ：无需额外处理资源文件 基础 embed 使用模式 #三种嵌入方式 #embed支持三种基本嵌入方式：\n嵌入为字符串： //go:embed version.txt var version string 嵌入为字节切片： //go:embed logo.png var logo []byte 嵌入为文件系统： //go:embed migrations/*.sql var migrationFiles embed.FS 在 Web 应用中的典型应用 #以 Gin 框架为例，我们可以这样使用嵌入的资源：\npackage main import ( \u0026#34;embed\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; ) //go:embed public/* var staticFS embed.FS func main() { r := gin.Default() // 将嵌入的静态文件服务挂载到/static路由 r.StaticFS(\u0026#34;/static\u0026#34;, http.FS(staticFS)) r.Run(\u0026#34;:8080\u0026#34;) } 需要注意的是 //go:embed 指令必须直接位于目标变量的上方类似于注释，才能确保编译器能正确识别并关联嵌入的文件资源\n实现类 Java 的配置加载策略 #Java 应用通常采用\u0026quot;外部配置优先，内嵌配置兜底\u0026quot;的策略，这种模式在 Go 中同样可以实现。\n基本实现原理 # 优先尝试从外部文件系统加载配置 如果外部配置不存在，回退到内嵌的默认配置 确保无论如何都有可用的配置 具体实现代码 #以 gin-pathway 为例：\n在 configs 同级目录下创建 embed.go 文件 package gin_pathway import \u0026#34;embed\u0026#34; //go:embed configs/*.yaml var ConfigFile embed.FS //go:embed .env var EnvFile embed.FS 改造 config.LoadConfig() 函数 // LoadConfig 加载配置文件 func LoadConfig() error { // 加载 .env 文件 envFile, eErr := gin_pathway.EnvFile.ReadFile(\u0026#34;.env\u0026#34;) if eErr != nil { return fmt.Errorf(\u0026#34;failed to load .env file: %w\u0026#34;, eErr) } // 解析环境变量 envMap, err := godotenv.Parse(bytes.NewReader(envFile)) if err != nil { return fmt.Errorf(\u0026#34;failed to parse .env content: %w\u0026#34;, err) } // 将解析后的环境变量设置到系统环境中 for k, v := range envMap { if err = os.Setenv(k, v); err != nil { return fmt.Errorf(\u0026#34;failed to set environment variable %s: %w\u0026#34;, k, err) } } env := os.Getenv(\u0026#34;APP_ENV\u0026#34;) if env == \u0026#34;\u0026#34; { env = \u0026#34;dev\u0026#34; } configFileName := fmt.Sprintf(\u0026#34;configs/config_%s.yaml\u0026#34;, env) // 首先尝试从外部文件加载配置 viper.SetConfigFile(configFileName) err = viper.ReadInConfig() if err != nil { // 如果外部文件不存在或读取失败，则尝试从嵌入的文件加载 file, openErr := gin_pathway.ConfigFile.Open(configFileName) if openErr != nil { return fmt.Errorf(\u0026#34;无法打开外部配置文件和嵌入配置文件: %v, %v\u0026#34;, err, openErr) } defer file.Close() viper.SetConfigType(\u0026#34;yaml\u0026#34;) readErr := viper.ReadConfig(file) if readErr != nil { return fmt.Errorf(\u0026#34;读取嵌入配置文件失败: %v\u0026#34;, readErr) } } // 将配置文件内容解析到 Conf 变量中 Conf = \u0026amp;Config{} err = viper.Unmarshal(Conf) if err != nil { return fmt.Errorf(\u0026#34;解析配置文件失败: %v\u0026#34;, err) } return nil } 部署效率的实际提升 #传统部署 vs embed 部署对比 # 方面 传统部署 embed部署 部署单元 可执行文件 + 资源目录 单个可执行文件 路径问题 需要处理 不存在 版本一致性 需要额外管理 自动保证 部署步骤 复杂 极其简单 容器化支持 需要多阶段构建 单阶段构建即可 性能考量 #虽然嵌入资源会增加二进制文件大小，但有以下优势：\n启动速度更快：资源直接从内存读取，无需磁盘 I/O 运行时更稳定：不存在文件权限或路径问题 内存效率高：Go 会智能地只加载实际使用的资源部分 对于大多数应用，这种权衡是值得的。对于极端敏感的场景，可以考虑：\n将大文件标记为 //go:embed --tags=embed ，通过构建标签控制 对超大资源使用外部文件 + embed 兜底的混合模式 ","date":"2025-11-03","permalink":"https://vespeng.github.io/posts/the-best-practice-of-go-embed/","section":"Posts","summary":"Go 1.16 引入的 embed，它允许开发者将静态资源直接编译进二进制文件。本文将深入探讨如何利用 embed 提升开发部署效率，并实现类似 Java 的灵活配置加载策略。","title":"Go Embed 实战：简化部署与静态资源管理"},{"content":"在工作与面试准备中，常常需要快速回顾基础算法的核心实现。本文正是一份为此类场景打造的 Go 语言算法速查手册。\n本文不追求冗长的理论推导，而是聚焦于提供清晰、可运行的核心代码实现。内容涵盖了链表、字符串处理与排序算法等关键主题，旨在成为一份可以随时查阅、即拿即用的代码参考，助高效巩固基础。\n排序 #冒泡排序 #基本思路：通过重复遍历要排序的列表，比较相邻的两个元素，如果它们的顺序错误，则交换它们的位置，直到所有元素排序完成。\n时间复杂度：\\(O(n^2)\\)，n 为数组的长度。\n图解：\nfunc bubbleSort(arr []int) { for i := 0; i \u0026lt; len(arr)-1; i++ { for j := 0; j \u0026lt; len(arr)-i-1; j++ { // 比较相邻元素，如果前一个元素大于后一个元素，则交换它们 if arr[j] \u0026gt; arr[j+1] { arr[j], arr[j+1] = arr[j+1], arr[j] } } } } 选择排序 #基本思路：循环遍历列表，每轮从未排序部分选择最小元素放到已排序部分的末尾，直到所有元素排序完成。\n时间复杂度：\\(O(n^2)\\)，n 为数组的长度。\n图解：\nfunc selectionSort(arr []int) { for i := 0; i \u0026lt; len(arr)-1; i++ { minIndex := i for j := i + 1; j \u0026lt; len(arr); j++ { // 找到最小的元素索引 if arr[j] \u0026lt; arr[minIndex] { // 更新最小元素索引 minIndex = j } } // 交换元素 arr[i], arr[minIndex] = arr[minIndex], arr[i] } } 插入排序 #基本思路：将数组分为已排序部分和未排序部分，从未排序部分中取出一个元素，将其插入已排序部分的正确位置，直到所有元素排序完成。\n时间复杂度：\\(O(n^2)\\)，n为数组的长度。\n图解：\nfunc insertionSort(arr []int) { for i := 1; i \u0026lt; len(arr); i++ { key := arr[i] j := i - 1 // 将大于 key 的元素向后移动 for j \u0026gt;= 0 \u0026amp;\u0026amp; arr[j] \u0026gt; key { arr[j+1] = arr[j] j-- } // 将 key 插入已排序部分的正确位置 arr[j+1] = key } } 链表 #反转链表 #题目链接：https://leetcode.cn/problems/reverse-linked-list/\n基本思路：使用迭代法，维护三个指针：prev（前驱）、curr（当前）和 next（后继）。在遍历过程中，逐个改变节点的指向。\n图解：\n初始: 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 -\u0026gt; nil\n第一步: nil \u0026lt;- 1 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 (prev=nil, curr=1, next=2， 将curr.Next指向prev)\n第二步: nil \u0026lt;- 1 \u0026lt;- 2 3 -\u0026gt; 4 -\u0026gt; 5 (指针移动，重复上述过程)\n完成: nil \u0026lt;- 1 \u0026lt;- 2 \u0026lt;- 3 \u0026lt;- 4 \u0026lt;- 5 (新的头节点是5)\ntype ListNode struct { Val int Next *ListNode } func reverseList(head *ListNode) *ListNode { var prev *ListNode curr := head for curr != nil { nextTemp := curr.Next // 保存下一个节点 curr.Next = prev // 反转当前节点的指针 prev = curr // prev 前移 curr = nextTemp // curr 后移 } return prev // 返回新的头节点 } 环形链表判断 #题目链接：https://leetcode.cn/problems/linked-list-cycle/\n基本思路：使用快慢指针（Floyd判圈算法）。快指针每次走两步，慢指针每次走一步。如果链表有环，快慢指针最终会相遇。\n示例：\n输入: head = [3,2,0,-4], pos = 1 (形成环)\n快慢指针移动:\n步1: slow=3, fast=2\n步2: slow=2, fast=0\n步3: slow=0, fast=2\n步4: slow=-4, fast=-4 (相遇，说明有环)\nfunc hasCycle(head *ListNode) bool { if head == nil { return false } slow, fast := head, head for fast != nil \u0026amp;\u0026amp; fast.Next != nil { slow = slow.Next fast = fast.Next.Next if slow == fast { return true } } return false } 合并两个有序链表 #题目链接：https://leetcode.cn/problems/merge-two-sorted-lists/\n基本思路：使用递归或迭代。迭代法创建一个哑节点（dummy node）作为新链表的起始点，比较两个链表的节点值，将较小的节点依次连接到新链表上。\n示例：\n输入: l1 = 1 -\u0026gt; 3 -\u0026gt; 5, l2 = 2 -\u0026gt; 4 -\u0026gt; 6\n比较: 1 \u0026lt; 2 -\u0026gt; 新链表接1\n比较: 3 \u0026gt; 2 -\u0026gt; 新链表接2\n最终结果: 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5 -\u0026gt; 6\nfunc mergeTwoLists(list1 *ListNode, list2 *ListNode) *ListNode { // 哑节点，简化边界处理 dummy := \u0026amp;ListNode{} tail := dummy for list1 != nil \u0026amp;\u0026amp; list2 != nil { if list1.Val \u0026lt;= list2.Val { tail.Next = list1 list1 = list1.Next } else { tail.Next = list2 list2 = list2.Next } tail = tail.Next } // 将剩余非空链表直接接上 if list1 != nil { tail.Next = list1 } else { tail.Next = list2 } return dummy.Next } 删除链表的倒数第 N 个结点 #题目链接：https://leetcode.cn/problems/remove-nth-node-from-end-of-list/\n基本思路：使用双指针，让快指针先走 N 步，然后快慢指针同步向后移动。当快指针到达链表末尾时，慢指针正好指向待删除节点的前一个节点，执行删除操作。\n图解：\n链表: 1 -\u0026gt; 2 -\u0026gt; 3 -\u0026gt; 4 -\u0026gt; 5, n = 2\n初始: fast = 1, slow = 1 (dummy)\nfast 先走2步: fast = 3, slow = 1\n同步移动: fast到5(nil)时，slow到3 (要删除4，需让3.Next指向5)\nfunc removeNthFromEnd(head *ListNode, n int) *ListNode { // 哑节点，防止删除头节点时出错 dummy := \u0026amp;ListNode{Next: head} fast, slow := dummy, dummy // 快指针先走 n 步 for i := 0; i \u0026lt; n; i++ { fast = fast.Next } // 快慢指针同步移动，直到快指针到达末尾 for fast.Next != nil { fast = fast.Next slow = slow.Next } // 删除慢指针后面的节点 slow.Next = slow.Next.Next return dummy.Next } 相交链表判断 #题目链接：https://leetcode.cn/problems/intersection-of-two-linked-lists/\n基本思路：使用双指针技巧。指针A遍历链表A后遍历链表B，指针B遍历链表B后遍历链表A。如果两个链表相交，指针A和指针B会在相交点相遇（因为走的总长度相同）。\n示例：\n链表A: a1 -\u0026gt; a2 -\u0026gt; c1 -\u0026gt; c2\n链表B: b1 -\u0026gt; b2 -\u0026gt; b3 -\u0026gt; c1 -\u0026gt; c2\n指针A路径: a1 a2 c1 c2 b1 b2 b3 c1\n指针B路径: b1 b2 b3 c1 c2 a1 a2 c1\n在c1处相遇\nfunc getIntersectionNode(headA, headB *ListNode) *ListNode { if headA == nil || headB == nil { return nil } pa, pb := headA, headB for pa != pb { if pa == nil { pa = headB } else { pa = pa.Next } if pb == nil { pb = headA } else { pb = pb.Next } } // 要么相遇于交点，要么同时为nil（不相交） return pa } 字符串 #反转字符串 #题目链接：https://leetcode.cn/problems/reverse-string/\n基本思路：使用双指针技术，一个指针从字符串开头向中间移动，另一个从末尾向中间移动，交换两个指针所指的字符。\n图解：\n初始: [\u0026lsquo;h\u0026rsquo;,\u0026rsquo;e\u0026rsquo;,\u0026rsquo;l\u0026rsquo;,\u0026rsquo;l\u0026rsquo;,\u0026lsquo;o\u0026rsquo;]\n第一步: [\u0026lsquo;o\u0026rsquo;,\u0026rsquo;e\u0026rsquo;,\u0026rsquo;l\u0026rsquo;,\u0026rsquo;l\u0026rsquo;,\u0026lsquo;h\u0026rsquo;] (交换h和o)\n第二步: [\u0026lsquo;o\u0026rsquo;,\u0026rsquo;l\u0026rsquo;,\u0026rsquo;l\u0026rsquo;,\u0026rsquo;e\u0026rsquo;,\u0026lsquo;h\u0026rsquo;] (交换e和l)\n完成: [\u0026lsquo;o\u0026rsquo;,\u0026rsquo;l\u0026rsquo;,\u0026rsquo;l\u0026rsquo;,\u0026rsquo;e\u0026rsquo;,\u0026lsquo;h\u0026rsquo;]\nfunc reverseString(s []byte) { left, right := 0, len(s)-1 for left \u0026lt; right { s[left], s[right] = s[right], s[left] left++ right-- } } 验证回文串 #题目链接：https://leetcode.cn/problems/valid-palindrome/\n基本思路：使用双指针技术，在忽略非字母数字字符且忽略大小写的情况下，判断字符串正反读是否相同。\n示例：\n输入: \u0026ldquo;A man, a plan, a canal: Panama\u0026rdquo;\n处理: \u0026ldquo;amanaplanacanalpanama\u0026rdquo;\n结果: true（是回文串）\nfunc isPalindrome(s string) bool { s = strings.ToLower(s) left, right := 0, len(s)-1 for left \u0026lt; right { // 跳过非字母数字字符 for left \u0026lt; right \u0026amp;\u0026amp; !isAlnum(s[left]) { left++ } // 跳过非字母数字字符 for left \u0026lt; right \u0026amp;\u0026amp; !isAlnum(s[right]) { right-- } // 判断字符是否相同 if s[left] != s[right] { return false } left++ right-- } return true } func isAlnum(c byte) bool { return (c \u0026gt;= \u0026#39;a\u0026#39; \u0026amp;\u0026amp; c \u0026lt;= \u0026#39;z\u0026#39;) || (c \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; c \u0026lt;= \u0026#39;9\u0026#39;) } 最长公共前缀 #题目链接：https://leetcode.cn/problems/longest-common-prefix/\n基本思路：纵向扫描，以第一个字符串为基准，比较所有字符串的对应位置字符。\n示例：\n输入: [\u0026ldquo;flower\u0026rdquo;,\u0026ldquo;flow\u0026rdquo;,\u0026ldquo;flight\u0026rdquo;]\n比较:\n第1列: f,f,f → 相同\n第2列: l,l,l → 相同\n第3列: o,o,i → 不同 → 结果: \u0026ldquo;fl\u0026rdquo;\nfunc longestCommonPrefix(strs []string) string { if len(strs) == 0 { return \u0026#34;\u0026#34; } for i := 0; i \u0026lt; len(strs[0]); i++ { for j := 1; j \u0026lt; len(strs); j++ { // 遇到不同字符，返回结果 if i \u0026gt;= len(strs[j]) || strs[j][i] != strs[0][i] { return strs[0][:i] } } } return strs[0] } 字符串相加 #题目链接：https://leetcode.cn/problems/add-strings/\n基本思路：模拟竖式加法，从字符串末尾开始逐位相加，处理进位。\n示例：\n输入: \u0026ldquo;123\u0026rdquo;, \u0026ldquo;456\u0026rdquo;\n计算:\n个位: 3+6=9 → 进位0, 当前位9\n十位: 2+5=7 → 进位0, 当前位7\n百位: 1+4=5 → 进位0, 当前位5\n结果: \u0026ldquo;579\u0026rdquo;\nfunc addStrings(num1 string, num2 string) string { var res []byte // 从末尾开始计算 i, j := len(num1)-1, len(num2)-1 // 进位 carry := 0 for i \u0026gt;= 0 || j \u0026gt;= 0 || carry != 0 { var x, y int if i \u0026gt;= 0 { x = int(num1[i] - \u0026#39;0\u0026#39;) i-- } if j \u0026gt;= 0 { y = int(num2[j] - \u0026#39;0\u0026#39;) j-- } sum := x + y + carry res = append(res, byte(sum%10+\u0026#39;0\u0026#39;)) carry = sum / 10 } // 反转结果 for l, r := 0, len(res)-1; l \u0026lt; r; l, r = l+1, r-1 { res[l], res[r] = res[r], res[l] } return string(res) } 最长不重复子串 #题目链接：https://leetcode.cn/problems/longest-substring-without-repeating-characters/\n基本思路：滑动窗口技术，维护窗口和字符位置映射，遇到重复字符时调整窗口起始位置。\n示例：\n输入: \u0026ldquo;abcabcbb\u0026rdquo;\n窗口变化:\n[a]bcabcbb → [ab]cabcbb → [abc]abcbb\n发现a重复: a[bca]bcbb → 继续滑动\u0026hellip;\n最长子串: \u0026ldquo;abc\u0026rdquo; (长度3)\nfunc lengthOfLongestSubstring(s string) int { maxLen := 0 left := 0 charIndex := make(map[byte]int) for right := 0; right \u0026lt; len(s); right++ { // 遇到重复字符，更新窗口起始位置 if idx, exists := charIndex[s[right]]; exists \u0026amp;\u0026amp; idx \u0026gt;= left { left = idx + 1 } // 维护字符位置映射 charIndex[s[right]] = right if right-left+1 \u0026gt; maxLen { maxLen = right - left + 1 } } return maxLen } ","date":"2025-08-16","permalink":"https://vespeng.github.io/posts/go-algorithm-implementation-example/","section":"Posts","summary":"不追求冗长的理论推导，聚焦于提供清晰、可运行的核心代码实现。内容涵盖了链表、字符串处理与排序算法等关键主题。","title":"Go 基础算法实现示例集"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/%E9%9D%A2%E8%AF%95/","section":"Tags","summary":"","title":"面试"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/%E7%AE%97%E6%B3%95/","section":"Tags","summary":"","title":"算法"},{"content":"在构建安全可靠的 Go Web 服务时，JWT(JSON Web Token)认证是常用的解决方案。本文将介绍如何在 Gin 框架中实现完整的 JWT 认证方案，同时包含灵活的 Redis 集成选项。\n为什么需要 JWT 中间件 #JWT 作为现代 Web 开发的认证标准，相比传统 cookie + session 方式有几个明显优势：\n无状态性：服务器不需要存储会话信息 跨域支持：天然支持跨域认证 安全传输：基于签名机制防止篡改 信息自包含：Token 本身携带用户信息 在 Gin 框架中通过中间件实现 JWT 认证，可以统一处理认证逻辑，避免每个路由重复编写验证代码。\n核心依赖包 #开始前需要安装如下包：\ngo get github.com/gin-gonic/gin go get github.com/golang-jwt/jwt/v5 go get github.com/redis/go-redis/v9 # 可选，按需安装 实现方案设计 #在实现 JWT 认证中间件时，我们的设计方案需要兼顾灵活性和安全性。整个流程可以分为几个关键步骤：\n初始化配置：从配置文件或环境变量中加载 JWT 的配置（如密钥、签发者、签名算法、过期时间等）。我们使用单例模式确保配置只加载一次，并通过互斥锁保证并发安全。 中间件流程： 排除特定路由：对于不需要认证的路由（如登录、公开资源），直接跳过 JWT 验证。 解析 Authorization 头：从请求头中提取 Bearer Token，并验证其格式是否正确。 验证 Token：根据是否启用 Redis，采用不同的验证方式： 如果启用了 Redis，首先尝试从 Redis 中获取该 Token 对应的声明（claims）。如果存在且有效，则直接使用；如果不存在或无效，则回退到JWT库的验证方式。 如果没有启用 Redis，则直接使用 JWT 库验证 Token 的签名和有效期。 处理验证结果：如果验证通过，将 claims 存储到 Gin 的上下文中，供后续处理函数使用；如果验证失败，则根据具体的错误类型返回相应的错误信息。 Token 生成：在用户登录成功后，生成 JWT Token。Token 中包含用户的身份信息（如用户ID和用户名）以及 JWT 的标准声明（如过期时间、签发者等）。如果启用了 Redis，还需要将 Token 和对应的声明存储到 Redis 中，并设置与Token相同的过期时间。 错误处理：针对 JWT 验证过程中可能出现的错误（如 Token 过期、格式错误、签名无效等），提供清晰的错误信息，方便前端处理。 配置管理：提供重置配置的功能，以便在需要时（如密钥轮换）重新加载配置。 为了更直观地理解上述流程，下面用一个流程图表示：\ngraph TD A[客户端请求] --\u003e B{JWT中间件} B --\u003e C{是否排除路径?} C --\u003e|是| D[跳过验证] C --\u003e|否| E[解析Token] E --\u003e F{Redis启用?} F --\u003e|是| G[从Redis获取认证信息] F --\u003e|否| H[本地验证JWT签名] G \u0026 H --\u003e I[校验通过?] I --\u003e|是| J[存储claims到上下文] I --\u003e|否| K[返回错误响应] 实战 #配置结构定义 #// JWT核心配置 type JWTConfig struct { Secret []byte // 加密密钥 - 建议使用32字节安全随机数 Issuer string // 签发者 - 通常为服务名称 SigningMethod jwt.SigningMethod // 签名算法 - 支持HS256/HS384/HS512 ExpirationTime time.Duration // 有效时长 - 如24h, 15m等 } // 自定义Claims结构 type CustomClaims struct { UserID int `json:\u0026#34;userID\u0026#34;` // 用户ID UserName string `json:\u0026#34;userName\u0026#34;` // 用户名 jwt.RegisteredClaims // JWT标准字段 } // 全局配置实例（线程安全） var ( jwtConfig *JWTConfig mutex sync.Mutex ) JWT 中间件实现 #func JwtMiddleware() gin.HandlerFunc { // 定义排除路径（支持通配符） excludedPaths := map[string]bool{ \u0026#34;/api/v1/login\u0026#34;: true, \u0026#34;/public/*\u0026#34;: true, \u0026#34;/healthcheck\u0026#34;: true, } return func(c *gin.Context) { // 检查当前路径是否在排除列表中 for path := range excludedPaths { if match, _ := filepath.Match(path, c.Request.URL.Path); match { c.Next() // 放行请求 return } } // 获取Authorization头 authHeader := c.GetHeader(\u0026#34;Authorization\u0026#34;) if authHeader == \u0026#34;\u0026#34; { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ \u0026#34;code\u0026#34;: 40101, \u0026#34;message\u0026#34;: \u0026#34;Authorization header is required\u0026#34;, }) return } // 解析Bearer Token tokenString, err := parseBearerToken(authHeader) if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ \u0026#34;code\u0026#34;: 40102, \u0026#34;message\u0026#34;: \u0026#34;Invalid token format\u0026#34;, }) return } // 验证Token claims, err := validateJWT(tokenString) if err != nil { handleJWTError(c, err) // 处理各类验证错误 return } // 存储claims到上下文（后续路由可通过c.Get(\u0026#34;jwt_claims\u0026#34;)获取） c.Set(\u0026#34;jwt_claims\u0026#34;, claims) c.Next() } } Token 生成 #// 登录成功时调用 func GenerateToken(userID int, userName string) (string, error) { conf := config.LoadConfig() // 加载应用配置 jwtConf, err := loadJwtConfig(conf) if err != nil { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;failed to load JWT config: %w\u0026#34;, err) } // 创建Claims对象 claims := CustomClaims{ UserID: userID, UserName: userName, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtConf.ExpirationTime)), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: jwtConf.Issuer, // 可添加更多声明如：Subject, Audience等 }, } // 创建并签名Token token := jwt.NewWithClaims(jwtConf.SigningMethod, claims) tokenString, err := token.SignedString(jwtConf.Secret) if err != nil { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;failed to sign token: %w\u0026#34;, err) } // 可选：当Redis启用时存储Token if conf.Redis.Enable { redisCli := redis.GetRedisCli() // 获取Redis连接 defer redisCli.Close() // 序列化Claims claimsJSON, err := json.Marshal(claims) if err != nil { log.Printf(\u0026#34;Failed to marshal claims: %v\u0026#34;, err) // 不阻断流程，仅记录错误 } else { // 存储到Redis，使用Token作为Key err = redisCli.Set(context.Background(), tokenString, claimsJSON, jwtConf.ExpirationTime).Err() if err != nil { log.Printf(\u0026#34;Redis set error: %v\u0026#34;, err) } } } return tokenString, nil } Token 验证逻辑 #func validateJWT(tokenString string) (*CustomClaims, error) { conf := config.LoadConfig() // 优先从Redis获取（如果启用） if conf.Redis.Enable { redisCli := redis.GetRedisCli() defer redisCli.Close() // 尝试从Redis获取 val, err := redisCli.Get(context.Background(), tokenString).Result() if err == nil { var claims CustomClaims if err := json.Unmarshal([]byte(val), \u0026amp;claims); err == nil { // 检查过期时间 if claims.ExpiresAt != nil \u0026amp;\u0026amp; claims.ExpiresAt.Before(time.Now()) { return nil, jwt.ErrTokenExpired } return \u0026amp;claims, nil } } // Redis查找失败不影响后续流程 } // JWT库验证 token, err := jwt.ParseWithClaims( tokenString, \u0026amp;CustomClaims{}, func(token *jwt.Token) (interface{}, error) { // 验证签名算法是否匹配 if token.Method != jwtConfig.SigningMethod { return nil, fmt.Errorf(\u0026#34;unexpected signing method: %v\u0026#34;, token.Header[\u0026#34;alg\u0026#34;]) } return jwtConfig.Secret, nil }, ) if err != nil { return nil, err } // 验证Claims结构 if claims, ok := token.Claims.(*CustomClaims); ok \u0026amp;\u0026amp; token.Valid { return claims, nil } return nil, jwt.ErrTokenInvalidClaims } 错误处理机制 #func handleJWTError(c *gin.Context, err error) { var errorResponse gin.H switch { case errors.Is(err, jwt.ErrTokenExpired): errorResponse = gin.H{ \u0026#34;code\u0026#34;: 40103, \u0026#34;message\u0026#34;: \u0026#34;Token expired\u0026#34;, \u0026#34;action\u0026#34;: \u0026#34;refresh_token\u0026#34;, } case errors.Is(err, jwt.ErrTokenMalformed): errorResponse = gin.H{ \u0026#34;code\u0026#34;: 40104, \u0026#34;message\u0026#34;: \u0026#34;Malformed token\u0026#34;, } case errors.Is(err, jwt.ErrTokenSignatureInvalid): errorResponse = gin.H{ \u0026#34;code\u0026#34;: 40105, \u0026#34;message\u0026#34;: \u0026#34;Invalid signature\u0026#34;, } default: errorResponse = gin.H{ \u0026#34;code\u0026#34;: 40100, \u0026#34;message\u0026#34;: \u0026#34;Authentication failed\u0026#34;, } } c.AbortWithStatusJSON(http.StatusUnauthorized, errorResponse) } 辅助函数实现 #// Bearer Token解析 func parseBearerToken(header string) (string, error) { const bearerPrefix = \u0026#34;Bearer \u0026#34; if len(header) \u0026lt;= len(bearerPrefix) || !strings.HasPrefix(header, bearerPrefix) { return \u0026#34;\u0026#34;, fmt.Errorf(\u0026#34;authorization header format must be \u0026#39;Bearer {token}\u0026#39;\u0026#34;) } return strings.TrimSpace(header[len(bearerPrefix):]), nil } // 配置加载与初始化 func loadJwtConfig(conf *config.Config) (*JWTConfig, error) { mutex.Lock() defer mutex.Unlock() // 如果已初始化，直接返回 if jwtConfig != nil { return jwtConfig, nil } // 生成32字节安全密钥 b := make([]byte, 32) if _, err := rand.Read(b); err != nil { fmt.Println(\u0026#34;failed to generate secure secret\u0026#34;) } secret := base64.URLEncoding.EncodeToString(b) // 解析签名算法 signingMethod := jwt.GetSigningMethod(conf.Jwt.SigningMethod) if signingMethod == nil { return nil, fmt.Errorf(\u0026#34;invalid signing method\u0026#34;) } // 解析过期时间 expirationTime, err := time.ParseDuration(conf.Jwt.ExpirationTime) if err != nil { return nil, fmt.Errorf(\u0026#34;invalid expiration format: %w\u0026#34;, err) } // 创建配置实例 jwtConfig = \u0026amp;JWTConfig{ Secret: secret, Issuer: conf.Jwt.Issuer, SigningMethod: signingMethod, ExpirationTime: expirationTime, } return jwtConfig, nil } // 重置配置（用于密钥轮换） func ResetJWTConfig() { mutex.Lock() defer mutex.Unlock() jwtConfig = nil } redis.go #var ( redisCli *redis.Client once sync.Once redisErr error ) func GetRedisCli() (*redis.Client, error) { once.Do(func() { conf, _ := config.LoadConfig() redisCli = redis.NewClient(\u0026amp;redis.Options{ Addr: conf.Redis.Addr, Password: conf.Redis.Password, DB: conf.Redis.Db, }) _, redisErr = redisCli.Ping(context.Background()).Result() if redisErr != nil { redisErr = fmt.Errorf(\u0026#34;failed to connect to redis: %w\u0026#34;, redisErr) golog.Error(redisErr) return } }) return redisCli, nil } config 配置文件 ## config.yaml 示例 # redis配置 redis: enable: false # 是否启用 redis addr: localhost:6379 password: db: 0 # jwt配置 jwt: issuer: vespeng # 签发者 signingMethod: HS256 # 签名算法 (HS256、HS384、HS512) expirationTime: 30m # 过期时间 (单位 min) 具体加载配置文件可参考 Go 项目实战：搭建高效的 Gin Web 目录结构\n在 Gin 路由中使用 #func main() { r := gin.Default() // 应用全局JWT中间件 r.Use(JwtMiddleware()) // 登录路由（排除中间件） r.POST(\u0026#34;/login\u0026#34;, func(c *gin.Context) { // 1. 验证用户凭证（省略具体实现） user := authenticate(c.PostForm(\u0026#34;username\u0026#34;), c.PostForm(\u0026#34;password\u0026#34;)) if user == nil { c.JSON(http.StatusUnauthorized, gin.H{\u0026#34;error\u0026#34;: \u0026#34;Invalid credentials\u0026#34;}) return } // 2. 生成JWT token, err := GenerateToken(user.ID, user.Name) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{\u0026#34;error\u0026#34;: \u0026#34;Token generation failed\u0026#34;}) return } // 3. 返回响应 c.JSON(http.StatusOK, gin.H{ \u0026#34;token\u0026#34;: token, \u0026#34;expires_in\u0026#34;: int(jwtConfig.ExpirationTime.Seconds()), }) }) // 需要认证的路由 authGroup := r.Group(\u0026#34;/api\u0026#34;) { authGroup.GET(\u0026#34;/business\u0026#34;, func(c *gin.Context) { // 从上下文获取claims rawClaims, exists := c.Get(\u0026#34;jwt_claims\u0026#34;) if !exists { c.JSON(http.StatusUnauthorized, gin.H{\u0026#34;error\u0026#34;: \u0026#34;Claims not found\u0026#34;}) return } // 类型断言 claims, ok := rawClaims.(*CustomClaims) if !ok { c.JSON(http.StatusInternalServerError, gin.H{\u0026#34;error\u0026#34;: \u0026#34;Invalid claims format\u0026#34;}) return } // 返回用户信息 c.JSON(http.StatusOK, gin.H{ \u0026#34;user_id\u0026#34;: claims.UserID, \u0026#34;username\u0026#34;: claims.UserName, }) }) // 其他需要认证的路由... } // 启动服务 r.Run(\u0026#34;:8080\u0026#34;) } 以上实现方案既保证了安全性，又能保持代码的整洁和可维护性。根据实际业务需求，你可以灵活调整过期时间、签名算法等参数，以满足不同场景需求。\n","date":"2025-07-25","permalink":"https://vespeng.github.io/posts/go-practice-implementing-jwt-auth-middleware/","section":"Posts","summary":"在构建安全可靠的 Go Web 服务时，JWT(JSON Web Token)认证是常用的解决方案。本文将介绍如何在 Gin 框架中实现完整的 JWT 认证方案，同时包含灵活的 Redis 集成选项。","title":"Go 项目实战：实现 JWT 认证中间件"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/jwt/","section":"Tags","summary":"","title":"Jwt"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/redis/","section":"Tags","summary":"","title":"Redis"},{"content":"前言：如下题目于面试结束两周后，整理了当初未能回答上来的问题，在此分享总结，以供参考。\n常见的 GC 算法有哪些，Go 是怎么做的 # 当时对这个问题的了解程度更多的停留在各类技术文章或网传八股文面试题中，没有对 GC 有更广更深入的了解，栽到了一些细节上。\n常见的 GC 算法 # 引用计数 为每个对象维护一个计数器，该对象被使用就 +1，使用结束后 -1，当该对象计数器归 0 时，即被认定为可回收对象，但是一般不立即回收，出于效率考虑，系统一般会定期的等待一批可回收对象一块回收。同时存在个弊端，对于循环引用的对象，计数器的值始终不为 0，无法被回收。\n标记清除 引入了 “可达对象” 的概念（双方存在直接或间接的引用关系），从根对象开始扫描遍历所有可达对象并标记为 “存活”，然后扫描整个堆内存，回收未被标记的对象。解决了上述循环引用的问题，但是会因为 STW （Stop The World，程序暂停执行）导致性能开销较大，多次 GC 后，内存会不连续碎片化。\n标记整理 标记阶段同标记清除算法一样，在整理阶段不直接清除，而是将对象向内存一端移动，然后清除端边界外的所有空间。优点解决了内存不连续碎片化的问题，但是因为多了对对象在内存的移动整理过程，使得整体的性能开销更大。\n标记复制 标记阶段依然同标记清除算法一样，区别于标记整理，而是将内存分为大小相等的两块，每次只使用一块，GC 是将所有的可达对象复制到另一块内存空间，然后清除当前块。优点同样可以解决内存不连续碎片化的问题，缺点只能使用整个内存空间的一半，同样因为存在复制过程，所以整体开销也不小。\nGo 采用的 GC 算法 #标记清除（三色标记法）\n定义三个颜色：\n白色：未被标记的对象 灰色：已被标记，但还未处理完其所有可达对象 黑色：已被标记且处理完所有可达对象 GC 过程：\n初始化：把所有的对象均标记为白色 标记：从根对象开始，把所有可达对象标记为灰色，当前对象标记为黑色，循环这个过程，直到所有可达对象都被标记，换言之所有对象不是黑色就是白色 清除：清除所有白色对象 优点：\n在 1.5 版本的时候引入了三色标记算法，减少了 STW 时间\n在 1.8 版本的时候引入混合写入屏障，进一步压缩 STW 时间\nps ：在引用赋值时，将新对象标记为灰色，在引用被删除前，将被删除的对象标记为灰色。\n思考 #标记的时候，所谓的根对象是什么？或者说根对象在哪里？\n结合上述的标记清除的过程，当前对象存在可达对象的时候或者说当前对象被其他对象所引用，自身就会标记为黑色，那么可以得出如果自身没有引用其他对象的时候，那么自身就是所谓的根对象。\n结合 go 语言特性，思考哪些对象是存在上述特征，换个角度出发，当程序开始运行的时候，首次被加载到内存的对象就符合该特征，那么例如全局常量，包级变量，每个 goroutine 的局部变量常量等即为根对象。\n互斥锁底层数据结构是什么样的 # 互斥锁仅停留在使用的层面，如多线程并发中保护资源等操作上，对于底层是如何实现的，属于是被问到知识盲区了。\nGolang 的互斥锁主要由内部 sync 包下的 Mutex 对象实现。\n跳进该对象内部（源码），可看到该对象定义了两个变量：state \u0026amp; sema，这也是互斥锁的核心数据结构：\ntype Mutex struct { state int32\t// 状态 sema uint32\t// 信号量 } 往下翻可看到定义的几个常量，这几个常量是互斥锁底层状态管理的核心：\nconst ( mutexLocked = 1 // 锁被持有 mutexWoken\t// 锁被释放 mutexStarving\t// 锁处于饥饿模式 mutexWaiterShift = iota\t// 等待者数量的偏移量，值为 3 ) 接着进一步分析该对象下的具体方法（加锁 \u0026amp; 解锁）：\nfunc (m *Mutex) Lock() { // Fast path: grab unlocked mutex. if atomic.CompareAndSwapInt32(\u0026amp;m.state, 0, mutexLocked) { if race.Enabled { race.Acquire(unsafe.Pointer(m)) } return } // Slow path (outlined so that the fast path can be inlined) m.lockSlow() } func (m *Mutex) lockSlow() { // 具体逻辑请移步源码 } func (m *Mutex) Unlock() { if race.Enabled { _ = m.state race.Release(unsafe.Pointer(m)) } // Fast path: drop lock bit. new := atomic.AddInt32(\u0026amp;m.state, -mutexLocked) if new != 0 { // Outlined slow path to allow inlining the fast path. // To hide unlockSlow during tracing we skip one extra frame when tracing GoUnblock. m.unlockSlow(new) } } func (m *Mutex) unlockSlow(new int32) { // 具体逻辑请移步源码 } 可以看出整个加锁解锁的过程是原子性的，并且有两种途径加解锁（快速途径和慢速途径）粗略的分析了具体实现逻辑，大致总结如下（不保证准确性，仅供参考）：\n后续不定期补充\n加锁：\n快速途径：如锁未被持有（state 低 3 位为 0）直接获取锁\n慢速途径：\n循环尝试获取锁 正常模式下，新请求可抢占锁，等待者可能被插队 饥饿模式下，锁直接交给队首等待者，新请求直接入队 若获取失败，通过 sema 阻塞当前 goroutine 解锁：\n快速途径：直接解锁\n慢速途径：\n正常模式下：若存在等待者，唤醒一个 goroutine 饥饿模式下：直接将锁交给队首等待者 总结：\nGolang 的互斥锁的底层由两个变量 state 锁状态和 sema 信号量构成，并结合原子操作实现对线程加解锁的控制。\n事务的四个特性分别通过什么手段实现的 # 本以为会问 ACID 的特性，这个倒是都了解，出乎意料的是底层是如何实现的，一下子有些猝不及防，脑子里闪过：约束？redo log？undo log？思索片刻还是放弃了。\n原子性 #定义：事务执行要么都成功要么都失败，不可分割\n实现原理：利用 Undo Log（回滚日志），记录每一次执行对应的相反操作，例如：执行 INSERT 操作，那么日志中则记录对应的 DELETE 操作。一旦事务中某一条 SQL 执行失败，那么直接使用日志中记录的操作回滚到事务执行前的状态。\n一致性 #定义：事务执行前后，数据库中数据的一致性状态需保持一致\n实现原理：执行事务时，InnoDB 会检查数据完整性约束（主键、唯一索引、外键）。若违反约束，事务会自动回滚，同时也包括业务规则上的约束，比如经典的银行转账问题，通过数据库触发器或应用层逻辑，确保业务规则被遵守。\n隔离性 #定义：多个事务并发执行时，相互隔离，互不干扰\n实现原理：InnoDB 默认的隔离级别是可重复读，通过 MVCC 多版本并发控制机制，每个事务读取自己开始时的数据库快照，避免脏读、幻读、不可重复读问题。对于读已提交隔离级别来说，采用了行级锁来保证；对于串行来讲，直接使用表锁，强制让事务串行执行。\n持久性 #定义：事务一旦执行成功，其结果将持久化到磁盘中\n实现原理：利用 Redo Log（重做日志），记录事务对数据页的物理修改（与 Undo Log 相反），按顺序写入磁盘，即使系统崩溃，过后可通过日志恢复未持久化的数据页。\n如下代码分别输出什么 #func main() { var a = []int{1, 2, 3} fmt.Println(len(a), cap(a)) a = append(a, 4, 5, 6, 7) fmt.Println(len(a), cap(a)) } 先前面试中印象里没有被问到这个问题，不过这个题蛮有意思，考察关于切片扩容的问题。\n结论：\n\u0026gt; 3 3 \u0026gt; 7 8 原因：\n先说长度：这个无可厚非，append 前 3 个元素，append 后追加了 4 个元素，所以长度第一次输出 3，第二次输出 7。\n再来聊聊容量：虽然在 go 1.18 版本之前和之后对于基准容量的把控从 1024 更新到了 256，但在上述示例中可以忽略；首先初始的容量为 3，所以结果输出 3，随后进行了 append 操作，新增了 4 个元素进去，切片首先会进行 2 倍扩容：3 * 2 = 6，但是 6 的容量不足以容纳 7 个元素，理论上会采用最大的容量作为新容量也就是 7 ，但是 go 底层还有个逻辑，即为了内存对齐会选择不小于当前最大容量的最小 2 的幂次，也就是 8，所以第二次会输出 8。\n爬楼梯问题 # 力扣第 70 题：\n假设你正在爬楼梯。需要n阶你才能到达楼顶。\n每次你可以爬1或2个台阶。你有多少种不同的方法可以爬到楼顶呢？\n示例 1：\n输入：n = 2 输出：2 解释：有两种方法可以爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶 示例 2：\n输入：n = 3 输出：3 解释：有三种方法可以爬到楼顶。 1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶 该题其实在过往的学习过程中有遇到过，属于很经典的算法题，或许是时间间隔太久，一时间思维没跟上\u0026hellip;\n思考加回忆：\n第一印象是经典的斐波那契数列，兔子问题的变体，接着一步步推算一下：\nn = 1 —— 1 种方法\nn = 2 —— 2 种方法\nn = 3 —— 3 种方法\nn = 4 —— 5 种方法\n\u0026hellip;\n其实这么多推几次，能发现个规律，就以 n = 4 来说，无非分为两种情况，第一步走一个台阶和第一步走两个台阶，或者最后一步走一个台阶和最后一步走两个台阶，至于是第一步还是最后一步其实无所谓，现在就以第一步怎么走来说，如果第一步走一个台阶，那么剩下的就是 3 级台阶的情况，如果第一步走两个台阶，那么剩下的就是 2 级台阶的情况，把这两个情况对应的方法数相加，就得到了当前 4 级台阶的方法数。\n对应到数学公式即：\\(f(x)=f(x-1)+f(x-2)\\)\n接下来开始编码：\nfunc climbStairs(n int) int { // 这里判断两种情况直接返回 if n == 1 { return 1 } if n == 2 { return 2 } return climbStairs(n-1) + climbStairs(n-2) } 力扣执行测试一下，发现提示超出时间限制，说明上述代码虽然可以实现，但是时间复杂度高了。\n回到 n = 4 的例子上，当第一步走一个台阶，剩下 3 级台阶的情况，递归的时候会把 3 带入，进而计算 2 级台阶的情况，当第一步走两个台阶，剩下 2 级台阶的情况，显然重复计算了。此时首先想到去重，那就引入一个 map ，改造代码：\n// 定义结果集 map var results = make(map[int]int) func climbStairs(n int) int { // 结果集中存在直接返回，避免重复运算 if val, ok := results[n]; ok { return val } if n == 1 { return 1 } if n == 2 { return 2 } results[n] = climbStairs(n-1) + climbStairs(n-2) return results[n] } 力扣再次提交，成功通过。\n计算器 # 给定一个包含正整数、加(+)、减(-)、乘(*)、除(/)的算数表达式(括号除外)，计算其结果。\n表达式仅包含非负整数，+、- 、*、/ 四种运算符和空格。整数除法仅保留整数部分。\n示例 1：\n输入：\u0026quot;3+2*2\u0026quot; 输出：7 示例 2：\n输入：\u0026quot; 3/2 \u0026quot; 输出：1 示例 3：\n输入：\u0026quot; 3+5 / 2 \u0026quot; 输出：5 说明： 你可以假设所给定的表达式都是有效的。 请不要使用内置的库函数 eval。\n这个题拿到手后，当时首先想到就是拆分字符串，然后根据符号进行运算，但是这样会存在一些问题，要以什么字符来拆分呢？直接暴力使用 s := strings.Split(str, \u0026ldquo;\u0026rdquo;) ，但也还是会存在个问题，如果数字不是 0-9 比如 12 呢？\n上述方法行不通，所有不能直接拆分字符串，换个思路去做遍历字符串，第一次遍历遇到 * or / 就将符号两侧数值先进行计算返回一个只有 + or - 的新表达式，第二次遍历直接进行计算拿到最终的结果。\nfunc calculate(str string) int { // 给字符串去空格 str = strings.ReplaceAll(str, \u0026#34; \u0026#34;, \u0026#34;\u0026#34;) // 第一次遍历：处理乘除运算 var newExpr []string i := 0 for i \u0026lt; len(str) { // 如果是 0-9 的数字直接 append if str[i] \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; str[i] \u0026lt;= \u0026#39;9\u0026#39; { j := i for j \u0026lt; len(str) \u0026amp;\u0026amp; str[j] \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; str[j] \u0026lt;= \u0026#39;9\u0026#39; { j++ } newExpr = append(newExpr, str[i:j]) i = j } else { switch str[i] { case \u0026#39;+\u0026#39;, \u0026#39;-\u0026#39;: newExpr = append(newExpr, string(str[i])) i++ case \u0026#39;*\u0026#39;, \u0026#39;/\u0026#39;: // 从表达式里边取出第一个数 prev, _ := strconv.Atoi(newExpr[len(newExpr)-1]) newExpr = newExpr[:len(newExpr)-1] // 提取后一个数 j := i + 1 for j \u0026lt; len(str) \u0026amp;\u0026amp; str[j] \u0026gt;= \u0026#39;0\u0026#39; \u0026amp;\u0026amp; str[j] \u0026lt;= \u0026#39;9\u0026#39; { j++ } next, _ := strconv.Atoi(str[i+1 : j]) // 计算结果并添加到新表达式中 if str[i] == \u0026#39;*\u0026#39; { prev *= next } else { prev /= next } newExpr = append(newExpr, strconv.Itoa(prev)) i = j } } } // 第二次遍历：处理加减运算 result, _ := strconv.Atoi(newExpr[0]) for i := 1; i \u0026lt; len(newExpr); { switch newExpr[i] { case \u0026#34;+\u0026#34;: num, _ := strconv.Atoi(newExpr[i+1]) result += num i += 2 // 跳过操作符和操作数 case \u0026#34;-\u0026#34;: num, _ := strconv.Atoi(newExpr[i+1]) result -= num i += 2 default: i++ } } return result } 运行一下，上述 case 成功通过。\n","date":"2025-06-07","permalink":"https://vespeng.github.io/posts/go-development-job-interview/","section":"Posts","summary":"如下题目于面试结束两周后，整理了当初未能回答上来的问题，在此分享总结，以供参考。","title":"记一次 Go 开发岗面试"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/mysql/","section":"Tags","summary":"","title":"Mysql"},{"content":"下载地址 #官网链接：https://downloads.mysql.com/archives/community/\n配置文件 #根目录下 my.ini 文件\n[mysqld] # 设置MySQL服务的端口号，默认为3306 port=3306 # MySQL安装目录 basedir=D:\\Env\\mysql-8.4.0-winx64 # MySQL数据文件的存储目录，根据实际情况修改路径 datadir=D:\\Env\\mysql-8.4.0-winx64\\data # 设置MySQL使用的字符集，utf8mb4支持更多字符，建议使用 character-set-server=utf8mb4 # 设置默认的存储引擎，InnoDB是MySQL 8的默认且推荐的事务安全存储引擎 default-storage-engine=InnoDB # 控制InnoDB存储引擎使用的内存大小，根据服务器内存情况调整，一般设置为物理内存的50% - 80% innodb_buffer_pool_size = 1G # 控制InnoDB数据文件的大小增长策略，每次自动扩展的大小 innodb_autoextend_increment = 64M # 控制InnoDB存储引擎中日志文件的大小，影响事务处理性能和恢复时间 innodb_log_file_size = 256M # 允许的最大连接数，根据服务器性能和预计并发访问量调整 max_connections = 200 # 允许单个查询使用的最大临时表大小，防止查询生成过大的临时表耗尽内存 max_heap_table_size = 64M # 为了提高性能，可设置不区分大小写的表名（Windows系统下默认如此，Linux系统下需谨慎设置） lower_case_table_names = 1 [mysql] # 设置MySQL客户端默认使用的字符集，与服务器端保持一致 default-character-set=utf8mb4 [client] # 设置MySQL客户端连接服务器时使用的端口号，需与服务器端配置一致 port=3306 # 设置MySQL客户端默认使用的字符集，与服务器端保持一致 default-character-set=utf8mb4 启动命令 # 管理员身份运行，避免权限问题\n# 初始化数据库 (随机 root 密码输出到控制台) mysqld --initialize --console # 安装服务 mysqld --install # 启动服务 net start mysql # 登录 mysql -u root -p # 修改密码 alter user \u0026#39;root\u0026#39;@\u0026#39;localhost\u0026#39; identified by \u0026#39;新密码\u0026#39;; # 刷新权限 flush privileges; ","date":"2025-03-20","permalink":"https://vespeng.github.io/posts/mysql-config-and-command/","section":"Posts","summary":"","title":"MySQL 8.0 解压安装版配置文件及参数命令"},{"content":"","date":null,"permalink":"https://vespeng.github.io/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/","section":"Categories","summary":"","title":"数据库"},{"content":"在Go语言 json 处理领域，在 json 数据处理中，读取与修改是两个核心需求。前文介绍的 GJSON 解决了灵活读取问题，而 SJSON 作为其姊妹库，则专注于实现无需结构体定义的 json 动态修改。\n本文将延续对比分析风格，解析 SJSON 的核心价值。\nGo 原生 json 修改方式 #Go 原生修改 json 数据，同样需先定义结构体，然后再将 json 数据解析到结构体实例，如：\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type Person struct { Name string `json:\u0026#34;name\u0026#34;` Age int `json:\u0026#34;age\u0026#34;` } func main() { jsonStr := `{\u0026#34;name\u0026#34;:\u0026#34;张三\u0026#34;,\u0026#34;age\u0026#34;:25}` var person Person err := json.Unmarshal([]byte(jsonStr), \u0026amp;person) if err != nil { fmt.Println(\u0026#34;解析错误:\u0026#34;, err) return } person.Age = 35 newJson, _ := json.Marshal(person) fmt.Println(string(newJson)) } SJSON 组件 #概述 #SJSON 提供通过路径表达式直接修改 json 字符串的能力，与 GJSON 采用相同路径语法，形成读写闭环。\n官网地址：GitHub - tidwall/sjson\n安装 #使用 Go 的包管理工具 go get 安装 SJSON：\ngo get -u github.com/tidwall/sjson SJSON 核心用法 #基础值修改 #package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/tidwall/sjson\u0026#34; ) func main() { jsonStr := `{\u0026#34;name\u0026#34;:\u0026#34;张三\u0026#34;,\u0026#34;age\u0026#34;:25}` // 修改 age 值为 35 newJson, _ := sjson.Set(jsonStr, \u0026#34;age\u0026#34;, 35) fmt.Println(string(newJson)) } 嵌套结构修改 #package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/tidwall/sjson\u0026#34; ) func main() { jsonStr := `{ \u0026#34;name\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;age\u0026#34;: 25, \u0026#34;hobby\u0026#34;: { \u0026#34;h1\u0026#34;: \u0026#34;sing\u0026#34;, \u0026#34;h2\u0026#34;: \u0026#34;dance\u0026#34;, \u0026#34;h3\u0026#34;: \u0026#34;rap\u0026#34;, \u0026#34;h4\u0026#34;: \u0026#34;basketball\u0026#34; }` // 修改 hobby.h4 的值: basketball =\u0026gt; football newJson, _ := sjson.Set(jsonStr, \u0026#34;hobby.h4\u0026#34;, \u0026#34;football\u0026#34;) fmt.Println(string(newJson)) } 数组操作 #package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/tidwall/sjson\u0026#34; ) func main() { jsonStr := `{\u0026#34;hobby\u0026#34;: [\u0026#34;sing\u0026#34;,\u0026#34;dance\u0026#34;,\u0026#34;rap\u0026#34;,\u0026#34;basketball\u0026#34;]}` // 修改 hobby 数组第4个元素为 football newJson, _ := sjson.Set(jsonStr, \u0026#34;hobby.3\u0026#34;, \u0026#34;football\u0026#34;) fmt.Println(string(newJson)) // 追加 hobby 数组第5个元素为 game newJson, _ = sjson.Set(jsonStr, \u0026#34;tags.-1\u0026#34;, \u0026#34;game\u0026#34;) fmt.Println(string(newJson)) } 字段删除 #package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/tidwall/sjson\u0026#34; ) func main() { jsonStr := `{\u0026#34;name\u0026#34;:\u0026#34;张三\u0026#34;,\u0026#34;age\u0026#34;:25}` // 删除age字段 newJson, _ := sjson.Delete(jsonStr, \u0026#34;age\u0026#34;) fmt.Println(string(newJson)) } SJSON 与原生方案对比 # SJSON 摆脱结构体定义束缚，保持原始 json 结构完整性，避免修改后丢失未定义字段的问题。\nSJSON 路径直达修改位置，规避嵌套结构嵌套带来的问题，与 GJSON 组成完整处理链路。\nSJSON 支持运行时动态路径构建，避免硬编码路径带来的问题。\n","date":"2025-03-09","permalink":"https://vespeng.github.io/posts/go-sjson-component/","section":"Posts","summary":"前文介绍的 \u003ccode\u003eGJSON\u003c/code\u003e 解决了灵活读取问题，而 \u003ccode\u003eSJSON\u003c/code\u003e 作为其姊妹库，则专注于实现无需结构体定义的 json 动态修改。","title":"Go-SJSON 组件，JSON 动态修改新方案"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/json/","section":"Tags","summary":"","title":"Json"},{"content":"在 Go 语言开发领域，json 数据处理是极为常见的任务。Go 标准库提供了 encoding/json 包用于处理 json 数据，同时第三方库 GJSON \u0026amp; SJSON 也在 json 处理方面表现出色。\n本文将深入探讨下 GJSON 组件，通过与原生处理方式对比，它存在什么特别之处，它的优势体现在哪。\nGo 原生 json 读取方式 #Go 原生读取 json 数据，通常需先定义结构体，然后再将 json 数据解析到结构体实例，如：\n{ \u0026#34;name\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;age\u0026#34;: 25 } 具体处理逻辑：\npackage main import ( \u0026#34;encoding/json\u0026#34; \u0026#34;fmt\u0026#34; ) type Person struct { Name string `json:\u0026#34;name\u0026#34;` Age int `json:\u0026#34;age\u0026#34;` } func main() { jsonStr := `{\u0026#34;name\u0026#34;:\u0026#34;张三\u0026#34;,\u0026#34;age\u0026#34;:25}` var person Person err := json.Unmarshal([]byte(jsonStr), \u0026amp;person) if err!= nil { fmt.Println(\u0026#34;解析错误:\u0026#34;, err) return } fmt.Println(\u0026#34;Name:\u0026#34;, person.Name) fmt.Println(\u0026#34;Age:\u0026#34;, person.Age) } 这种方式虽能准确解析 json 数据，但如果 json 存在多层嵌套，层级过度包装，那么结构体定义以及解析过程就会变得相当繁琐。\nGJSON 组件 #概述 #GJSON 是一个轻量级且高性能的 JSON 解析库，它允许开发者通过简洁的语法，无需定义结构体，就能快速提取 JSON 数据中的特定值。\n官网地址：GitHub - tidwall/gjson\n安装 #使用 Go 的包管理工具 go get 安装 GJSON：\ngo get -u github.com/tidwall/gjson GJSON 基本用法 #简单 json 数据获取 #对于简单的 json，像前面那个例子，直接用 gjson.Get 方法，传入 json 字符串和要获取的字段名，就能拿到对应的值。比如获取 name 字段，gjson.Get(jsonStr, \u0026quot;name\u0026quot;) 就可以搞定，例如：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/tidwall/gjson\u0026#34; ) func main() { jsonStr := `{\u0026#34;name\u0026#34;:\u0026#34;张三\u0026#34;,\u0026#34;age\u0026#34;:25}` name := gjson.Get(jsonStr, \u0026#34;name\u0026#34;) age := gjson.Get(jsonStr, \u0026#34;age\u0026#34;) fmt.Println(\u0026#34;Name:\u0026#34;, name.String()) fmt.Println(\u0026#34;Age:\u0026#34;, age.Int()) } 嵌套 json 数据获取 #上述提到，原生的处理方式对于多层级的 json 很不友好，然而 gjon 可以直接通过点号分隔路径定位数据，这时候它的优势就逐渐明显，例如：\n{ \u0026#34;name\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;age\u0026#34;: 25, \u0026#34;hobby\u0026#34;: { \u0026#34;h1\u0026#34;: \u0026#34;sing\u0026#34;, \u0026#34;h2\u0026#34;: \u0026#34;dance\u0026#34;, \u0026#34;h3\u0026#34;: \u0026#34;rap\u0026#34;, \u0026#34;h4\u0026#34;: \u0026#34;basketball\u0026#34; } } 具体处理逻辑：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/tidwall/gjson\u0026#34; ) func main() { jsonStr := `{ \u0026#34;name\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;age\u0026#34;: 25, \u0026#34;hobby\u0026#34;: { \u0026#34;h1\u0026#34;: \u0026#34;sing\u0026#34;, \u0026#34;h2\u0026#34;: \u0026#34;dance\u0026#34;, \u0026#34;h3\u0026#34;: \u0026#34;rap\u0026#34;, \u0026#34;h4\u0026#34;: \u0026#34;basketball\u0026#34; }` name := gjson.Get(jsonStr, \u0026#34;name\u0026#34;) ball := gjson.Get(jsonStr, \u0026#34;hobby.h1\u0026#34;) fmt.Println(\u0026#34;name:\u0026#34;, name.String()) fmt.Println(\u0026#34;ball:\u0026#34;, ball.String()) } 相比原生方式，无需复杂结构体定义，操作更简便。\njson 数组获取 #如果在 json 中嵌套了数组，对于这种的处理也比较简单，直接通过数组下标来定位数据即可，如：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/tidwall/gjson\u0026#34; ) func main() { jsonStr := `{\u0026#34;hobby\u0026#34;: [\u0026#34;sing\u0026#34;,\u0026#34;dance\u0026#34;,\u0026#34;rap\u0026#34;,\u0026#34;basketball\u0026#34;]}` hobby := gjson.Get(jsonStr, \u0026#34;hobby.3\u0026#34;) // 输出第4个爱好 fmt.Println(\u0026#34;hobby:\u0026#34;, hobby.String()) } 相比于原生方式处理数组，得先解析成切片，操作起来就没这么直接。\nGJSON 与原生 JSON 处理方式对比 # GJSON 语法简单直观，熟悉 json 结构即可快速上手，无需学习结构体定义及标签使用等知识。而原生方式在结构体定义上相对复杂，尤其是处理复杂 json 结构时。\nGJSON 无需将整个 json 数据解析为结构体，在处理大型 json 数据时，内存占用少，解析速度快。原生方式在解析复杂 json 数据时，结构体构建和内存分配开销较大。\nGJSON 对各种复杂 json 结构都能灵活应对，根据需求按路径获取数据，无需频繁修改代码结构。原生方式则需根据 json 结构变化，频繁修改结构体定义，灵活性较差。\n","date":"2025-03-02","permalink":"https://vespeng.github.io/posts/go-gjson-component/","section":"Posts","summary":"Go 标准库提供了 \u003ccode\u003eencoding/json\u003c/code\u003e 包用于处理 json 数据，同时第三方库 \u003ccode\u003eGJSON\u003c/code\u003e \u0026amp; \u003ccode\u003eSJSON\u003c/code\u003e 也在 json 处理方面表现出色。","title":"Go-GJSON 组件，解锁 JSON 读取新姿势"},{"content":"最近 AI 大模型的热度可谓是如火如荼，尤其是国内的 DeepSeek 名闻遐迩，因其与 OpenAI 的模型相比，DeepSeek 推理大模型 R1 的训练成本仅为其 3%-5%，直接导致英伟达公司股票短时间内大跌。\n此时我就比较好奇，为什么大模型训练中更多的是依赖 GPU，而不是 CPU 呢？\n随后这两天网上翻阅了些资料，也询问 AI 帮忙解答，大体总结出如下几点：\n1. 并行计算能力\nGPU 具有高度并行的架构，能够同时处理大量的数据和计算任务。比如在进行矩阵运算时，它可以同时对多个元素进行操作，瞬间完成复杂的计算。\n相比之下，CPU 虽然在串行计算方面表现不错，但并行处理能力相对较弱，无法像 GPU 那样同时处理众多任务。\n2. 内存带宽\n大模型训练需要处理的数据量巨大，比如 DeepSeek R1 满血版参数就高达 671B （6710亿）这对内存带宽要求极高。\nGPU 配备了超高的内存带宽，就类似拥有一条宽阔的高速公路，数据可以快速地传输到计算核心进行处理。\n而 CPU 的内存带宽则相对有限，面对海量数据时，就像走在狭窄的小道上，容易形成数据传输的瓶颈，拖慢训练速度。\n3. 浮点运算性能\n在大模型训练中，离不开浮点运算。\nGPU 在浮点运算性能方面优势明显，它拥有大量的浮点运算单元，能够在单位时间内执行海量的浮点运算操作。\nCPU 的浮点运算能力相对较弱，在处理大规模的浮点运算时，效率远远比不上 GPU 。\n4. 深度学习框架支持\n现在主流的深度学习框架，像是 TensorFlow 和 PyTorch 等，都对 GPU 进行了深度优化和兼容，能够充分发挥其强大的性能，自动将计算任务合理分配到 GPU 上并行执行。\n然而，对于 CPU 的优化就没那么给力，导致在实际训练中，使用 CPU 的效果不尽如人意。\n5. 能耗效率\n常理来看 GPU 功耗好像挺高，但从单位时间完成的计算量来看，它的能耗效率还是可以的。\n虽然 CPU 功耗低，但完成相同的训练任务需要更长时间，综合能耗并不占优势。\n6. 成本效益\nGPU 单价相比 CPU 确实较高，但是它能大幅缩短训练时间。有句话说的好，时间就是金钱！这样算下来，总体的成本反而更低。\n而 CPU 训练时间长，资源占用和维护成本累加起来也不少。\n所以，综合以上这些方面看，GPU 在大模型训练中脱颖而出，而 CPU 则在这个领域就有点力不从心了。\n","date":"2025-02-15","permalink":"https://vespeng.github.io/posts/why-ai-needs-gpu-not-cpu/","section":"Posts","summary":"GPU 具有高度并行的架构，能够同时处理大量的数据和计算任务。相比之下，CPU 虽然在串行计算方面表现不错，但并行处理能力相对较弱，无法像 GPU 那样同时处理众多任务。","title":"为什么大模型训练依赖 GPU 而不是 CPU"},{"content":"在 Go 项目开发中，有效的异常处理是确保程序健壮性和稳定性的关键因素之一。全局异常处理机制能够统一处理项目中可能出现的各种异常情况，提高代码的可读性、可维护性以及错误处理的一致性。\nGo 中的错误处理机制 #在 Go 语言中，并没有像其他语言那样的传统异常机制。而是期望开发者主动去识别处理这种“异常”，通过返回值来表示可能出现的错误。\n通常情况下，函数会返回一个结果集和一个错误值，我们需要判断错误值是否为 nil，如果不为 nil 则表示出现了“异常”。\npackage main import ( \u0026#34;fmt\u0026#34; ) // 模拟一个会返回错误的函数 func divide(a, b int) (int, error) { if b == 0 { return 0, fmt.Errorf(\u0026#34;除数不能为 0\u0026#34;) } return a / b, nil } func main() { result, err := divide(10, 0) if err != nil { fmt.Println(\u0026#34;出错啦:\u0026#34;, err) return } fmt.Println(\u0026#34;结果是:\u0026#34;, result) } Go 中的 panic #当程序遇到无法处理的错误时，就会被提示panic，程序会直接崩溃。\nrecover 函数用于捕获 panic 抛出的信息，让程序从 panic 状态恢复继续正常执行，前提 recover 只能在 defer 函数中使用。\npackage main import ( \u0026#34;fmt\u0026#34; ) func main() { defer func() { if r := recover(); r != nil { fmt.Println(\u0026#34;已捕获到恐慌:\u0026#34;, r) } }() // 手动触发一个 panic panic(\u0026#34;这是一个恐慌！\u0026#34;) } // 输出： // 已捕获到恐慌:这是一个恐慌！ 实现全局异常处理 #根据上述其实不难发现，错误处理是显式的，我们可以做前置判断，根据具体情况进行处理，但是panic 处理通常是隐式的，一旦被调用 panic 函数，程序的执行流程会被打乱，需捕获 panic 才能恢复程序的正常执行。\n所以针对这种隐式的、在编程过程中无法提前预知的错误，就很有必要做一层异常的处理，最好可以是全局处理。\n为了实现全局异常处理，我们可以创建一个中间件或者全局的异常处理函数。\nfunc GlobalErrorHandler() gin.HandlerFunc { return func(c *gin.Context) { defer func() { if err := recover(); err!= nil { log.Printf(\u0026#34;Recovered from panic: %v\u0026#34;, err) c.JSON(500, gin.H{ \u0026#34;message\u0026#34;: \u0026#34;Internal Server Error\u0026#34;, }) c.Abort() } }() c.Next() } } 在项目中的应用 #在实际的项目中，我们可以将这个全局异常处理中间件应用到 HTTP 服务器的路由处理中。\npackage main import ( \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;log\u0026#34; ) func main() { r := gin.Default() // 应用全局异常处理中间件 r.Use(GlobalErrorHandler()) r.GET(\u0026#34;/ping\u0026#34;, func(c *gin.Context) { // 模拟异常 panic(\u0026#34;Something went wrong!\u0026#34;) }) r.Run(\u0026#34;:8080\u0026#34;) } 这样下来，在程序的后续处理中，一旦遇到 panic 就会被捕获，从而不影响程序的继续运行。\n","date":"2025-02-09","permalink":"https://vespeng.github.io/posts/go-practical-global-exception-handling/","section":"Posts","summary":"在 Go 项目开发中，有效的异常处理是确保程序健壮性和稳定性的关键因素之一。全局异常处理机制能够统一处理项目中可能出现的各种异常情况，提高代码的可读性、可维护性以及错误处理的一致性。","title":"Go 项目实战：全局异常处理"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/panic/","section":"Tags","summary":"","title":"Panic"},{"content":"在 Web 项目的开发过程中，能够在不同的环境（如开发、测试、生产）中进行灵活部署是至关重要的。不同环境通常需要不同的配置，如服务器端口、数据库连接信息、缓存设置等。\n对于 Java 的 SpringBoot 框架来说，可以直接在 application.yml 中指定一个环境配置文件，通常application_dev.yml 代表开发环境，那么 go 可否参考这种方式呢？\n接下来本文将详细介绍如何使用多种方式来实现多环境开发部署，重点围绕 config.yaml 文件和 config.go 文件来进行配置读取和环境区分。\nGo 中的系统环境变量 #先来解释一个概念，在 Go 语言中，系统环境变量是操作系统为每个进程提供的键值对集合。这些环境变量可以用于配置应用程序的行为、连接数据库、设置日志级别等。Go 提供了标准库 os 来读取和操作这些环境变量。\n实战 #编写 .env 文件 #在项目根目录下新建一个 .env 文件，配置如下：\nAPP_ENV=dev 获取环境变量 #使用 os.Getenv 函数可以获取指定名称的环境变量值。\nenv := os.Getenv(\u0026#34;APP_ENV\u0026#34;) APP_ENV 是一个环境变量名，用于标识应用程序的运行环境（如开发、测试、生产等）。 如果 APP_ENV 未设置，os.Getenv(\u0026quot;APP_ENV\u0026quot;) 将返回空字符串。 设置默认值 #我们需要一个默认的环境，如果 APP_ENV 未设置，将其设为 \u0026quot;dev\u0026quot;：\nenv := os.Getenv(\u0026#34;APP_ENV\u0026#34;) if env == \u0026#34;\u0026#34; { env = \u0026#34;dev\u0026#34; // 默认环境为 dev } 这样可以确保即使没有显式设置 APP_ENV，程序也能有一个合理的默认行为。\n加载 .env 文件 #使用 github.com/joho/godotenv 包来加载 .env 文件中的环境变量。.env 文件通常用于本地开发环境，避免将敏感信息硬编码到代码中，这里其实挺像 vue 的环境加载方式。\nerr := godotenv.Load() if err != nil { return fmt.Errorf(\u0026#34;加载 .env 文件失败: %v\u0026#34;, err) } 这行代码会读取项目根目录下的 .env 文件，并将其中定义的环境变量加载到当前进程中。\n动态选择配置文件 #根据 APP_ENV 的值，动态选择不同的配置文件：\nviper.AddConfigPath(\u0026#34;./config\u0026#34;) viper.SetConfigName(fmt.Sprintf(\u0026#34;config_%s\u0026#34;, env)) viper.SetConfigType(\u0026#34;yaml\u0026#34;) err = viper.ReadInConfig() if err != nil { return fmt.Errorf(\u0026#34;读取配置文件失败: %v\u0026#34;, err) } 这段代码会根据 APP_ENV 的值（例如 dev 或 production），选择对应的配置文件（如 config_dev.yaml 或 config_prod.yaml）。这样可以根据不同的环境加载不同的配置。\n解析配置文件 #使用 viper.Unmarshal 将配置文件的内容解析到结构体中：\nConf = \u0026amp;Config{} err = viper.Unmarshal(Conf) if err != nil { return fmt.Errorf(\u0026#34;解析配置文件失败: %v\u0026#34;, err) } viper.Unmarshal 会将配置文件中的键值对映射到结构体字段上，前提是结构体字段标签（如 yaml 和 mapstructure）与配置文件中的键匹配。\n通过上述方式，我们可以根据项目的实际需求和情况，确保项目在不同环境下都能正确配置并稳定运行。\n","date":"2025-01-25","permalink":"https://vespeng.github.io/posts/go-practical-multi-environment-development/","section":"Posts","summary":"在 Web 项目的开发过程中，能够在不同的环境（如开发、测试、生产）中进行灵活部署是至关重要的。不同环境通常需要不同的配置，如服务器端口、数据库连接信息、缓存设置等。","title":"Go 项目实战：如何部署多环境开发"},{"content":"在 Go 项目开发中，日志处理是一项至关重要的任务。它不仅有助于我们在开发过程中调试代码，还能在生产环境中帮助我们快速定位问题。本文将详细介绍如何在 Go 项目中优雅地处理日志，包括日志的级别、格式、输出以及如何使用第三方日志库等方面。\n日志级别的重要性 #日志级别是控制日志输出的重要手段。通过设置不同的日志级别，我们可以灵活地控制日志的详细程度。在 Go 语言中，常见的日志级别有DEBUG、INFO、WARN、ERROR和FATAL。不同级别的日志用于记录不同类型的信息，例如：\nDEBUG：用于记录详细的调试信息，仅在开发环境中启用。 INFO：用于记录正常的业务流程信息，例如请求的处理、数据的加载等。 WARN：用于记录可能存在的问题或异常情况，但不影响系统的正常运行。 ERROR：用于记录严重的错误信息，这些错误可能导致系统无法正常运行。 FATAL：用于记录非常严重的错误信息，这些错误会导致程序立即退出。 日志格式的选择 #日志格式的选择对于日志的可读性和分析性至关重要。一个好的日志格式应该包含足够的信息，以便我们能够快速定位问题。常见的日志格式有 JSON、XML 和文本格式等。\n在 Go 语言中，我们可以使用第三方库来实现不同的日志格式。例如，使用 logrus 库可以轻松地将日志格式化为 JSON 格式：\npackage main import ( \u0026#34;github.com/sirupsen/logrus\u0026#34; ) func main() { // 设置日志格式为JSON logrus.SetFormatter(\u0026amp;logrus.JSONFormatter{}) // 记录不同级别的日志 logrus.Debug(\u0026#34;这是一条DEBUG级别的日志\u0026#34;) logrus.Info(\u0026#34;这是一条INFO级别的日志\u0026#34;) logrus.Warn(\u0026#34;这是一条WARN级别的日志\u0026#34;) logrus.Error(\u0026#34;这是一条ERROR级别的日志\u0026#34;) logrus.Fatal(\u0026#34;这是一条FATAL级别的日志\u0026#34;) } 日志输出的方式 #日志输出的方式有很多种，例如输出到控制台、文件、数据库等。在 Go 语言中，我们可以使用标准库的log包来实现基本的日志输出功能。例如，使用标准库 log.Println 方法可以将日志输出到控制台：\npackage main import \u0026#34;log\u0026#34; func main() { // 记录日志到控制台 log.Println(\u0026#34;这是一条日志信息\u0026#34;) } 如果需要将日志输出到文件，我们可以这么做：\npackage main import ( \u0026#34;log\u0026#34; \u0026#34;os\u0026#34; ) func main() { // 创建日志文件 file, err := os.OpenFile(\u0026#34;app.log\u0026#34;, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil { log.Fatal(err) } defer file.Close() // 设置日志输出到文件 log.SetOutput(file) // 记录日志 log.Println(\u0026#34;这是一条日志信息\u0026#34;) } 除了常见的输出到控制台或指定文件，我们还可以将日志输出到数据库、Elasticsearch 等其他存储介质中。具体的实现方式还需根据实际需求进行选择。\n使用第三方日志库 #虽然说 Go 语言的标准库提供了基本的日志处理功能，但在实际项目中，往往需要结合第三方更为强大的库来满足日常需求：\nlogrus：一个功能强大的日志库，支持多种日志格式、日志级别、日志输出方式等。 zap：一个高性能的日志库，具有快速、灵活、可扩展等特点。 zerolog：一个极简主义的日志库，专注于提供高性能和简单的 API。 ps: 上述简介来自于AI，注意甄别\n这些第三方日志库都提供了丰富的功能和灵活的配置选项，可以帮助我们更好地处理日志。在这里个人比较推荐 logrus ，不过具体需求还是得具体选择。\n实战 #介绍再多也是空谈，接下来结合具体的项目，我们优雅的配置一下。\n默认项目已经安装 logrus ，没有的话可以执行下如下命令：\ngo get github.com/sirupsen/logrus 配置config.yaml #为了方便随时更改切换 log 级别或者输出格式，我们可以单独抽离出来实现配置化：\nlog: format: json # 输出格式 level: debug # 日志级别 report_caller: true # 是否开启调试 配置config.go #有了参数配置，还缺一步解析：\n具体的解析可以参考 Go 项目实战：搭建高效的 Gin Web 目录结构\n新建logger.go #在这里我们统一配置 logrus 参数，包括日志级别，输出格式：\npackage app import ( log \u0026#34;github.com/sirupsen/logrus\u0026#34; ) // InitializeLogger 设置日志输出 func InitializeLogger() error { // 设置日志格式 switch config.Conf.Log.Format { case \u0026#34;json\u0026#34;: log.SetFormatter(\u0026amp;log.JSONFormatter{}) case \u0026#34;text\u0026#34;: log.SetFormatter(\u0026amp;log.TextFormatter{}) default: log.SetFormatter(\u0026amp;log.JSONFormatter{}) } // 设置日志级别 switch config.Conf.Log.Level { case \u0026#34;debug\u0026#34;: log.SetLevel(log.DebugLevel) case \u0026#34;info\u0026#34;: log.SetLevel(log.InfoLevel) case \u0026#34;warn\u0026#34;: log.SetLevel(log.WarnLevel) case \u0026#34;error\u0026#34;: log.SetLevel(log.ErrorLevel) case \u0026#34;fatal\u0026#34;: log.SetLevel(log.FatalLevel) case \u0026#34;panic\u0026#34;: log.SetLevel(log.PanicLevel) default: log.SetLevel(log.InfoLevel) } // 设置打印调用信息 log.SetReportCaller(config.Conf.Log.ReportCaller) return nil } 输出日志到文件 #控制台打印日志，肯定是不满足一个项目的正常使用的，我们非常有必要将日志持久化到一个单独文件中。\n但是这样还不够，会存在另一个问题：日志文件会越来越大后期不利于日志排查。所以还需要对日志进行一个分割，最好的实践方式就是按天分割，所以我们接着在上述初始化文件中去做设置：\npackage app import ( \u0026#34;os\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/lestrrat-go/file-rotatelogs\u0026#34; log \u0026#34;github.com/sirupsen/logrus\u0026#34; \u0026#34;your_project/config\u0026#34; ) // InitializeLogger 设置日志输出并初始化日志文件 func InitializeLogger() error { // 设置日志格式 ... // 设置日志级别 ... // 设置打印调用信息 ... // 创建日志目录 logDir := \u0026#34;../logs\u0026#34; err := os.MkdirAll(logDir, 0755) if err != nil { log.Fatalf(\u0026#34;创建日志目录失败: %v\u0026#34;, err) } // 设置日志输出，按天切割 logFilePath := logDir + \u0026#34;/app.%Y%m%d.log\u0026#34; writer, err := rotatelogs.New( logFilePath, rotatelogs.WithLinkName(logDir+\u0026#34;/app.log\u0026#34;), rotatelogs.WithMaxAge(7*24*time.Hour), // 保留7天 rotatelogs.WithRotationTime(24*time.Hour), // 每天切割一次 ) if err != nil { log.Fatalf(\u0026#34;设置日志输出失败: %v\u0026#34;, err) } log.SetOutput(writer) return nil } 调用InitializeLogger() #package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; log \u0026#34;github.com/sirupsen/logrus\u0026#34; \u0026#34;your_project/config\u0026#34; \u0026#34;your_project/internal/api/v1\u0026#34; \u0026#34;your_project/internal/app\u0026#34; ) func main() { // 加载配置文件 err := config.LoadConfig() if err != nil { log.Error(\u0026#34;配置文件加载错误: %v\u0026#34;, err) return } // 初始化 logger err = InitializeLogger() if err != nil { log.Error(\u0026#34;logger 初始化错误: %v\u0026#34;, err) return } r := gin.Default() v1.SetupRoutes(r, Engine) err = r.Run(fmt.Sprintf(\u0026#34;:%d\u0026#34;, config.Conf.App.Port)) if err != nil { log.Error(\u0026#34;服务启动错误: %v\u0026#34;, err) return } } 到这里一个完整的日志流程就算是配置好了。\n","date":"2025-01-22","permalink":"https://vespeng.github.io/posts/go-practical-processing-log/","section":"Posts","summary":"在 Go 项目开发中，日志处理是一项至关重要的任务。本文将详细介绍如何在 Go 项目中优雅地处理日志，包括日志的级别、格式、输出以及如何使用第三方日志库等方面。","title":"Go 项目实战：如何优雅的处理日志"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/log/","section":"Tags","summary":"","title":"Log"},{"content":"引言 #在当今迅速迭代的软件开发领域，挑选合适的工具与框架对于项目能否顺利推进至关重要。Gin 框架，作为 Go 语言生态中备受青睐的 Web 开发框架，凭借其卓越的性能、简洁的设计以及丰富的功能特性，在众多选项中脱颖而出。本文旨在深入剖析如何在使用 Gin 框架的过程中，构建一个既高效又便于管理的项目架构，助力开发者打造既快速响应又易于维护的 Web 应用程序。\nGin 概述 #引入官网的描述：Gin 是一个使用 Go 语言开发的 Web 框架。 它提供类似 Martini 的 API，但性能更佳，速度提升高达40倍。 如果你是性能和高效的追求者, 你会爱上 Gin。\n对比 Beego 框架，Gin 框架采用了极简主义的方法，为追求简单和高性能，没有多余文件或目录，他甚至什么也没有，没有集成任何中间件，一个 main 文件即可启动一个web服务。\n正因为如上所述，过分精简对于开发一个项目来说，前期的项目搭建工作就显得尤为重要。\n项目结构设计 #有过 Java 开发经验的伙伴应该了解，SpringBoot 遵循着 MVC 的设计理念，这一套设计理念一直沿用至今，他的优秀难以言喻，Gin 框架完全可以参照这个模式来做，如下是我个人设计的一套架构：\n├── /cmd │ └── main.go ├── /configs │ └── config.yaml ├── /docs ├── /internal │ ├── /api │ │ ├── v1 │ │ │ ├── /routes.go │ ├── /app │ │ ├── bootstrap.go │ │ ├── config.go │ │ ├── db.go │ │ └── ... │ ├── /controller │ │ ├── user_controller.go │ │ └── ... │ ├── /middleware │ │ ├── error.go │ │ └── ... │ ├── /model │ │ ├── user_entity.go │ │ └── ... │ ├── /repository │ │ ├── user_repository.go │ │ └── ... │ ├── /service │ │ ├── user_service.go │ │ └── ... │ └── /utils ├── /pkg ├── /scripts ├── /tests ├── .env ├── go.mod ├── go.sum 目录职责 # /cmd 存放应用的入口文件。 main.go：是整个应用的入口，在这里启动应用。 /configs 存放应用的配置文件和配置加载逻辑。 config.yaml：应用的配置文件，通常包含数据库连接信息、服务器设置等。 /docs 存放应用的文档，如API文档、用户手册等。 /internal 存放应用的内部逻辑，这些代码不能被外部包所引入，可根据实际需求进而拆分目录。 api：包含应用中核心的业务路由等，即URL路径与控制器方法的映射。 app：包含应用的核心逻辑，如初始化、启动等。 controller：包含控制器逻辑，处理请求并返回响应。 middleware：存放中间件代码，用于在请求处理流程中的特定阶段执行代码。 model：定义应用的数据模型，通常与数据库表结构对应。 repository：实现数据访问逻辑，与数据库进行交互。 service：实现业务逻辑，调用repository中的方法来处理业务需求。 utils：包含通用的工具函数，这些函数可以被多个包所共享。 /pkg 存放第三方库，如第三方中间件、工具库等。 /scripts 存放各种脚本，如项目部署脚本、测试脚本等。 /tests 存放测试代码，包括单元测试、集成测试等。 这里的目录结构可以根据需要自行组织，以支持不同类型的测试。 以上目录结构有助于清晰地分离应用的不同部分，使得代码更加模块化、易于理解和维护。同时，我也参照众多优秀开源项目的目录搭建思想，使其完美遵循了 Go 语言的最佳实践。\n实战 #目录搭建好后，开始填充代码\n下边简单实现集成数据库，配置路由，启动服务\n配置config #在 config.yaml 文件下配置端口和数据库连接，这里选择xorm：\n# 基础配置 app: port: 8080 database: driver: mysql source: root:123456@tcp(127.0.0.1:3306)/xxx_table?charset=utf8mb4\u0026amp;parseTime=True\u0026amp;loc=Local 在 config.go 下解析配置\npackage config import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/spf13/viper\u0026#34; ) type Config struct { App AppConfig `yaml:\u0026#34;app\u0026#34; mapstructure:\u0026#34;app\u0026#34;` Database DatabaseConfig `yaml:\u0026#34;database\u0026#34; mapstructure:\u0026#34;database\u0026#34;` } type AppConfig struct { Port int `mapstructure:\u0026#34;port\u0026#34;` } type DatabaseConfig struct { Driver string `yaml:\u0026#34;driver\u0026#34; mapstructure:\u0026#34;driver\u0026#34;` Source string `yaml:\u0026#34;source\u0026#34; mapstructure:\u0026#34;source\u0026#34;` } var Conf *Config // LoadConfig 加载配置文件 func LoadConfig() error { // 设置配置文件路径和名称 viper.AddConfigPath(\u0026#34;./configs\u0026#34;) viper.SetConfigName(\u0026#34;config\u0026#34;) viper.SetConfigType(\u0026#34;yaml\u0026#34;) // 读取配置文件 err = viper.ReadInConfig() if err != nil { return fmt.Errorf(\u0026#34;读取配置文件失败: %v\u0026#34;, err) } // 将配置文件内容解析到 Conf 变量中 Conf = \u0026amp;Config{} err = viper.Unmarshal(Conf) if err != nil { return fmt.Errorf(\u0026#34;解析配置文件失败: %v\u0026#34;, err) } return nil } 配置init #数据库及其他的初始化统一放置到 app 目录下，在这里新建 loader.go 来初始化 mysql，但是为了之后方便管理，我们另单独创建 db.go 文件：\n如需要加载其他如 redis，那就新建 redis.go 文件\npackage app import ( _ \u0026#34;github.com/go-sql-driver/mysql\u0026#34; \u0026#34;github.com/go-xorm/xorm\u0026#34; log \u0026#34;github.com/sirupsen/logrus\u0026#34; \u0026#34;yourProject/config\u0026#34; ) var Engine *xorm.Engine // InitializeMySQL 数据库初始化 func InitializeMySQL() error { var err error // 创建数据库引擎 Engine, err = xorm.NewEngine(config.Conf.Database.Driver, config.Conf.Database.Source) if err != nil { log.Errorf(\u0026#34;数据库初始化失败: %v\u0026#34;, err) return err } // 测试数据库连接 if err = Engine.Ping(); err != nil { log.Errorf(\u0026#34;数据库连接失败: %v\u0026#34;, err) return err } return nil } app.go 中调用 InitializeMySQL()\npackage app import ( \u0026#34;fmt\u0026#34; ) // InitializeAll 初始化所有模块 func InitializeAll() error { err := InitializeMySQL() if err != nil { return fmt.Errorf(\u0026#34;MySQL初始化错误: %v\u0026#34;, err) } return nil } 配置model #在 model 下新建 user_entity.go，注意：这个需要和数据库对应\npackage model type User struct { Id int64 `xorm:\u0026#34;pk autoincr \u0026#39;id\u0026#39;\u0026#34;` UserID int64 `xorm:\u0026#34;not null \u0026#39;user_id\u0026#39;\u0026#34;` Password string `xorm:\u0026#34;varchar(50) not null \u0026#39;password\u0026#39;\u0026#34;` UserName string `xorm:\u0026#34;varchar(30) \u0026#39;user_name\u0026#39;\u0026#34;` Email string `xorm:\u0026#34;varchar(50) \u0026#39;email\u0026#39;\u0026#34;` PhoneNumber int64 `xorm:\u0026#34;\u0026#39;phone_number\u0026#39;\u0026#34;` Sex string `xorm:\u0026#34;char(1) \u0026#39;sex\u0026#39;\u0026#34;` Remark string `xorm:\u0026#34;varchar(500) \u0026#39;remark\u0026#39;\u0026#34;` } // TableName 方法用于返回表名 func (u User) TableName() string { return \u0026#34;user\u0026#34; } 配置controller #在 controller 下新建 user_controller.go\npackage controller import ( \u0026#34;your_project/internal/service\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;net/http\u0026#34; ) type UserController struct { UserService *service.UserService } func NewUserController(UserService *service.UserService) *UserController { return \u0026amp;UserController{UserService: UserService} } func (uc *UserController) GetUsers(c *gin.Context) { users, err := uc.UserService.GetUsers() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{\u0026#34;error\u0026#34;: \u0026#34;Failed to fetch users\u0026#34;}) return } c.JSON(http.StatusOK, gin.H{\u0026#34;users\u0026#34;: users}) } 配置service #在 service 下新建 user_service.go\npackage service import ( \u0026#34;your_project/internal/model\u0026#34; \u0026#34;your_project/internal/repository\u0026#34; \u0026#34;github.com/go-xorm/xorm\u0026#34; ) type UserService struct { userRepo *repository.UserRepository } func NewUserService(engine *xorm.Engine) *UserService { return \u0026amp;UserService{userRepo: repository.NewUserRepository(engine)} } func (us *UserService) GetUsers() ([]*model.User, error) { return us.userRepo.GetUsers() } 配置repository #在 repository 下新建 user_repo.go\npackage repository import ( \u0026#34;your_project/internal/model\u0026#34; \u0026#34;github.com/go-xorm/xorm\u0026#34; ) type UserRepository struct { engine *xorm.Engine } func NewUserRepository(engine *xorm.Engine) *UserRepository { return \u0026amp;UserRepository{engine: engine} } // GetUsers 获取所有用户 func (r *UserRepository) GetUsers() ([]*model.User, error) { var users []*model.User err := r.engine.Table(model.User{}.TableName()).Find(\u0026amp;users) return users, err } 配置api #routes.go 中设置路由，这里设置路由组，为方便日后迭代\npackage v1 import ( \u0026#34;github.com/gin-gonic/gin\u0026#34; \u0026#34;github.com/go-xorm/xorm\u0026#34; \u0026#34;your_project/internal/controller\u0026#34; \u0026#34;your_project/internal/service\u0026#34; ) func SetupRoutes(r *gin.Engine, engine *xorm.Engine) { // 定义用户路由组 user := r.Group(\u0026#34;/user\u0026#34;) { // 创建 UserService 实例 UserService := service.NewUserService(engine) // 创建 UserController 实例 UserController := controller.NewUserController(UserService) user.GET(\u0026#34;/\u0026#34;, UserController.GetUsers) } } 配置bootstrap #package app import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/gin-gonic/gin\u0026#34; log \u0026#34;github.com/sirupsen/logrus\u0026#34; \u0026#34;your_project/config\u0026#34; \u0026#34;your_project/internal/api/v1\u0026#34; \u0026#34;your_project/internal/app\u0026#34; ) func Start() { // 加载配置文件 err := config.LoadConfig() if err != nil { log.Errorf(\u0026#34;配置文件加载错误: %v\u0026#34;, err) return } // 初始化所有模块 err = InitializeAll() if err != nil { log.Errorf(\u0026#34;模块初始化错误: %v\u0026#34;, err) return } r := gin.Default() v1.SetupRoutes(r, Engine) err = r.Run(fmt.Sprintf(\u0026#34;:%d\u0026#34;, config.Conf.App.Port)) if err != nil { log.Errorf(\u0026#34;服务启动错误: %v\u0026#34;, err) return } } 配置main #package app import \u0026#34;your_project/internal/app\u0026#34; func main() { app.Start() } 截至这里，一个基本的查询请求就已构建完成\n启动项目 #cmd 目录下直接运行 main 函数，正常会输出如下信息：\nListening and serving HTTP on :8080 接着访问 http://localhost:8080/user 正常查询结果回显 json 如下：\n{ \u0026#34;users\u0026#34;: [ { \u0026#34;Id\u0026#34;: 1, \u0026#34;UserID\u0026#34;: \u0026#34;000001\u0026#34;, \u0026#34;Password\u0026#34;: \u0026#34;123456\u0026#34;, ... } ] } 上述示例，已上传至 GitHub - vespeng/gin-pathway，欢迎 fork 和 star。\n","date":"2025-01-19","permalink":"https://vespeng.github.io/posts/go-practical-gin-directory-structure/","section":"Posts","summary":"Gin框架，作为Go语言生态中备受青睐的Web开发框架，凭借其卓越的性能、简洁的设计以及丰富的功能特性，在众多选项中脱颖而出。本文旨在深入剖析如何在使用Gin框架的过程中，构建一个既高效又便于管理的项目架构。","title":"Go 项目实战：搭建高效的 Gin Web 目录结构"},{"content":"简介 #你好，我是 Vespeng 👋\n一名后端研发，致力于构建可靠、可维护的服务与系统，持续追求代码清晰、架构健壮，并在性能、可读性与可维护性之间寻求更优解。\n本站点作为个人技术实践与学习思考的沉淀平台，记录项目实践中的问题分析、解决方案及经验总结，偶尔也分享些许日常生活中的观察与感悟。 但所有内容均基于个人理解和特定上下文，不构成任何专业建议，也不保证绝对正确或适用于你的环境。 如果你参考了其中的方法，请务必结合自身情况审慎评估。\n此外，无论你是想讨论技术细节、指出文章中的疏漏、探讨潜在的合作可能，还是只是偶然读到某篇内容后想打个招呼——都欢迎随时联系我。 你可以在任意文章下方留言评论，也可以按如下方式联系到我，我会认真阅读每一条消息。\n最后，很高兴在这里遇见你 🌱\n联系方式 #邮箱：hi@vespeng.com\n微信: Vespeng\n","date":"2025-01-12","permalink":"https://vespeng.github.io/about/","section":"Vespeng.Record","summary":"","title":"关于"},{"content":"首先我们先明确下 json 包下 Unmarshal() 函数是什么：\n它是 Go 语言标准库 encoding/json 中的一个函数，用于将 JSON 数据解析为 Go 语言中的数据结构。它的作用是将一个 JSON 格式的字节切片（[]byte）转换为对应的 Go 语言数据类型，如结构体、切片、映射等。\n其次了解了它的作用后，再来看下这个坑点：\n假设有一个 json 串如下：\n{ \u0026#34;id\u0026#34;: 1, \u0026#34;name\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;age\u0026#34;: 20 } 现在要将它解析成一个 map，拿到 json 原始的数据，方便后续处理：\nfunc main() { str := \u0026#34;{\\\u0026#34;id\\\u0026#34;:1,\\\u0026#34;name\\\u0026#34;:\\\u0026#34;张三\\\u0026#34;,\\\u0026#34;age\\\u0026#34;:20}\u0026#34; jsonMap := make(map[string]interface{}) json.Unmarshal([]byte(str), \u0026amp;amp;jsonMap) // 遍历map for key, value := range jsonMap { fmt.Printf(\u0026#34;key: %s, value: %v\\n\u0026#34;, key, value) } } // 输出： // key: id, value: 1 // key: name, value: 张三 // key: age, value: 20 这样看着确实没什么问题，每个 key、value 值都是按照预期输出；\n现在我把 json 调整一下，假设 id 是一个毫秒级时间戳 1736325205000（13 位）：\nfunc main() { str := \u0026#34;{\\\u0026#34;id\\\u0026#34;:1736325205000,\\\u0026#34;name\\\u0026#34;:\\\u0026#34;张三\\\u0026#34;,\\\u0026#34;age\\\u0026#34;:20}\u0026#34; jsonMap := make(map[string]interface{}) json.Unmarshal([]byte(str), \u0026amp;amp;jsonMap) // 遍历map for key, value := range jsonMap { fmt.Printf(\u0026#34;key: %s, value: %v\\n\u0026#34;, key, value) } } // 输出 // key: id, value: 1.736325205e+12 // key: name, value: 张三 // key: age, value: 20 此时坑来了， id 的值变成了一个科学计数法的字符串，显然这不符合我的预期；\n那么为什么会变成这样呢？\n首先观察到我使用了 %v 进行处理，然而 json 中原本的数据是一个 int，我应该用处理 int 的占位符 %d ：\nfunc main() { str := \u0026#34;{\\\u0026#34;id\\\u0026#34;:1736325205000,\\\u0026#34;name\\\u0026#34;:\\\u0026#34;张三\\\u0026#34;,\\\u0026#34;age\\\u0026#34;:20}\u0026#34; jsonMap := make(map[string]interface{) json.Unmarshal([]byte(str), \u0026amp;amp;jsonMap) fmt.Printf(\u0026#34;%d\u0026#34;,jsonMap[\u0026#34;id\u0026#34;]) } // 输出 // %!d(float64=1.736325205e+12) 到这里本以为是 ok 的，结果输出了这么个玩意，仔细读一下发现 float64 ，输出这个的原因是我要把一个 float64 的元素强行用 int 类型的占位符进行处理；\n所以现在进一步清晰了，json.Unmarshal 函数会把 id 转为 float64；\n那么问题又来了，为什么它会把 id 转为 float64 类型呢？id == 1 的时候为什么能正常输出呢？\n进源码，看看函数内部做了什么：\nfunc Unmarshal(data []byte, v any) error { // Check for well-formedness. // Avoids filling out half a data structure // before discovering a JSON syntax error. var d decodeState err := checkValid(data, \u0026amp;amp;d.scan) if err != nil { return err } d.init(data) return d.unmarshal(v) } // 可以看到 checkValid() 方法引用了 decodeState 结构体 // 进结构体里看下： // decodeState represents the state while decoding a JSON value. type decodeState struct { data []byte off int // next read offset in data opcode int // last read result scan scanner errorContext *errorContext savedError error useNumber bool disallowUnknownFields bool } // 初步观察有个 bool 类型的 useNumber 属性 // 接着看下这个结构体具体的实现方法： // convertNumber converts the number literal s to a float64 or a Number // depending on the setting of d.useNumber. func (d *decodeState) convertNumber(s string) (any, error) { if d.useNumber { return Number(s), nil } f, err := strconv.ParseFloat(s, 64) if err != nil { return nil, \u0026amp;amp;UnmarshalTypeError{Value: \u0026#34;number \u0026#34; + s, Type: reflect.TypeOf(0.0), Offset: int64(d.off)} } return f, nil } // 到这里大概能清楚，是这个方法把我的 id 转成了 float64，但是再转之前还有一层 if 会把原始值输出； // 接下来就回去上一级，看看 d.scan 到底做了什么： // 努力中... // ——————看不懂 经过多方查找：\n理论上 json 会把超过 int64 长度的数值转成 float64，但是这个说法经实践不成立，毫秒级时间戳 13 位，远没有超过 int64 的最大长度；\n多次翻阅资料后，有一个说法比较靠谱：\n当处理非常大的整数（如毫秒级的时间戳）时，如果直接使用 Go 语言中的整数类型（如 int 或 int64），可能会因为超出这些类型的表示范围而导致溢出。虽然 int64 类型在大多数情况下可以容纳毫秒级的时间戳，但为了确保能够处理所有可能的 JSON 数字，encoding/json 包选择了 float64 类型作为默认解析结果。\n到这里其实我们最初的目的也能够轻松处理：\nfunc main() { str := \u0026#34;{\\\u0026#34;id\\\u0026#34;:1736325205000,\\\u0026#34;name\\\u0026#34;:\\\u0026#34;张三\\\u0026#34;,\\\u0026#34;age\\\u0026#34;:20}\u0026#34; jsonMap := make(map[string]interface{}) json.Unmarshal([]byte(str), \u0026amp;amp;jsonMap) // 断言类型为 float64 fmt.Println(jsonMap[\u0026#34;id\u0026#34;]) if f, ok := jsonMap[\u0026#34;id\u0026#34;].(float64); ok { fmt.Println(int(f)) } } // 输出 // 1736325205000 ","date":"2025-01-08","permalink":"https://vespeng.github.io/posts/json-unmarshall-parsing-numeric-types/","section":"Posts","summary":"","title":"Json.Unmarshal 解析数值类型（踩坑）"},{"content":"高效地处理多个数据源并将其整合为有意义的结果是开发中一项重要的任务。Go 语言，以其强大的并发特性，为我们提供了优雅而高效的解决方案。那么我们探讨一下如何利用 Go 语言的协程，同时调用多个接口获取数据，并将这些数据无缝地合并为一个完整的数据集。\n先假定一个场景：\n现有一需求，需要请求n个接口（暂定为3个）获取接口数据，然后对数据进行二次处理并返回。\n按照过往的经验，我们会依次请求接口拿到数据暂存，最后对数据进行包装处理，这种自上而下的处理方式其实并无不妥，现在想要提高下效率，利用牺牲 cpu 资源来换取查询性能。\n先模拟创建几个接口，分别返回(k1,v1)、(k2,v2)、(k3,v4)：\n// 模拟接口A func getDataFromA() map[string]interface{} { return map[string]interface{}{ \u0026#34;key1\u0026#34;: \u0026#34;value1\u0026#34;, } } // 模拟接口B func getDataFromB() map[string]interface{} { return map[string]interface{}{ \u0026#34;key2\u0026#34;: \u0026#34;value2\u0026#34;, } } // 模拟接口C func getDataFromC() map[string]interface{} { return map[string]interface{}{ \u0026#34;key3\u0026#34;: \u0026#34;value3\u0026#34;, } } 开启协程分别请求上述接口：\n首先得思考一个问题，协程执行不保证顺序，请求到的数据应该怎么保存？怎么判断全部协程都执行完毕？怎么拿到全部的数据？\n上述接口定义中返回的数据均是map，那么我完全可以用map来保存数据，所以我定义方法就可以这么定义：\nfunc getAllData() map[string]interface{} { return nil // 暂时先不做处理 } 为了防止主协程先于其他执行结束，需要引入 sync.WaitGroup 包控制；所有协程返回的数据，可以用通道来暂存，make 一个容量为 3 的 channel：\nfunc getAllData() map[string]interface{} { var wg sync.WaitGroup resultChan := make(chan map[string]interface{}, 3) return nil // 暂时先不做处理 } 接下来就可以开启协程去调用:\nfunc getAllData() map[string]interface{} { var wg sync.WaitGroup resultChan := make(chan map[string]interface{}, 3) wg.Add(3) go func() { defer wg.Done() resultChan \u0026lt;- getDataFromA() }() go func() { defer wg.Done() resultChan \u0026lt;- getDataFromB() }() go func() { defer wg.Done() resultChan \u0026lt;- getDataFromC() }() wg.Wait() close(resultChan) return nil // 暂时先不做处理 } 最后可以对数据做个简单处理，封装成一个大map返回，实际业务当然按需处理：\nnewMap := make(map[string]interface{}) for res := range resultChan { for k, v := range res { newMap [k] = v } } return newMap 执行验证返回结果：\n\u0026gt; [Running] go run \u0026#34;main.go\u0026#34; \u0026gt; map[key1:value1 key2:value2 key3:value3] ","date":"2024-12-22","permalink":"https://vespeng.github.io/posts/collaborative-processing-of-multiple-interfaces/","section":"Posts","summary":"高效地处理多个数据源并将其整合为有意义的结果是开发中一项重要的任务。这里我们探讨一下如何利用 Go 语言的协程，同时调用多个接口获取数据，并将这些数据无缝地合并为一个完整的数据集。","title":"Go 并发实战：利用协程处理多个接口数据"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/%E5%B9%B6%E5%8F%91/","section":"Tags","summary":"","title":"并发"},{"content":"","date":null,"permalink":"https://vespeng.github.io/tags/%E5%8D%8F%E7%A8%8B/","section":"Tags","summary":"","title":"协程"},{"content":"在如今的编程领域，一个程序能够同时处理多个任务的能力非常重要，而 Golang 在并发编程方面表现十分出色，具有很多独特的优势。\n轻量级的协程（Goroutine） #在传统的像 Java 这样的编程语言中，创建线程来实现并发往往需要较大的资源开销和复杂的管理。但在 Golang 里，有了 Goroutine 就截然不同。\nGoroutine 的创建几乎不费力气，我们可以毫无压力地同时启动成千上万的 Goroutine 来完成不同的任务，而且不用担心资源被大量消耗。\n举个例子：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func task() { fmt.Println(\u0026#34;Hello Goroutine!\u0026#34;) } func main() { go task() time.Sleep(1 * time.Second) } 在这段代码里，我们用go task()轻松地启动了一个 Goroutine 去执行task函数。\n当然这样可能更直观：\npackage main import ( \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) func main() { go func() { fmt.Println(\u0026#34;Hello Goroutine!\u0026#34;) }() time.Sleep(1 * time.Second) } 高效的通道（Channel） #在并发编程中，不同的任务之间需要数据通信，Golang 提供了一种更为直观和易于理解的方式来处理并发，Goroutine 和 Channel 的组合使用。\nGoroutine \u0026amp; Channel 协作流程 #package main import \u0026#34;fmt\u0026#34; func main() { ch := make(chan int) // 无缓冲的通道 go func() { ch \u0026lt;- 1 }() num := \u0026lt;-ch fmt.Println(num) } 通过这个通道ch，我们成功地在两个不同的 Goroutine 之间传递了数据。\n优势：\n无锁通信：Channel 内部基于循环队列和互斥锁实现，但开发者无需感知 同步简化：无缓冲 Channel 天然实现\u0026quot;发送-接收\u0026quot;原子操作，替代 WaitGroup 流水线模式：多级 Channel 串联可构建生产者-消费者管道 错误处理机制 #Golang 的并发错误处理机制也更加简洁和有效。它能够帮助开发者更快速地定位和解决并发环境中可能出现的问题，减少了因并发导致的错误排查难度和时间成本。\nfunc worker(ch chan\u0026lt;- Result) { res, err := compute() if err != nil { ch \u0026lt;- Result{Err: err} // 错误通过Channel返回 return } ch \u0026lt;- Result{Data: res} } 优势：\n统一错误流：错误与结果同通道传递，避免并发场景下的异常丢失 defer资源回收：确保Goroutine退出时自动释放资源 优秀的内存管理和并发调度 #在编程语言中，内存管理和并发调度是影响程序性能和稳定性的关键因素。Golang 在这两个方面展现出了卓越的特性。\nGolang 内存管理机制（三级缓存架构） #Golang 拥有一套自动且高效的内存回收机制。这意味着开发者无需像在 Java 等语言中那样，时刻关注内存的分配与释放，避免了因手动管理内存而可能导致的内存泄漏和野指针等问题。 这种自动内存管理机制不仅减轻了开发者的负担，还提高了程序的可靠性和可维护性。\n层级 组件 对象大小 锁机制 功能特点 线程本地缓存 mcache \u0026lt; 32KB 无锁（P 独占） 每个 P（处理器）独立缓存小对象，分配速度极快，减少全局竞争。 中心缓存 mcentral 16B - 32KB 需加锁 全局共享，按大小分类管理 Span，为 mcache 提供后备资源。 全局堆 mheap ≥ 32KB 需加锁 管理大对象和操作系统内存申请，处理跨 Span 分配，碎片整理由 GC 完成。 优化设计：\n对象分级：67 种 Size Class（如 8B/16B/32B），减少内存碎片 逃逸分析：编译器自动判断对象分配在栈（局部变量）或堆（跨作用域），减少 GC 压力 对象池：sync.Pool 重用对象，避免高频分配 并发调度策略对比（GMP 模型） #Golang 的并发调度机制极具智能性。它能够根据系统的负载和各个 Goroutine 的状态，合理地分配 CPU 资源，确保每个 Goroutine 都能获得公平的执行机会。 与 Java 等语言的线程调度相比，Golang 的调度更加轻量和灵活，能够在高并发场景下实现更高效的资源利用，从而显著提升程序的整体性能和响应速度。\n策略 触发条件 抢占点 优势 工作窃取（Work Stealing） P 的本地队列为空 本地队列无任务时 负载均衡：P 从全局队列或其他 P 偷取 G，提升 CPU 利用率。 协作式调度 G 主动让出（如 runtime.Gosched()） 函数调用点 低开销：无强制中断，但可能因阻塞导致饥饿（Go 1.14 式）。 抢占式调度 G 运行超时（10ms） 异步安全点 公平性：强制切换长时间运行的 G，避免“饿死”（Go 1.14 式）。 调度组件：\nGoroutine（G）：轻量级协程（初始栈 2KB，可动态扩展） Machine（M）：OS 线程，绑定 P 执行 G Processor（P）：逻辑处理器，管理本地队列（最多存放 256 个 G） 通过三级内存缓存降低锁竞争，结合智能调度策略（窃取+抢占），Golang 在保证自动内存安全的同时，实现高并发场景下的低延迟与高吞吐。\n","date":"2024-12-22","permalink":"https://vespeng.github.io/posts/the-core-mechanism-of-go-concurrency/","section":"Posts","summary":"在如今的编程领域，一个程序能够同时处理多个任务的能力非常重要，而 Golang 在并发编程方面表现十分出色，具有很多独特的优势。","title":"Go 并发的核心机制解读"},{"content":"下载 Golang #官网链接：https://golang.google.cn/dl/\n配置环境 # 安装好之后须添加如下环境变量：\n变量名 变量值 说明 GOPATH E:\\gowork Go语言的工作目录，存放自己编写的 .go 文件、项目、包、编译的二进制文件等 GOROOT D:\\go Go的安装路径 在 Path 路径下，新增 %GOROOT%\\bin\nPS: GOPATH 的值，需要根据实际情况按需配置，默认路径为C盘用户目录下\n开启 go mod 及配置国内代理：\ngo env -w GO111MODULE=on go env -w GOPROXY=https://goproxy.cn,direct 配置 VSCode #安装 Go、vscode-go-syntax 两插件\n届时，VSCode 会弹出需要安装其他 go 扩展的提示，点击 Install All 即可\n如条件允许更推荐使用 GoLand 进行开发\n","date":"2024-03-15","permalink":"https://vespeng.github.io/posts/go-development-environment/","section":"Posts","summary":"","title":"Go 开发环境搭建：基于 Windows 操作系统"}]