文件上传下载 #
一、文件上传概述 #
1.1 文件上传流程 #
text
客户端 → multipart/form-data → 服务端 → 解析表单 → 保存文件
1.2 相关方法 #
| 方法 | 说明 |
|---|---|
| FormFile | 获取单个文件 |
| MultipartForm | 获取多文件表单 |
| SaveUploadedFile | 保存上传文件 |
二、单文件上传 #
2.1 基本上传 #
go
func main() {
r := gin.Default()
r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"filename": file.Filename,
"size": file.Size,
"header": file.Header,
})
})
r.Run()
}
2.2 保存文件 #
go
func main() {
r := gin.Default()
r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 保存到指定目录
dst := "./uploads/" + file.Filename
if err := c.SaveUploadedFile(file, dst); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"message": "上传成功",
"filename": file.Filename,
"size": file.Size,
"path": dst,
})
})
r.Run()
}
2.3 重命名文件 #
go
func main() {
r := gin.Default()
r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 生成唯一文件名
ext := filepath.Ext(file.Filename)
filename := uuid.New().String() + ext
dst := "./uploads/" + filename
if err := c.SaveUploadedFile(file, dst); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"message": "上传成功",
"filename": filename,
"original": file.Filename,
"path": dst,
})
})
r.Run()
}
三、多文件上传 #
3.1 批量上传 #
go
func main() {
r := gin.Default()
r.POST("/uploads", func(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
files := form.File["files"]
var results []gin.H
for _, file := range files {
dst := "./uploads/" + file.Filename
if err := c.SaveUploadedFile(file, dst); err != nil {
results = append(results, gin.H{
"filename": file.Filename,
"error": err.Error(),
})
continue
}
results = append(results, gin.H{
"filename": file.Filename,
"size": file.Size,
"status": "success",
})
}
c.JSON(200, gin.H{
"message": "上传完成",
"files": results,
})
})
r.Run()
}
3.2 带其他表单数据 #
go
func main() {
r := gin.Default()
r.POST("/upload-with-data", func(c *gin.Context) {
// 获取表单数据
title := c.PostForm("title")
description := c.PostForm("description")
// 获取文件
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
dst := "./uploads/" + file.Filename
c.SaveUploadedFile(file, dst)
c.JSON(200, gin.H{
"title": title,
"description": description,
"filename": file.Filename,
"size": file.Size,
})
})
r.Run()
}
四、文件验证 #
4.1 文件类型验证 #
go
func main() {
r := gin.Default()
r.POST("/upload/image", func(c *gin.Context) {
file, err := c.FormFile("image")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 打开文件
f, err := file.Open()
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
defer f.Close()
// 检测文件类型
buffer := make([]byte, 512)
f.Read(buffer)
contentType := http.DetectContentType(buffer)
// 允许的图片类型
allowedTypes := map[string]bool{
"image/jpeg": true,
"image/png": true,
"image/gif": true,
"image/webp": true,
}
if !allowedTypes[contentType] {
c.JSON(400, gin.H{"error": "不支持的文件类型"})
return
}
// 重置文件指针
f.Seek(0, 0)
dst := "./uploads/" + file.Filename
c.SaveUploadedFile(file, dst)
c.JSON(200, gin.H{
"message": "上传成功",
"filename": file.Filename,
"contentType": contentType,
})
})
r.Run()
}
4.2 文件大小验证 #
go
func main() {
r := gin.Default()
r.POST("/upload/size", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 限制文件大小 (5MB)
maxSize := int64(5 * 1024 * 1024)
if file.Size > maxSize {
c.JSON(400, gin.H{"error": "文件大小不能超过5MB"})
return
}
dst := "./uploads/" + file.Filename
c.SaveUploadedFile(file, dst)
c.JSON(200, gin.H{
"message": "上传成功",
"filename": file.Filename,
"size": file.Size,
})
})
r.Run()
}
4.3 文件扩展名验证 #
go
func main() {
r := gin.Default()
r.POST("/upload/ext", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 允许的扩展名
allowedExts := map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
".gif": true,
".pdf": true,
}
ext := strings.ToLower(filepath.Ext(file.Filename))
if !allowedExts[ext] {
c.JSON(400, gin.H{"error": "不支持的文件类型"})
return
}
dst := "./uploads/" + file.Filename
c.SaveUploadedFile(file, dst)
c.JSON(200, gin.H{
"message": "上传成功",
"filename": file.Filename,
})
})
r.Run()
}
4.4 完整验证示例 #
go
type FileValidator struct {
MaxSize int64
AllowedExts []string
AllowedTypes []string
}
func (v *FileValidator) Validate(file *multipart.FileHeader) error {
// 检查大小
if file.Size > v.MaxSize {
return fmt.Errorf("文件大小不能超过 %d 字节", v.MaxSize)
}
// 检查扩展名
ext := strings.ToLower(filepath.Ext(file.Filename))
extAllowed := false
for _, allowed := range v.AllowedExts {
if ext == allowed {
extAllowed = true
break
}
}
if !extAllowed {
return fmt.Errorf("不支持的文件扩展名: %s", ext)
}
// 检查文件类型
f, err := file.Open()
if err != nil {
return err
}
defer f.Close()
buffer := make([]byte, 512)
f.Read(buffer)
contentType := http.DetectContentType(buffer)
typeAllowed := false
for _, allowed := range v.AllowedTypes {
if contentType == allowed {
typeAllowed = true
break
}
}
if !typeAllowed {
return fmt.Errorf("不支持的文件类型: %s", contentType)
}
return nil
}
func main() {
r := gin.Default()
validator := &FileValidator{
MaxSize: 5 * 1024 * 1024,
AllowedExts: []string{".jpg", ".jpeg", ".png", ".gif"},
AllowedTypes: []string{"image/jpeg", "image/png", "image/gif"},
}
r.POST("/upload/validate", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
if err := validator.Validate(file); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
dst := "./uploads/" + file.Filename
c.SaveUploadedFile(file, dst)
c.JSON(200, gin.H{
"message": "上传成功",
"filename": file.Filename,
})
})
r.Run()
}
五、文件下载 #
5.1 基本下载 #
go
func main() {
r := gin.Default()
r.GET("/download/:filename", func(c *gin.Context) {
filename := c.Param("filename")
filepath := "./uploads/" + filename
c.File(filepath)
})
r.Run()
}
5.2 带下载头的响应 #
go
func main() {
r := gin.Default()
r.GET("/download/:filename", func(c *gin.Context) {
filename := c.Param("filename")
filepath := "./uploads/" + filename
// 检查文件是否存在
if _, err := os.Stat(filepath); os.IsNotExist(err) {
c.JSON(404, gin.H{"error": "文件不存在"})
return
}
// 设置下载头
c.Header("Content-Description", "File Transfer")
c.Header("Content-Transfer-Encoding", "binary")
c.Header("Content-Disposition", "attachment; filename="+filename)
c.Header("Content-Type", "application/octet-stream")
c.File(filepath)
})
r.Run()
}
5.3 中文文件名 #
go
func main() {
r := gin.Default()
r.GET("/download/:filename", func(c *gin.Context) {
filename := c.Param("filename")
filepath := "./uploads/" + filename
// 处理中文文件名
encodedFilename := url.QueryEscape(filename)
c.Header("Content-Disposition",
"attachment; filename*=UTF-8''"+encodedFilename)
c.File(filepath)
})
r.Run()
}
5.4 文件流下载 #
go
func main() {
r := gin.Default()
r.GET("/stream/:filename", func(c *gin.Context) {
filename := c.Param("filename")
filepath := "./uploads/" + filename
file, err := os.Open(filepath)
if err != nil {
c.JSON(404, gin.H{"error": "文件不存在"})
return
}
defer file.Close()
fileInfo, _ := file.Stat()
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Length", strconv.FormatInt(fileInfo.Size(), 10))
c.Header("Content-Disposition", "attachment; filename="+filename)
io.Copy(c.Writer, file)
})
r.Run()
}
5.5 分块下载 #
go
func main() {
r := gin.Default()
r.GET("/chunk/:filename", func(c *gin.Context) {
filename := c.Param("filename")
filepath := "./uploads/" + filename
file, err := os.Open(filepath)
if err != nil {
c.JSON(404, gin.H{"error": "文件不存在"})
return
}
defer file.Close()
fileInfo, _ := file.Stat()
// 支持断点续传
http.ServeContent(c.Writer, c.Request, filename, fileInfo.ModTime(), file)
})
r.Run()
}
六、图片处理 #
6.1 图片缩略图 #
go
func main() {
r := gin.Default()
r.POST("/upload/thumbnail", func(c *gin.Context) {
file, err := c.FormFile("image")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 打开图片
f, _ := file.Open()
defer f.Close()
img, _, err := image.Decode(f)
if err != nil {
c.JSON(400, gin.H{"error": "无效的图片"})
return
}
// 生成缩略图
thumbnail := resize.Thumbnail(200, 200, img, resize.Lanczos3)
// 保存缩略图
out, _ := os.Create("./uploads/thumb_" + file.Filename)
defer out.Close()
jpeg.Encode(out, thumbnail, nil)
c.JSON(200, gin.H{
"message": "上传成功",
"filename": file.Filename,
"thumbnail": "thumb_" + file.Filename,
})
})
r.Run()
}
6.2 图片预览 #
go
func main() {
r := gin.Default()
r.GET("/preview/:filename", func(c *gin.Context) {
filename := c.Param("filename")
filepath := "./uploads/" + filename
// 设置为内联显示
c.Header("Content-Disposition", "inline; filename="+filename)
c.File(filepath)
})
r.Run()
}
七、文件存储 #
7.1 按日期存储 #
go
func main() {
r := gin.Default()
r.POST("/upload/date", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 按日期创建目录
dateDir := time.Now().Format("2006/01/02")
uploadDir := "./uploads/" + dateDir
if err := os.MkdirAll(uploadDir, 0755); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
// 生成唯一文件名
ext := filepath.Ext(file.Filename)
filename := uuid.New().String() + ext
dst := uploadDir + "/" + filename
c.SaveUploadedFile(file, dst)
c.JSON(200, gin.H{
"message": "上传成功",
"path": dateDir + "/" + filename,
})
})
r.Run()
}
7.2 云存储上传 #
go
func main() {
r := gin.Default()
r.POST("/upload/oss", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// 打开文件
f, _ := file.Open()
defer f.Close()
// 上传到OSS
bucketName := "your-bucket"
objectKey := "uploads/" + uuid.New().String() + filepath.Ext(file.Filename)
// 这里使用阿里云OSS SDK示例
// client, _ := oss.New(endpoint, accessID, accessKey)
// bucket, _ := client.Bucket(bucketName)
// bucket.PutObject(objectKey, f)
c.JSON(200, gin.H{
"message": "上传成功",
"url": "https://" + bucketName + ".oss-cn-hangzhou.aliyuncs.com/" + objectKey,
})
})
r.Run()
}
八、总结 #
8.1 核心要点 #
| 要点 | 说明 |
|---|---|
| 单文件上传 | c.FormFile() |
| 多文件上传 | c.MultipartForm() |
| 保存文件 | c.SaveUploadedFile() |
| 文件下载 | c.File() |
| 文件验证 | 类型、大小、扩展名 |
8.2 最佳实践 #
| 实践 | 说明 |
|---|---|
| 文件验证 | 验证类型、大小、扩展名 |
| 重命名 | 使用UUID避免冲突 |
| 目录组织 | 按日期或类型分目录 |
| 安全存储 | 不暴露真实路径 |
8.3 下一步 #
现在你已经掌握了文件上传下载,接下来让我们学习 数据绑定,了解Gin的数据绑定机制!
最后更新:2026-03-28