第一章:Golang文件的上传和下载
Go语言标准库提供了强大而简洁的HTTP处理能力,结合net/http与mime/multipart包,可高效实现文件上传与下载功能,无需依赖第三方框架。
文件上传实现
服务端需解析multipart表单数据。以下是一个基础上传处理器示例:
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 解析多部分表单,最大内存缓存32MB
err := r.ParseMultipartForm(32 << 20)
if err != nil {
http.Error(w, "Unable to parse form", http.StatusBadRequest)
return
}
// 获取名为"file"的文件字段
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "No file received", http.StatusBadRequest)
return
}
defer file.Close()
// 保存到本地磁盘(生产环境建议使用对象存储)
dst, err := os.Create("./uploads/" + header.Filename)
if err != nil {
http.Error(w, "Failed to create file", http.StatusInternalServerError)
return
}
defer dst.Close()
// 流式拷贝,避免内存溢出
if _, err = io.Copy(dst, file); err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("File uploaded successfully"))
}
文件下载实现
通过设置响应头触发浏览器下载行为:
func downloadHandler(w http.ResponseWriter, r *http.Request) {
filename := r.URL.Query().Get("name")
if filename == "" {
http.Error(w, "Missing 'name' parameter", http.StatusBadRequest)
return
}
filePath := "./uploads/" + filename
f, err := os.Open(filePath)
if err != nil {
http.Error(w, "File not found", http.StatusNotFound)
return
}
defer f.Close()
// 设置Content-Disposition强制下载
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, url.PathEscape(filename)))
w.Header().Set("Content-Length", strconv.FormatInt(getFileSize(filePath), 10))
io.Copy(w, f) // 直接流式传输,节省内存
}
关键注意事项
- 上传路径需预先创建
./uploads/目录,否则os.Create会失败 - 生产部署中应校验文件类型、大小及扩展名,防范恶意上传
- 下载时务必对
filename参数进行URL转义,防止HTTP头注入 - 建议配合
context.WithTimeout为上传/下载操作添加超时控制
| 场景 | 推荐做法 |
|---|---|
| 大文件上传 | 使用io.Copy流式处理,禁用全部读入内存 |
| 并发上传 | 限制ParseMultipartForm内存上限,避免OOM |
| 安全防护 | 白名单校验扩展名(如.pdf, .jpg) |
第二章:HTTP文件上传机制深度解析与实践
2.1 multipart/form-data协议原理与Go标准库实现剖析
multipart/form-data 是 HTTP 中用于上传文件及混合数据的标准编码格式,其核心在于边界分隔(boundary)和 MIME 部分封装。
协议结构要点
- 每个字段以
--{boundary}开始,以\r\n分隔; - 每部分含
Content-Disposition头,指定name和可选filename; - 文件内容以原始二进制传输,不进行 URL 编码。
Go 标准库关键组件
// http.Request.ParseMultipartForm() 内部调用流程
func (r *Request) ParseMultipartForm(maxMemory int64) error {
r.multipart = NewMultiReader(r.Body, r.Header.Get("Content-Type"))
// 解析 boundary 并流式分割 parts
return r.multipart.Parse()
}
maxMemory 控制内存缓存阈值:小文件驻留内存,超限自动落盘至临时文件(由 os.TempDir() 管理)。
Boundary 解析机制
| 字段 | 说明 |
|---|---|
boundary |
从 Content-Type: multipart/form-data; boundary=abc 提取 |
maxMemory |
默认 32MB,影响 multipart.Reader 的缓冲策略 |
graph TD
A[HTTP Body] --> B{Find Boundary}
B --> C[Parse Header Block]
C --> D[Extract name/filename]
D --> E[Stream Part Body]
E --> F[Buffer or TempFile]
Go 的 multipart.Reader 采用惰性解析:仅在 NextPart() 调用时定位下一个分隔符并读取头部,兼顾性能与内存可控性。
2.2 文件流式上传与内存/磁盘缓冲策略对比实战
流式上传核心逻辑
使用 ReadableStream 分块读取大文件,避免一次性加载至内存:
const uploadChunk = async (chunk, offset) => {
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('offset', offset);
formData.append('filename', 'report.zip');
return fetch('/api/upload/chunk', { method: 'POST', body: formData });
};
该函数将二进制分块(如 4MB)与偏移量封装上传,服务端按序合并;offset 确保断点续传一致性,chunk 为 Uint8Array 或 Blob,规避 V8 内存限制。
缓冲策略性能对比
| 策略 | 内存占用 | 速度 | 适用场景 |
|---|---|---|---|
| 纯内存缓冲 | 高 | 快 | |
| 磁盘临时缓存 | 低 | 中 | GB 级视频/备份文件 |
| 混合流式 | 极低 | 可调 | 超大文件 + 断点续传 |
磁盘缓冲流程示意
graph TD
A[客户端分块] --> B{块大小 ≤ 8MB?}
B -->|是| C[内存暂存]
B -->|否| D[写入 IndexedDB]
C --> E[批量提交]
D --> E
2.3 大文件分片上传与断点续传的Go服务端设计
核心设计原则
- 基于唯一
upload_id关联分片元数据与存储状态 - 所有分片操作幂等,支持重复提交与乱序接收
- 元数据持久化至 Redis(高性能)+ MySQL(强一致性归档)
分片元数据结构
| 字段 | 类型 | 说明 |
|---|---|---|
| upload_id | string | 客户端生成的 UUID,全局唯一 |
| file_hash | string | 文件整体 SHA256,用于完整性校验 |
| total_chunks | int | 总分片数,由客户端预计算并首次上报 |
| uploaded_chunks | set[int] | 已成功接收的分片索引集合 |
上传流程(mermaid)
graph TD
A[客户端发起初始化请求] --> B[服务端生成 upload_id 并返回]
B --> C[客户端并发上传分片<br>携带 upload_id + chunk_index + data]
C --> D[服务端校验签名/范围/去重]
D --> E[写入对象存储 + 更新 Redis 元数据]
E --> F{是否全部分片就绪?}
F -->|是| G[触发合并任务:按序拼接 + 校验 file_hash]
F -->|否| C
合并逻辑示例(Go)
func mergeChunks(ctx context.Context, uploadID string) error {
meta, err := redis.GetUploadMeta(ctx, uploadID) // 获取元数据
if err != nil || len(meta.UploadedChunks) != meta.TotalChunks {
return errors.New("incomplete upload")
}
// 按 chunk_index 升序读取分片并流式写入最终文件
for i := 0; i < meta.TotalChunks; i++ {
chunkData, _ := oss.GetObject(ctx, fmt.Sprintf("%s_%d", uploadID, i))
_, _ = finalWriter.Write(chunkData)
}
return oss.PutObject(ctx, meta.FileHash, finalWriter.Bytes())
}
该函数确保原子性合并:仅当所有分片存在时才执行拼接,并通过 file_hash 反向验证整体完整性;uploadID 作为命名空间隔离并发上传,避免冲突。
2.4 安全校验:MIME类型验证、文件头检测与恶意内容拦截
为何单一校验不可靠?
仅依赖客户端 Content-Type 易被伪造;仅检查扩展名无法识别伪装文件(如 .jpg 实际为 PHP 脚本)。
三重校验协同机制
- MIME 类型验证:服务端解析
multipart/form-data中的Content-Type,并与白名单比对 - 文件头检测:读取前 4–8 字节(Magic Bytes),匹配真实格式指纹
- 恶意内容拦截:扫描脚本标签、
eval(、<script>等高危特征
文件头 Magic Bytes 示例
| 格式 | 前 4 字节(十六进制) | 说明 |
|---|---|---|
| PNG | 89 50 4E 47 |
固定签名,不可伪造 |
| JPEG | FF D8 FF E0 或 FF D8 FF DB |
支持多种 SOI 标识 |
25 50 44 46 |
%PDF ASCII 编码 |
def validate_file_header(file_stream):
file_stream.seek(0)
header = file_stream.read(4) # 读取前4字节
magic_map = {
b'\x89PNG': 'image/png',
b'\xff\xd8\xff': 'image/jpeg', # JPEG 可能含第4字节变体
b'%PDF': 'application/pdf'
}
for magic, mime in magic_map.items():
if header.startswith(magic): # 兼容 JPEG 多种起始模式
return mime
raise ValueError("Invalid file header")
逻辑分析:
seek(0)确保从流首开始读取;startswith()支持 JPEG 的多种合法头部变体(如FF D8 FF E0和FF D8 FF DB);返回 MIME 类型供后续白名单校验,避免仅依赖扩展名或 HTTP 头。
校验流程图
graph TD
A[接收上传文件] --> B{MIME 类型白名单校验}
B -->|通过| C[读取文件头 Magic Bytes]
B -->|拒绝| D[中断上传]
C -->|匹配| E[扫描恶意字符串]
C -->|不匹配| D
E -->|无威胁| F[保存文件]
E -->|发现 eval 或 <script>| D
2.5 上传进度跟踪与客户端实时反馈接口实现
核心设计思路
采用“服务端事件流(SSE)+ 分块上传元数据同步”双机制,避免轮询开销,保障低延迟反馈。
进度状态管理
服务端为每个上传任务维护轻量级状态对象:
// 示例:Redis中存储的进度哈希结构
{
"upload:abc123": {
"totalSize": 10485760, // 总字节数(10MB)
"uploaded": 3284992, // 已上传字节数
"chunks": [true, true, false, true], // 已接收分块标记
"updatedAt": "2024-06-15T10:22:31Z"
}
}
该结构支持原子性 HINCRBY 更新已上传字节数,并通过 HGETALL 快速获取完整状态;chunks 数组便于校验完整性,防止重复或丢失分块。
实时推送流程
graph TD
A[客户端发起分块上传] --> B[服务端更新Redis进度]
B --> C[POST /progress/notify?taskId=abc123]
C --> D[SSE EventSource监听/stream?id=abc123]
D --> E[浏览器接收text/event-stream]
接口响应规范
| 字段 | 类型 | 说明 |
|---|---|---|
progress |
number | 0–100 的整数百分比 |
speed |
number | 当前瞬时速率(B/s) |
etaSeconds |
number | 预估剩余秒数(-1 表示不可预估) |
第三章:基础文件下载流程与Content-Disposition规范落地
3.1 HTTP响应头语义解析:Content-Type、Content-Length与Cache-Control协同策略
HTTP响应头三者构成服务端内容交付的语义铁三角:Content-Type定义数据本质,Content-Length声明字节边界,Cache-Control约束重用生命周期。
协同失效场景示例
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 42
Cache-Control: public, max-age=3600
Content-Type中charset=utf-8明确解码方式,避免 JSON 解析乱码Content-Length: 42精确匹配实际字节(含换行与空格),防止分块传输干扰流式解析max-age=3600与public组合允许 CDN 缓存,但若Content-Type为text/html且含用户敏感信息,则需改用private
常见冲突矩阵
| Content-Type | Cache-Control | 风险点 |
|---|---|---|
application/json |
public, max-age=86400 |
静态配置缓存过期 |
text/html |
no-cache |
强制 revalidation |
image/png |
immutable |
配合 ETag 实现零往返 |
graph TD
A[客户端请求] --> B{服务端生成响应}
B --> C[计算真实body长度]
B --> D[推导MIME类型]
B --> E[依据资源时效性设定Cache策略]
C & D & E --> F[三头协同写入响应]
3.2 Content-Disposition标准语法与浏览器兼容性矩阵分析
Content-Disposition 响应头用于指示资源是内联展示还是作为附件下载,并可指定默认文件名。
标准语法结构
Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="report.pdf"
Content-Disposition: attachment; filename*=UTF-8''%E6%8A%A5%E5%91%8A.pdf
inline:建议浏览器内联渲染(如 PDF 插件);attachment:强制触发下载;filename:ASCII 文件名,不支持 Unicode;filename*:RFC 5987 编码格式,支持国际化字符(如 UTF-8 + percent-encoding)。
浏览器兼容性关键差异
| 浏览器 | 支持 filename |
支持 filename* |
备注 |
|---|---|---|---|
| Chrome ≥50 | ✅ | ✅ | 优先解析 filename* |
| Firefox ≥54 | ✅ | ✅ | 严格遵循 RFC 5987 |
| Safari 15+ | ✅ | ⚠️(部分编码失效) | 对 filename* 解析不稳定 |
兼容性策略流程
graph TD
A[服务端生成响应头] --> B{是否含中文/特殊字符?}
B -->|是| C[使用 filename* + UTF-8 编码]
B -->|否| D[回退 filename ASCII]
C --> E[Chrome/Firefox 正常]
C --> F[Safari 可能截断]
F --> G[添加冗余 filename 字段兜底]
3.3 Go net/http中设置响应头的典型陷阱与最佳实践
响应头设置时机决定成败
Header() 方法返回 http.Header,但必须在 WriteHeader() 或首次 Write() 调用前设置,否则被忽略:
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") // ✅ 正确:写入前设置
w.Header().Set("X-Cache", "MISS")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]bool{"ok": true})
}
Header()返回的是响应头映射的引用,但底层responseWriter在首次写入后进入“已提交”状态,后续Set()调用静默失效。
常见陷阱对比
| 陷阱类型 | 表现 | 后果 |
|---|---|---|
| 写入后设头 | w.Write(...); w.Header().Set(...) |
头部丢失,无报错 |
Set vs Add |
重复 Set("Cache-Control", "no-cache") |
覆盖而非追加 |
| 非法字符 | Set("Content-Disposition", "attachment; filename=中文.pdf") |
HTTP/1.1 协议违规 |
安全响应头推荐写法
使用 Add() 避免覆盖,配合标准安全头:
func secureHeaders(h http.Header) {
h.Add("X-Content-Type-Options", "nosniff")
h.Add("X-Frame-Options", "DENY")
h.Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
}
Add()允许同一键多值(如多个Vary),而Set()总是重置;Strict-Transport-Security必须用Set确保唯一性。
第四章:多语言文件名编码终极解法:UTF-8+ISO-8859-1双轨方案
4.1 RFC 5987与RFC 6266对国际化文件名的标准化演进
HTTP响应头 Content-Disposition 原生仅支持ASCII文件名,导致中文、日文等非ASCII文件名被截断或乱码。
从RFC 5987到RFC 6266的演进路径
- RFC 5987(2010)首次定义参数编码:
filename*=UTF-8''%E4%B8%AD%E6%96%87.pdf - RFC 6266(2011)正式废弃旧式
filename=,明确要求优先使用filename*,并规范编码格式与fallback机制
关键语法对比
| 字段 | RFC 5987 示例 | RFC 6266 要求 |
|---|---|---|
| 主参数 | filename="ascii.pdf" |
保留兼容但不推荐 |
| 国际化参数 | filename*=UTF-8''%E6%96%87%E4%BB%B6.pdf |
必须包含语言子标签(如zh-CN) |
Content-Disposition: attachment;
filename="report.pdf";
filename*=UTF-8'zh-CN'%E6%8A%A5%E5%91%8A.pdf
此示例中:
UTF-8为字符集,zh-CN为语言标签(RFC 6266强制要求),%E6%8A%A5%E5%91%8A为URL编码的“报告”。浏览器依此解析并显示本地化文件名,同时向旧客户端提供ASCII fallback。
graph TD
A[客户端发起下载] --> B{是否支持filename*?}
B -->|是| C[解码UTF-8+语言标签→原生文件名]
B -->|否| D[回退至filename=ASCII值]
4.2 UTF-8编码fallback机制在Chrome/Firefox/Safari/Edge中的行为差异实测
当HTML文档未声明<meta charset>且HTTP Content-Type缺失字符集时,浏览器需依据字节序列触发UTF-8 fallback策略。
不同引擎的探测逻辑
- Chrome(Blink):对前1024字节执行UTF-8合法性扫描,遇首个非法序列(如
0xC0 0x00)即回退至系统默认编码(如Windows-1252); - Firefox(Gecko):采用“UTF-8宽容解析”——允许单字节
0xC0–0xC1和0xF5–0xFF作为起始字节,仅在解码失败后切换; - Safari(WebKit):严格校验首3个UTF-8序列,若全部合法则锁定UTF-8,否则立即fallback至
ISO-8859-1; - Edge(Chromium内核):行为与Chrome完全一致。
实测响应对比
| 浏览器 | 非法UTF-8序列 0xC0 0x00 出现位置 |
fallback编码 |
|---|---|---|
| Chrome | 第200字节 | windows-1252 |
| Firefox | 第50字节 | ISO-8859-1 |
| Safari | 第10字节 | ISO-8859-1 |
| Edge | 第200字节 | windows-1252 |
<!-- 测试页片段:含非法UTF-8双字节 -->
<!DOCTYPE html>
<html><body>正常文本\xC0\x00乱码后内容</body></html>
该HTML中\xC0\x00是非法UTF-8起始(0xC0要求后跟续字节,但0x00非有效续字节),触发各引擎fallback判定点差异。参数0xC0为overlong编码禁用字节,0x00为非法续字节,共同构成强fallback信号。
graph TD A[接收HTTP响应] –> B{Content-Type含charset?} B — 是 –> C[使用指定编码] B — 否 –> D[扫描HTML前1024B meta/charset] D — 找到 –> C D — 未找到 –> E[执行UTF-8字节合法性探测] E –> F[Chrome/Edge: 首非法处fallback] E –> G[Firefox: 宽容解析失败后fallback] E –> H[Safari: 前3序列全合法才保留UTF-8]
4.3 ISO-8859-1安全转码算法实现与Go语言字节级编码转换实践
ISO-8859-1 是单字节编码,其 0x00–0xFF 直接映射 Unicode 码点 U+0000–U+00FF,无替换字符(如 “),天然适合 HTTP Header、表单编码等场景。
核心约束与安全边界
- 仅允许
0x00–0xFF输入字节;超出范围需预校验或截断 - 不支持多字节序列,禁止 UTF-8 解码误用
Go 实现:零拷贝字节映射
// safeISO88591ToUTF8 将 ISO-8859-1 字节切片无损转为 UTF-8 字符串
func safeISO88591ToUTF8(b []byte) string {
// 预校验:拒绝非法字节(如 0xFF 以上)
for _, c := range b {
if c > 0xFF {
panic("invalid ISO-8859-1 byte: " + strconv.FormatUint(uint64(c), 16))
}
}
// 利用 Go 字符串底层字节共享特性,零分配转换
return string(b)
}
逻辑分析:Go 中
string(b)对[]byte的转换是只读视图构建,不复制内存;因 ISO-8859-1 每字节即对应一个有效 Unicode 码点(U+0000–U+00FF),该转换完全保真且线程安全。参数b必须为原始字节流,不可含 UTF-8 多字节残留。
常见误用对比
| 场景 | 正确做法 | 危险操作 |
|---|---|---|
| HTTP Header 解码 | url.PathUnescape → safeISO88591ToUTF8 |
直接 utf8.DecodeRune 解析未校验字节 |
| 表单提交处理 | 先 bytes.TrimRight(b, "\x00") 再转换 |
调用 golang.org/x/text/encoding/charmap.ISO8859_1.NewDecoder()(引入冗余依赖) |
graph TD
A[原始字节流] --> B{是否全在 0x00-0xFF?}
B -->|是| C[直接 string(b) 构造 UTF-8 字符串]
B -->|否| D[panic 或返回 error]
4.4 双轨响应头构造:filename与filename*并行输出的兼容性兜底方案
HTTP 文件下载场景中,Content-Disposition 头需兼顾旧浏览器(仅支持 filename)与国际化环境(需 filename* RFC 5987 编码)。
兼容性双轨策略
- 优先输出标准
filename*(UTF-8 + percent-encoding) - 必须紧随其后追加 ASCII-only
filename作为降级后备
Content-Disposition: attachment;
filename*=UTF-8''%E6%96%87%E4%BB%B6%E5%90%8D.pdf;
filename="wenjianming.pdf"
✅ 逻辑分析:
filename*满足 Unicode 安全传输;filename提供 IE8/Outlook 等老旧客户端兜底。二者共存时,现代 UA 忽略filename,旧 UA 忽略filename*(按规范静默丢弃未知参数)。
关键约束对照表
| 字段 | 编码要求 | 字符集限制 | 浏览器支持 |
|---|---|---|---|
filename* |
RFC 5987 编码 | 任意 Unicode | Chrome 12+, FF 4+ |
filename |
ISO-8859-1 或 US-ASCII | 仅 ASCII | 全版本兼容 |
graph TD
A[生成原始文件名] --> B{含非ASCII字符?}
B -->|是| C[URL-encode as UTF-8 → filename*]
B -->|否| D[直接赋值 → filename]
C --> E[同时写入 filename 降级值]
D --> E
E --> F[HTTP 响应头双轨输出]
第五章:Golang文件的上传和下载
文件上传的核心实现
Golang标准库 net/http 提供了原生支持 multipart/form-data 的能力。以下是一个生产级文件上传处理器示例,包含大小限制、安全校验与临时存储:
func uploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 限制最大上传体积为10MB
r.ParseMultipartForm(10 << 20)
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "No file received", http.StatusBadRequest)
return
}
defer file.Close()
// 安全校验:过滤危险扩展名与空字节
ext := strings.ToLower(filepath.Ext(header.Filename))
allowedExt := map[string]bool{".pdf": true, ".png": true, ".jpg": true, ".txt": true}
if !allowedExt[ext] {
http.Error(w, "Unsupported file type", http.StatusUnsupportedMediaType)
return
}
// 生成唯一文件名(避免路径遍历)
safeName := fmt.Sprintf("%s%s", uuid.New().String(), ext)
dst, err := os.Create(filepath.Join("/var/uploads", safeName))
if err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
return
}
defer dst.Close()
if _, err := io.Copy(dst, file); err != nil {
http.Error(w, "Failed to write file", http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(map[string]string{"status": "success", "filename": safeName})
}
文件下载的流式响应
下载需设置正确 Content-Type 和 Content-Disposition 头部,并采用 io.Copy 避免内存溢出。以下为按 ID 查询并流式传输的实现:
| 字段 | 值 | 说明 |
|---|---|---|
Content-Type |
application/octet-stream |
通用二进制类型 |
Content-Disposition |
attachment; filename="report.pdf" |
强制浏览器下载而非内嵌 |
Content-Length |
动态计算 | 提升客户端进度感知 |
func downloadHandler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
filePath := filepath.Join("/var/uploads", id)
info, err := os.Stat(filePath)
if os.IsNotExist(err) {
http.Error(w, "File not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, info.Name()))
w.Header().Set("Content-Length", strconv.FormatInt(info.Size(), 10))
file, err := os.Open(filePath)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer file.Close()
io.Copy(w, file) // 流式传输,不加载全量到内存
}
并发上传的限流控制
使用 semaphore 控制并发上传数,防止资源耗尽:
flowchart TD
A[HTTP POST /upload] --> B{Acquire Semaphore}
B -->|Success| C[Parse Multipart]
B -->|Blocked| D[Wait in Queue]
C --> E[Validate & Save]
E --> F[Release Semaphore]
客户端调用示例
前端使用 fetch 上传文件时需构造 FormData:
const formData = new FormData();
formData.append('file', document.getElementById('fileInput').files[0]);
fetch('/upload', {
method: 'POST',
body: formData
}).then(r => r.json()).then(console.log);
错误处理边界场景
- 空文件(
header.Size == 0)应返回400 Bad Request - 文件名含
../或\0字符时立即拒绝 os.Create失败需检查/var/uploads目录权限与磁盘空间io.Copy中断需记录日志并清理已写入的临时碎片
Nginx反向代理配置建议
为支持大文件上传,需在 nginx.conf 中调整:
location /upload {
client_max_body_size 10M;
proxy_buffering off;
proxy_request_buffering off;
} 