文件上传下载是 Web 应用常见的功能,Gin 对此有很好的支持。
最简单的单文件上传:
package main
import (
"net/http"
"path/filepath"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.MaxMultipartMemory = 8 << 20
r.POST("/upload", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
filename := filepath.Base(file.Filename)
dst := filepath.Join("./uploads", filename)
if err := c.SaveUploadedFile(file, dst); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "上传成功",
"filename": filename,
"size": file.Size,
})
})
r.Run(":8080")
}
前端表单:
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<button type="submit">上传</button>
</form>
上传多个文件:
r.POST("/uploads", func(c *gin.Context) {
form, err := c.MultipartForm()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
files := form.File["files"]
var uploaded []string
for _, file := range files {
filename := filepath.Base(file.Filename)
dst := filepath.Join("./uploads", filename)
if err := c.SaveUploadedFile(file, dst); err != nil {
continue
}
uploaded = append(uploaded, filename)
}
c.JSON(http.StatusOK, gin.H{
"message": "上传完成",
"uploaded": uploaded,
"count": len(uploaded),
})
})
上传前验证文件类型:
func allowedFileType(filename string) bool {
ext := strings.ToLower(filepath.Ext(filename))
allowed := map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
".gif": true,
".pdf": true,
}
return allowed[ext]
}
r.POST("/upload/image", func(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择文件"})
return
}
if !allowedFileType(file.Filename) {
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的文件类型"})
return
}
if file.Size > 5*1024*1024 {
c.JSON(http.StatusBadRequest, gin.H{"error": "文件大小不能超过5MB"})
return
}
ext := filepath.Ext(file.Filename)
newFilename := fmt.Sprintf("%d%s", time.Now().UnixNano(), ext)
dst := filepath.Join("./uploads", newFilename)
c.SaveUploadedFile(file, dst)
c.JSON(http.StatusOK, gin.H{"url": "/uploads/" + newFilename})
})
简单的文件下载:
r.GET("/download/:filename", func(c *gin.Context) {
filename := c.Param("filename")
filepath := filepath.Join("./uploads", filename)
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.GET("/download/large/:filename", func(c *gin.Context) {
filename := c.Param("filename")
filepath := filepath.Join("./uploads", filename)
file, err := os.Open(filepath)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
return
}
defer file.Close()
info, _ := file.Stat()
c.Header("Content-Disposition", "attachment; filename="+filename)
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Length", fmt.Sprintf("%d", info.Size()))
c.DataFromReader(http.StatusOK, info.Size(), "application/octet-stream", file, nil)
})
支持断点续传:
r.GET("/download/resumable/:filename", func(c *gin.Context) {
filename := c.Param("filename")
filepath := filepath.Join("./uploads", filename)
file, err := os.Open(filepath)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "文件不存在"})
return
}
defer file.Close()
info, _ := file.Stat()
c.Header("Accept-Ranges", "bytes")
c.Header("Content-Disposition", "attachment; filename="+filename)
rangeHeader := c.GetHeader("Range")
if rangeHeader == "" {
c.Header("Content-Length", fmt.Sprintf("%d", info.Size()))
c.DataFromReader(http.StatusOK, info.Size(), "application/octet-stream", file, nil)
return
}
var start, end int64
fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end)
if end == 0 || end >= info.Size() {
end = info.Size() - 1
}
file.Seek(start, 0)
c.Header("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, info.Size()))
c.Header("Content-Length", fmt.Sprintf("%d", end-start+1))
c.DataFromReader(http.StatusPartialContent, end-start+1, "application/octet-stream", file, nil)
})
实际项目中经常需要处理图片:
import (
"image"
"image/jpeg"
"image/png"
"github.com/disintegration/imaging"
)
r.POST("/upload/avatar", func(c *gin.Context) {
file, err := c.FormFile("avatar")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择头像"})
return
}
src, err := file.Open()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer src.Close()
img, _, err := image.Decode(src)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的图片文件"})
return
}
thumbnail := imaging.Resize(img, 200, 200, imaging.Lanczos)
filename := fmt.Sprintf("%d.jpg", time.Now().UnixNano())
dst := filepath.Join("./uploads/avatars", filename)
out, _ := os.Create(dst)
defer out.Close()
jpeg.Encode(out, thumbnail, &jpeg.Options{Quality: 85})
c.JSON(http.StatusOK, gin.H{"url": "/uploads/avatars/" + filename})
})
Gin 的文件上传下载功能足够满足大部分需求。上传时注意设置 MaxMultipartMemory 限制内存使用,下载大文件时使用流式传输。别忘了验证文件类型和大小,防止恶意上传。