Gin(四) 性能优化 文件上传

4 阅读3分钟

文件上传

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文件上传</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: Arial, sans-serif;
            background-color: #f5f5f5;
            padding: 40px;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 30px;
        }
        .upload-box {
            background: white;
            padding: 25px 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            width: 100%;
            max-width: 450px;
        }
        .upload-box p {
            margin: 12px 0 6px;
            font-size: 14px;
            color: #333;
            font-weight: bold;
        }
        input[type="file"] {
            display: block;
            margin-bottom: 10px;
            padding: 5px;
            width: 100%;
        }
        button {
            margin-top: 15px;
            padding: 8px 20px;
            background: #007bff;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
        }
        button:hover {
            background: #0056b3;
        }
    </style>
</head>
<body>

    <!-- 单个文件上传 -->
    <div class="upload-box">
        <h3>单个文件上传</h3>
        <form action="/regist1" method="post" enctype="multipart/form-data">
            <input type="file" name="file"/>
            <button type="submit">上传</button>
        </form>
    </div>

    <!-- 多文件上传 -->
    <div class="upload-box">
        <h3>多文件上传</h3>
        <form action="/regist2" method="post" enctype="multipart/form-data">
            <p>相册(可上传多个)</p>
            <input type="file" name="file"/>
            <input type="file" name="file"/>

            <p>头像</p>
            <input type="file" name="file2"/>

            <button type="submit">提交上传</button>
        </form>
    </div>

</body>
</html>
package main

import (
        "mime/multipart"
        "net/http"
        "os"
        "path/filepath"
        "strconv"
        "strings"

        "github.com/gin-gonic/gin"
        "github.com/google/uuid"
)

// ====================== 配置 ======================
const (
        MaxFileSize = 2 << 20 // 2MB
        UploadDir   = "./uploads"
)

// 允许的图片类型
var allowExts = map[string]bool{
        ".jpg":  true,
        ".jpeg": true,
        ".png":  true,
        ".gif":  true,
}

// 启动时创建上传目录
func init() {
        _ = os.MkdirAll(UploadDir, 0755)
}

func main() {
        r := gin.Default()

        // 🔥 访问 / 自动返回 static/html/index.html
        r.GET("/", func(c *gin.Context) {
                c.File("./static/html/index.html")
        })

        // 上传接口组(带大小限制中间件)
        uploadGroup := r.Group("/")
        uploadGroup.Use(UploadSizeLimit(MaxFileSize))
        uploadGroup.POST("/regist1", regist1)
        uploadGroup.POST("/regist2", regist2)

        // 启动服务
        r.Run(":8080")
}

// ====================== 大小限制中间件 ======================
func UploadSizeLimit(maxSize int64) gin.HandlerFunc {
        return func(c *gin.Context) {
                // 1. 校验请求头大小
                if cl := c.GetHeader("Content-Length"); cl != "" {
                        size, _ := strconv.ParseInt(cl, 10, 64)
                        if size > maxSize {
                                c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "文件不能超过 2MB"})
                                c.Abort()
                                return
                        }
                }

                // 2. 强制限制请求体大小
                c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize)

                // 执行接口
                c.Next()

                // 3. 捕获超限错误
                for _, e := range c.Errors {
                        if strings.Contains(e.Error(), "request body too large") {
                                c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "文件超过 2MB,已丢弃"})
                                c.Abort()
                        }
                }
        }
}

// ====================== 单文件上传 ======================
func regist1(c *gin.Context) {
        file, err := c.FormFile("file")
        if err != nil {
                _ = c.Error(err)
                return
        }

        // 校验类型
        ext := strings.ToLower(filepath.Ext(file.Filename))
        if !allowExts[ext] {
                c.JSON(400, gin.H{"error": "仅支持 jpg/png/gif"})
                return
        }

        // 保存
        path := saveFile(c, file)
        c.JSON(200, gin.H{"msg": "上传成功", "path": path})
}

// ====================== 多文件 + 混合上传 ======================
func regist2(c *gin.Context) {
        form, err := c.MultipartForm()
        if err != nil {
                _ = c.Error(err)
                return
        }

        // 处理多个 file(相册)
        var photos []string
        for _, file := range form.File["file"] {
                if allowExts[strings.ToLower(filepath.Ext(file.Filename))] {
                        photos = append(photos, saveFile(c, file))
                }
        }

        // 处理单个 file2(头像)
        var avatar string
        file2, err := c.FormFile("file2")
        if err == nil && allowExts[strings.ToLower(filepath.Ext(file2.Filename))] {
                avatar = saveFile(c, file2)
        }

        c.JSON(200, gin.H{
                "相册": photos,
                "头像": avatar,
        })
}

// ====================== 工具函数 ======================
func saveFile(c *gin.Context, file *multipart.FileHeader) string {
        ext := strings.ToLower(filepath.Ext(file.Filename))
        newName := uuid.NewString() + ext
        dst := filepath.Join(UploadDir, newName)
        _ = c.SaveUploadedFile(file, dst)
        return dst
}

现在文件的内存大小,超过了就保存到文件

func main() {
        r := gin.Default()

        // 🔥 修改默认上传大小:例如设置为 50MB
        r.MaxMultipartMemory = 50 << 20 // 50MB

        // ...你的路由
}

性能优化

内存优化 上传接口限制人数,防止1万人过来,直接内存爆炸,这个过程客户单是一直等待状态

r.Use(limiter.Limit(100)) // 最多 100 个上传并发

减少系统调用

目前主流的前段都是SPA,单页面应用程序。而使用static方法,会尝试open一次file,然后stat查看是否为文件夹,如果是文件夹,再拼接一个字符串index.html尝试打开文件。实际go还会再次stat一次文件。一共4次syscall

package main

import "github.com/gin-gonic/gin"

func main(){

        router := gin.New()

        // 虽然会自动找到目录下的index.html,但是会多4次系统调用
        router.Static("/","./static/html")
        router.Run()
}
long@ubuntu2:~/golang$ wrk -t20 -c200 -d30s http://localhost:8080/
Running 30s test @ http://localhost:8080/
  20 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     4.72ms    9.56ms 257.96ms   92.73%
    Req/Sec     5.89k     1.40k   16.28k    79.15%
  3512866 requests in 30.06s, 7.77GB read
Requests/sec: 116863.65
Transfer/sec:    264.58MB

优化后的代码

package main

import "github.com/gin-gonic/gin"
import "os"
var indexData []byte
func init(){
        data, err := os.ReadFile("./static/html/index.html")
        if err != nil {
                panic("首页没有找到")
        }
        indexData = data
}

func main(){

        router := gin.New()

        // 虽然会自动找到目录下的index.html,但是会多2次系统调用
        router.Static("/static","./static/html")
        router.GET("/",func(c *gin.Context){
                c.Data(200, "text/html", indexData)
        })
        router.Run()
}

优化后的性能

ong@ubuntu2:~/golang$ wrk -t20 -c200 -d30s http://localhost:8080/
Running 30s test @ http://localhost:8080/
  20 threads and 200 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.80ms    5.89ms 189.61ms   97.25%
    Req/Sec    15.05k     4.63k   78.66k    78.70%
  8971747 requests in 30.08s, 19.14GB read
Requests/sec: 298254.80
Transfer/sec:    651.65MB
指标优化前优化后提升
QPS11.6 万29.8 万+255%
平均延迟4.72ms1.80ms-62%
吞吐量264MB/s651MB/s+246%