文件上传下载 #

一、文件上传概述 #

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