上线前必须解决并发安全、短码唯一性和重定向状态码三个卡点:需用sync.rwmutex保护map;校验url合法性并统一返回302;短码生成须原子化,推荐自增id+base62。

直接用 Gin + 内存 map 能跑通,但上线前必须处理并发安全、短码唯一性和重定向状态码这三个实际卡点。
为什么不能直接用 map[string]string 存短链
看似简单,但裸用 map 在并发场景下会 panic:两个请求同时写入同一 key,或读写冲突,Go 运行时直接抛 fatal error: concurrent map writes。Gin 的 handler 是并发执行的,哪怕本地测试开两个浏览器标签页就可能触发。
实操建议:
- 用
sync.RWMutex包一层,读多写少时性能损耗极小 - 别把
map声明为全局变量,封装进结构体里更清晰,比如type Shortener struct { mu sync.RWMutex; store map[string]string } - 写操作(如生成新短码)必须
mu.Lock(),读操作(如跳转查询)用mu.RLock() - 不要在 handler 里直接
defer mu.Unlock()后续还访问 map —— 锁释放后 map 可能已被其他 goroutine 修改
POST /shorten 接口必须校验长 URL 格式
用户随便 POST 一个 {"url": "foo"},不校验就存进去,后续 GET /abc123 重定向时会触发 http.Redirect 报错 invalid URL escape 或静默失败。
实操建议:
- 用
url.ParseRequestURI(longURL)判断是否为合法可跳转的绝对 URL,拒绝javascript:、data:、无 scheme 的字符串 - 别只检查是否含
http—— 用户可能输https://开头但后面缺域名,ParseRequestURI才是权威校验 - 前端传参优先用 JSON body,而非表单;Gin 中用
c.ShouldBindJSON(&req)自动校验字段非空和类型,比c.PostForm更稳 - 返回错误时统一用
http.StatusBadRequest,别混用400和422,前端解析更确定
重定向必须用 http.StatusFound(302),不是 http.StatusMovedPermanently(301)
用 301 会导致浏览器和 CDN 强制缓存重定向结果。一旦原始 URL 改了,用户再访问短链,仍被 301 缓存跳转到旧地址,清缓存都救不回来。
实操建议:
- 所有跳转逻辑统一走
http.Redirect(c.Writer, c.Request, target, http.StatusFound) - 别手写
c.Header("Location", ...)+c.Status(...)—— 容易漏设 header 或状态码错位 - 如果真要支持“永久”跳转(极少场景),得加额外参数如
?permanent=1显式控制,不能默认 - 注意
http.StatusFound是临时重定向,搜索引擎不会传递权重,符合短链“可变目标”的本质
短码生成别用纯随机,优先选自增 ID + Base62
纯随机生成 6 位字符串,在 QPS 上千时碰撞概率陡增,查库+重试逻辑会让接口延迟毛刺明显,且无法预估总量。
实操建议:
- 用原子计数器
atomic.AddInt64(&nextID, 1)模拟自增 ID,避免数据库依赖 - Base62 编码函数必须用
uint64输入,别用int—— 32 位系统下int最大值才 21 亿,早晚会溢出 - 字符集严格用
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",共 62 个字符,不含+、/、=等需 URL 编码的符号 - 初期可跳过混淆(如异或掩码),靠
limit_req配置防遍历;量上来后再平滑切到id ^ 0x5DE4E6A2方案,不改存储格式
最易被忽略的是:短码生成和存储必须是原子操作。先生成 code、再查库、再插入,三步之间有时间窗口,高并发下必然重复写入。真正可靠的方案是——生成后立即写入带唯一索引的存储(如 SQLite 的 UNIQUE(short_code)),靠 DB 层兜底,应用层只做一次尝试 + 明确错误分支处理。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











