Go 接浏览器 Beacon API:页面关闭时上报请求、Content-Type 与失败兜底
页面刚切走,最后一次停留时长却总是少一截,问题往往不在 Go 接口,而在浏览器还没来得及把普通 fetch 发完。适合这种场景的是 Beacon API:前端在 visibilitychange 或页面离开前,把一小段数据交给浏览器异步发送;Go 服务端只需按一次普通 POST 接收并快速返回。
要点速览
navigator.sendBeacon()只负责小型异步 POST,不适合需要读取响应体的业务请求。- JSON Blob 会带来
Content-Type: application/json,Go 端不要用表单解析器代替 JSON 解码。 - 服务端应先限制请求体,再校验事件名和时间字段,成功后返回 204。
- Beacon 不可用、数据过大或必须控制请求方法时,再切到
fetch(..., {keepalive:true})。
页面关闭时,普通 fetch 为什么会丢最后一条事件
一个阅读页会在用户离开时上报 article_leave,字段包括文章编号、进入时间和停留秒数。最直观的写法是监听 beforeunload,然后调用普通 fetch。但卸载阶段的页面生命周期很短,浏览器可以中止未完成的异步任务;移动端切到后台、系统回收页面时,这个窗口还会更窄。
这里先不要把问题归因到 Go 超时。即使 /v1/events/leave 在本机 2 毫秒返回,页面也可能在请求发出前就被回收。更稳妥的触发点是 visibilitychange:文档进入 hidden 状态时,尽早把最后一小段数据交给浏览器。
Beacon API 的职责边界:交给浏览器发,不等业务响应
navigator.sendBeacon(url, data) 的返回值只表示数据是否被浏览器接受进发送队列,不代表 Go 接口已经返回 2xx。它适合离开页面统计、诊断事件和小型状态同步,不能拿来做“服务端返回优惠券编号后再更新页面”这类需要响应体的流程。
下面的页面代码只发送一次事件,并用布尔值防止 visibilitychange 重复触发:
let sent = false;
function reportLeave() {
if (sent) return;
sent = true;
const body = JSON.stringify({
event: "article_leave",
article_id: "go-beacon-101",
duration_sec: 37,
occurred_at: new Date().toISOString()
});
const blob = new Blob([body], { type: "application/json" });
const queued = navigator.sendBeacon("/v1/events/leave", blob);
if (!queued) {
sent = false;
fallbackReport(body);
}
}
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") reportLeave();
});
sendBeacon 使用 POST,调用方不能像普通请求那样读取响应体,也不应把它当成可靠消息队列。数据要短,事件也要允许重复或迟到;这些约束会直接影响 Go 端的校验和去重设计。
![]()
Go 接收 JSON Beacon:Content-Type 和 204 要先处理对
前端传入的是 JSON Blob,Go 端应从 r.Body 解码,而不是调用 r.ParseForm() 后读取表单字段。接口还应拒绝过大的请求,避免离开页面接口被当成任意文件入口。
package main
import (
"encoding/json"
"io"
"net/http"
"time"
)
type leaveEvent struct {
Event string `json:"event"`
ArticleID string `json:"article_id"`
Duration int `json:"duration_sec"`
OccurredAt time.Time `json:"occurred_at"`
}
func leaveHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if r.ContentLength > 64*1024 {
http.Error(w, "payload too large", http.StatusRequestEntityTooLarge)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 64*1024)
defer r.Body.Close()
var event leaveEvent
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&event); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
if event.Event != "article_leave" || event.ArticleID == "" || event.Duration
真实项目里可以把最后一段替换成消息队列或批量写入。示例用 io.Discard 只是让代码能独立编译;它不会持久化事件,验收时要把“收到请求”和“写入存储”分成两个指标。
返回 204 的意义是告诉客户端“请求处理完成,但没有响应正文”。不要在这个接口里拼一段 JSON 结果,Beacon 调用方本来就不会读取它。
![]()
一次真实验收:看请求头、状态码和服务端日志
先把 Go 服务跑在 HTTPS 页面可访问的地址上,再在浏览器开发者工具的 Network 面板中触发页面隐藏。重点看三件事:请求方法是否为 POST,Content-Type 是否为 application/json,响应状态是否为 204。
POST /v1/events/leave HTTP/1.1
Content-Type: application/json
Content-Length: 112
{"event":"article_leave","article_id":"go-beacon-101","duration_sec":37}
HTTP/1.1 204 No Content
如果 Network 里没有请求,先查页面是否在 HTTP 环境、浏览器是否支持 Beacon,以及 sendBeacon 返回值是不是 false。如果请求到了但 Go 返回 400,优先检查 JSON 字段、时间格式和请求体限制,不要先放宽校验。
Beacon 不够用时,keepalive fetch 怎么做兜底
需要自定义请求头、使用其他方法、读取响应,或者 Beacon 队列接收失败时,可以尝试 fetch 的 keepalive: true。它同样受请求体大小和浏览器实现限制,不能因为加了这个开关就把几 MB 的日志塞进页面关闭事件。
function fallbackReport(body) {
void fetch("/v1/events/leave", {
method: "POST",
headers: { "Content-Type": "application/json" },
body,
keepalive: true,
credentials: "same-origin"
}).catch(() => {
// 离开页面阶段不要弹窗;下次进入页面时可从本地短暂重试。
});
}
如果事件必须一条不丢,Beacon 也不是答案。应在页面仍然可交互时写入本地待发送记录,由正常请求、重试和服务端幂等键完成可靠投递;离开页面只做最后一次“尽力发送”。
常见问题
Beacon 能发送 GET 请求吗?
不能。sendBeacon 的接口语义是异步 POST;必须改用 GET 或读取响应时,应重新设计请求,不要把 Beacon 当成通用 HTTP 客户端。
为什么 Go 端读取不到表单里的字段?
JSON Blob 不是表单编码。读取 r.Body 并用 encoding/json 解码,同时检查 Content-Type 和请求体上限。
sendBeacon 返回 true 就代表数据已经入库了吗?
不是。true 只表示浏览器接受了发送请求;服务端仍可能返回 4xx、5xx 或在写入存储前失败,所以要用服务端接收计数和业务落库计数分别监控。
Beacon 适合上传完整操作日志吗?
不适合。它面向小型、异步、无需读取响应的请求。较大的日志应在页面活跃时分批发送,并给每条事件加幂等标识。
把页面离开上报做成一个小而可靠的接口
这套组合的关键不是把页面卸载事件“强行拦住”,而是把职责拆开:浏览器负责尽早排队,Go 负责快速校验和接收,后续存储负责幂等与重试。用 Network 面板确认 POST、JSON 和 204,再用服务端指标确认接收与落库,基本就能定位问题是在浏览器队列、网络,还是业务存储。
Go 1.23 range-over-func 怎么接入切片流水线:iter.Seq、break 与惰性遍历边界
- 上一篇
- Go 1.23 range-over-func 怎么接入切片流水线:iter.Seq、break 与惰性遍历边界
- 下一篇
- Go 1.26 errors.AsType 怎么替代 errors.As:泛型类型断言、旧代码兼容与测试边界
-
- Golang · Go教程 | 6小时前 |
- Go 1.25 的 synctest 怎么测定时器:把异步重试从真实等待改成可控推进
- 405浏览 收藏
-
- Golang · Go教程 | 8小时前 | 并发 · go · pprof · 性能排查 · pprof Go 1.26 goroutineleak goroutine 泄漏
- goroutineleak 真的能找到 Go 协程泄漏吗:pprof 可达性判断与回滚边界
- 257浏览 收藏
-
- Golang · Go教程 | 9小时前 | golang · https · TLS · Go教程 · 生产运维 · 证书轮换 · atomic.Value Go HTTPS证书热切换 GetCertificate tls.Certificate 证书轮换
- Go HTTPS 证书热切换怎么做:atomic.Value、GetCertificate 与失败回退
- 267浏览 收藏
-
- Golang · Go教程 | 11小时前 |
- Go 1.23 range-over-func 怎么接入切片流水线:iter.Seq、break 与惰性遍历边界
- 226浏览 收藏
-
- Golang · Go教程 | 1天前 | [] · []
- Go 服务出现 too many open files 怎么查:/proc/fd、ulimit 与连接泄漏
- 119浏览 收藏
-
- Golang · Go教程 | 1天前 | [] · []
- Go 批量导出如何避免结果归并拖垮内存:分段文件、排序归并与断点续写
- 487浏览 收藏
-
- Golang · Go教程 | 1天前 |
- Go bytes.Buffer.Reset 为什么不降内存:复用容量、Grow 与回收边界
- 333浏览 收藏
-
- Golang · Go教程 | 1天前 | [] · []
- Go 依赖被替换怎么查:GOSUMDB、GOPRIVATE 与私有模块边界
- 413浏览 收藏
-
- Golang · Go教程 | 1天前 | [] · []
- Go 依赖被替换怎么查:GOSUMDB、GOPRIVATE 与私有模块边界
- 351浏览 收藏
-
- Golang · Go教程 | 1天前 |
- Go 1.24 os.Root 如何限制文件系统越界:路径校验、符号链接与兼容边界
- 437浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ljg-skills
- ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
- 4807次使用
-
- MELO音乐
- MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
- 4399次使用
-
- UniScribe
- UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
- 4347次使用
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 4581次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 4530次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- Golang实现HTTP编程请求和响应
- 2022-12-28 101浏览
-
- golangNewRequest/gorequest实现http请求的示例代码
- 2023-01-24 343浏览
-
- 一文详解Golang中net/http包的实现原理
- 2022-12-29 419浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览

