<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
<title><![CDATA[牧野的 Go 后端笔记]]></title> 
<atom:link href="https://www.rxecs.com/rss.php" rel="self" type="application/rss+xml" />
<description><![CDATA[扬州竹安科技技术部的 Go 工程师个人技术博客，记录 Gin、GoFrame、MySQL、Redis、Linux 部署与工程化实践。]]></description>
<link>https://www.rxecs.com/</link>
<language>zh-cn</language>

<item>
    <title>关于这个博客：记录 Go 后端的真实取舍</title>
    <link>https://www.rxecs.com/1.html</link>
    <description><![CDATA[<p>这个博客主要记录我在 Go 后端开发中的日常判断：框架怎么选、接口怎么拆、事务边界放在哪里、日志字段留哪些，以及上线后如何定位问题。</p><p>我会尽量写小问题，不写大而空的架构口号。比如 Gin 中间件顺序为什么会影响日志完整性，GoFrame 的配置为什么要从启动阶段就固定，Redis 缓存空值为什么比简单删除更可靠。</p><p>技术栈会围绕 Go、Gin、GoFrame、MySQL、Redis 和 Linux 部署展开。文章不是教程合集，更像工作笔记：每一篇都回答一个实际问题，并说明我为什么这么处理。</p><p>如果一段代码只是能跑，我不会把它当成结论；只有它在可维护性、排障和团队协作上站得住，才会留下来。</p>]]></description>
    <pubDate>Mon, 25 May 2026 21:30:00 +0800</pubDate>
    <dc:creator>牧野</dc:creator>
    <guid>https://www.rxecs.com/1.html</guid>
</item>
<item>
    <title>Go 项目目录：cmd、internal、pkg 要不要全用</title>
    <link>https://www.rxecs.com/2.html</link>
    <description><![CDATA[<p>很多 Go 项目一开始就照着大仓库建 <code>cmd</code>、<code>internal</code>、<code>pkg</code>，最后目录很整齐，代码却不知道该放哪。我的经验是：目录结构应该服务边界，而不是服务仪式感。</p><p><code>cmd</code> 适合放入口，入口只做配置加载、依赖组装和启动服务；<code>internal</code> 适合放业务不可被外部复用的代码；<code>pkg</code> 只有在确实要给别的项目 import 时才值得出现。</p><pre><code>cmd/api/main.go
internal/order/service.go
internal/order/repository.go
internal/platform/mysql.go</code></pre><p>如果一个函数只被当前业务调用，先放在 <code>internal</code>。过早放进 <code>pkg</code> 会暗示它是稳定 API，后面每次改签名都要考虑外部依赖。</p><p>小项目可以先保持三层以内：入口、业务、基础设施。等边界真的变清楚，再拆包，成本更低。</p>]]></description>
    <pubDate>Sun, 24 May 2026 20:40:00 +0800</pubDate>
    <dc:creator>牧野</dc:creator>
    <guid>https://www.rxecs.com/2.html</guid>
</item>
<item>
    <title>context 不只是取消信号：超时和值怎么放</title>
    <link>https://www.rxecs.com/3.html</link>
    <description><![CDATA[<p><code>context.Context</code> 最常见的误用，是把它当成全局参数袋。请求 ID、租户 ID 可以放进去，但数据库连接、业务配置、可选参数不应该塞进去。</p><p>我通常遵守两个规则：第一，跨 API 边界且和请求生命周期绑定的值可以放；第二，业务核心参数必须显式出现在函数签名里。</p><pre><code>ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()

order, err := svc.Create(ctx, userID, req.Items)</code></pre><p>超时要在入口附近设置，因为只有入口知道 SLA。仓储层可以尊重 ctx，但不应该偷偷创建更长的超时。</p><p>这样做的好处是排障简单：请求取消、数据库超时、下游慢查询都会沿着同一个 ctx 传播，不会出现后台 goroutine 继续跑的隐患。</p>]]></description>
    <pubDate>Sat, 23 May 2026 19:20:00 +0800</pubDate>
    <dc:creator>牧野</dc:creator>
    <guid>https://www.rxecs.com/3.html</guid>
</item>
<item>
    <title>errgroup 控制并发：别让 goroutine 泄漏</title>
    <link>https://www.rxecs.com/4.html</link>
    <description><![CDATA[<p>直接起 goroutine 很容易写出“看起来并发，实际上不可控”的代码。只要其中一个任务失败，其他任务是否继续、错误如何返回、ctx 是否取消，都需要明确。</p><p><code>errgroup.WithContext</code> 的价值在于把错误和取消绑定起来。一个任务返回错误后，派生 ctx 会取消，其他任务可以尽快退出。</p><pre><code>g, ctx := errgroup.WithContext(ctx)
for _, id := range ids {
    id := id
    g.Go(func() error {
        return syncOne(ctx, id)
    })
}
if err := g.Wait(); err != nil {
    return err
}</code></pre><p>如果任务数量可能很大，要加并发限制。Go 新版本的 errgroup 支持 <code>SetLimit</code>，比手写信号量更清楚。</p><p>我的判断标准是：只要 goroutine 的生命周期超过当前函数，就必须能说清楚谁取消它、谁等待它、错误去哪了。</p>]]></description>
    <pubDate>Fri, 22 May 2026 18:45:00 +0800</pubDate>
    <dc:creator>牧野</dc:creator>
    <guid>https://www.rxecs.com/4.html</guid>
</item>
<item>
    <title>defer 的成本和可读性：哪些热路径要注意</title>
    <link>https://www.rxecs.com/5.html</link>
    <description><![CDATA[<p><code>defer</code> 的首要价值是让释放动作靠近申请动作。文件关闭、锁释放、事务回滚，用 defer 往往比手动分支更不容易漏。</p><p>但在极高频循环里，defer 可能带来额外开销，也可能把释放时机拖到函数结束。比如批量处理每一行数据时，把 defer 放在循环内部就需要小心。</p><pre><code>for rows.Next() {
    if err := handleRow(); err != nil {
        return err
    }
}
return rows.Close()</code></pre><p>我的默认策略是：普通业务代码优先可读性，热路径在压测或 pprof 证明有问题后再改。不要凭印象优化，也不要把所有 defer 都手动展开。</p><p>工程里真正危险的不是 defer 慢，而是资源释放路径不一致。性能问题可以量化，泄漏问题往往上线后才暴露。</p>]]></description>
    <pubDate>Thu, 21 May 2026 22:10:00 +0800</pubDate>
    <dc:creator>牧野</dc:creator>
    <guid>https://www.rxecs.com/5.html</guid>
</item>
<item>
    <title>Gin 中间件顺序：日志、恢复、鉴权、限流怎么排</title>
    <link>https://www.rxecs.com/6.html</link>
    <description><![CDATA[<p>Gin 中间件不是随便挂上去就完事。顺序会决定日志能不能记录 panic、限流是否覆盖未登录请求、鉴权失败是否仍有 request id。</p><p>我常用的顺序是：request id、访问日志、recover、CORS、限流、鉴权、业务路由。最外层先建立追踪信息，recover 要在业务之前，日志要能拿到最终状态码和耗时。</p><pre><code>r.Use(RequestID())
r.Use(AccessLog())
r.Use(gin.Recovery())
r.Use(RateLimit())
api := r.Group("/api", Auth())</code></pre><p>限流放鉴权前还是后，要看目标。如果要保护登录接口和匿名流量，放前面；如果按用户额度限流，放鉴权后。</p><p>不要把业务判断塞进全局中间件。中间件越靠外，影响面越大，越应该只处理横切能力。</p>]]></description>
    <pubDate>Wed, 20 May 2026 21:00:00 +0800</pubDate>
    <dc:creator>牧野</dc:creator>
    <guid>https://www.rxecs.com/6.html</guid>
</item>
<item>
    <title>Gin 参数校验：把 binding 错误变成可读响应</title>
    <link>https://www.rxecs.com/7.html</link>
    <description><![CDATA[<p>Gin 的 binding 能快速完成参数绑定和校验，但默认错误信息通常不适合直接返回给前端。工程里需要把字段名、规则和业务提示做一层翻译。</p><pre><code>type CreateUserReq struct {
    Name string `json:"name" binding:"required,min=2"`
    Age  int    `json:"age" binding:"gte=1,lte=120"`
}</code></pre><p>我的做法是在入口层统一处理校验错误，把 <code>validator.ValidationErrors</code> 转成固定结构，例如 <code>{field, message}</code>。这样前端可以稳定展示，也方便测试。</p><p>校验只负责“格式正确”，不要负责“业务存在”。例如手机号格式可以在 binding 做，手机号是否已注册应该交给 service。</p><p>分清格式校验和业务校验，接口层会薄很多，错误响应也更一致。</p>]]></description>
    <pubDate>Tue, 19 May 2026 20:15:00 +0800</pubDate>
    <dc:creator>牧野</dc:creator>
    <guid>https://www.rxecs.com/7.html</guid>
</item>
<item>
    <title>Gin 路由分组：版本号和后台接口怎么拆</title>
    <link>https://www.rxecs.com/8.html</link>
    <description><![CDATA[<p>路由分组的核心不是少写几个前缀，而是表达访问边界。公开接口、管理后台、内部回调，应该有不同的中间件和不同的稳定性承诺。</p><pre><code>api := r.Group("/api/v1")
admin := r.Group("/admin/api", AdminAuth())
callback := r.Group("/callbacks", VerifySignature())</code></pre><p>版本号建议放在对外 API 上，不一定放在后台管理接口上。后台接口通常和前端一起发布，兼容压力没那么大；开放 API 则要给调用方迁移时间。</p><p>同一个 handler 不要同时服务前台和后台。看起来复用，实际上权限、字段、审计要求都不一样。</p><p>当路由能一眼看出调用方是谁，后续加限流、审计和灰度都会轻松很多。</p>]]></description>
    <pubDate>Mon, 18 May 2026 17:50:00 +0800</pubDate>
    <dc:creator>牧野</dc:creator>
    <guid>https://www.rxecs.com/8.html</guid>
</item>
<item>
    <title>Gin 上传文件的几个坑</title>
    <link>https://www.rxecs.com/9.html</link>
    <description><![CDATA[<p>文件上传接口看起来简单，实际风险集中在三个地方：文件大小、文件名、存储路径。只用 <code>SaveUploadedFile</code> 很容易把用户输入带进文件系统。</p><p>入口层要限制 body 大小，业务层要重新生成文件名，存储层只接受已经确认过的对象 key。原始文件名可以当展示字段保存，但不应该参与真实路径。</p><pre><code>r.MaxMultipartMemory = 8 &lt;&lt; 20
file, err := c.FormFile("file")
key := uuid.NewString() + filepath.Ext(file.Filename)</code></pre><p>还要校验 content type 和扩展名，但不要只信任浏览器传来的 header。重要文件最好做魔数识别，图片类文件可以解码一次确认格式。</p><p>上传接口的目标不是“能收到文件”，而是保证收到的文件不会污染路径、压垮内存或绕过业务规则。</p>]]></description>
    <pubDate>Sun, 17 May 2026 16:30:00 +0800</pubDate>
    <dc:creator>牧野</dc:creator>
    <guid>https://www.rxecs.com/9.html</guid>
</item>
<item>
    <title>Gin 统一响应不等于到处 panic</title>
    <link>https://www.rxecs.com/10.html</link>
    <description><![CDATA[<p>统一响应结构是好事，但如果实现方式是业务里到处 panic，再由 recover 中间件兜底，就会把正常错误和程序缺陷混在一起。</p><p>我更倾向于 handler 显式处理业务错误，panic 只留给真正不可恢复的问题。统一响应可以通过小函数完成，不需要牺牲控制流。</p><pre><code>if err != nil {
    response.Error(c, err)
    return
}
response.OK(c, data)</code></pre><p>错误类型可以带 code、message 和 cause。对外返回稳定文案，对内日志保留原始错误，这样既不泄露细节，也不影响排障。</p><p>统一响应的价值是降低接口差异，不是隐藏所有错误路径。清楚地 return，通常比“抛出去让外层猜”更适合业务系统。</p>]]></description>
    <pubDate>Sat, 16 May 2026 19:00:00 +0800</pubDate>
    <dc:creator>牧野</dc:creator>
    <guid>https://www.rxecs.com/10.html</guid>
</item>
</channel>
</rss>