文件上传
<!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
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| QPS | 11.6 万 | 29.8 万 | +255% |
| 平均延迟 | 4.72ms | 1.80ms | -62% |
| 吞吐量 | 264MB/s | 651MB/s | +246% |