最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

Gin實現(xiàn)大文件分片上傳與斷點續(xù)傳過程

 更新時間:2026年04月26日 10:17:07   作者:碼刀攻城  
本文介紹了基于Go語言Gin框架實現(xiàn)大文件分片上傳與斷點續(xù)傳的完整流程,首先概述了分片上傳和斷點續(xù)傳的核心原理,接著詳細(xì)描述了服務(wù)端的實現(xiàn),包括全局配置、核心接口和路由注冊等,客戶端部分則提供了簡易示例代碼

在Web開發(fā)中,大文件上傳是常見的需求場景(如視頻、壓縮包、大型數(shù)據(jù)集等),但傳統(tǒng)的單文件上傳方式存在諸多問題:網(wǎng)絡(luò)中斷導(dǎo)致上傳失敗需從頭重傳、單次請求體過大引發(fā)超時、服務(wù)器內(nèi)存占用過高易觸發(fā)OOM等。為解決這些問題,分片上傳+斷點續(xù)傳 成為主流解決方案。

本文將基于Go語言的Gin Web框架,從零實現(xiàn)大文件分片上傳與斷點續(xù)傳功能,并結(jié)合實際場景拓展相關(guān)優(yōu)化思路。

一、核心原理剖析

在編寫代碼前,先理清分片上傳和斷點續(xù)傳的核心邏輯,這是實現(xiàn)的基礎(chǔ):

1. 分片上傳(Chunked Upload)

將一個大文件按照固定大?。ㄈ?MB)拆分成多個小分片(Chunk),客戶端逐個將分片上傳至服務(wù)端;

服務(wù)端接收所有分片后,按照分片的索引順序?qū)⑵浜喜橥暾脑嘉募?/p>

2. 斷點續(xù)傳(Resumable Upload)

基于分片上傳的基礎(chǔ),為每個文件生成唯一標(biāo)識(通常是文件內(nèi)容的MD5值,避免文件名重復(fù)導(dǎo)致的沖突)。

客戶端上傳前先向服務(wù)端查詢該文件已上傳的分片列表,僅上傳未完成的分片;服務(wù)端則記錄每個文件的分片上傳狀態(tài),確保斷點續(xù)傳的可行性。

3. 核心流程

  1. 客戶端:計算文件MD5 → 拆分文件為分片 → 查詢服務(wù)端已上傳分片 → 上傳未完成分片 → 全部上傳完成后請求服務(wù)端合并分片。
  2. 服務(wù)端:提供「分片查詢接口」→ 提供「分片接收接口」→ 提供「分片合并接口」→ 合并完成后清理臨時分片文件。

二、技術(shù)選型與環(huán)境準(zhǔn)備

1. 技術(shù)棧

  • 服務(wù)端:Go 1.20+ + Gin框架(輕量、高性能,適合Web API開發(fā))。
  • 客戶端:Go編寫簡易客戶端(也可替換為前端JS/TS,原理一致)。
  • 存儲:本地文件系統(tǒng)(臨時存儲分片,合并后存儲完整文件;生產(chǎn)環(huán)境可替換為MinIO、S3等對象存儲)。

2. 環(huán)境初始化

首先創(chuàng)建項目并安裝Gin依賴:

# 創(chuàng)建項目目錄
mkdir gin-large-file-upload && cd gin-large-file-upload
# 初始化Go模塊
go mod init gin-large-file-upload
# 安裝Gin
go get github.com/gin-gonic/gin

三、服務(wù)端實現(xiàn)

服務(wù)端需要實現(xiàn)三個核心接口:分片查詢、分片上傳、分片合并。同時需處理臨時文件存儲、分片狀態(tài)記錄、文件合并等邏輯。

1. 全局配置與工具函數(shù)

先定義全局配置(如分片大小、臨時目錄、目標(biāo)文件目錄)和通用工具函數(shù)(如生成文件MD5、檢查目錄是否存在):

// main.go
package main

import (
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"

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

// 全局配置
const (
	ChunkSize     = 5 * 1024 * 1024 // 分片大小:5MB
	TempDir       = "./temp_chunks" // 分片臨時存儲目錄
	UploadDir     = "./upload_files" // 最終文件存儲目錄
	MaxFileSize   = 100 * 1024 * 1024 // 最大文件限制:100MB
)

// 初始化目錄(確保臨時目錄和上傳目錄存在)
func initDir() error {
	if err := os.MkdirAll(TempDir, 0755); err != nil {
		return fmt.Errorf("創(chuàng)建臨時目錄失?。?v", err)
	}
	if err := os.MkdirAll(UploadDir, 0755); err != nil {
		return fmt.Errorf("創(chuàng)建上傳目錄失?。?v", err)
	}
	return nil
}

// 計算文件MD5(用于文件唯一標(biāo)識)
func calculateFileMD5(filePath string) (string, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return "", err
	}
	defer file.Close()

	hash := md5.New()
	if _, err := io.Copy(hash, file); err != nil {
		return "", err
	}

	return hex.EncodeToString(hash.Sum(nil)), nil
}

// 獲取分片文件的存儲路徑
func getChunkPath(fileMD5 string, chunkIndex int) string {
	return filepath.Join(TempDir, fmt.Sprintf("%s_%d", fileMD5, chunkIndex))
}

// 檢查分片是否已上傳
func isChunkExist(fileMD5 string, chunkIndex int) bool {
	chunkPath := getChunkPath(fileMD5, chunkIndex)
	_, err := os.Stat(chunkPath)
	return err == nil // 無錯誤則表示文件存在
}

// 檢查所有分片是否上傳完成
func isAllChunksUploaded(fileMD5 string, totalChunks int) bool {
	for i := 0; i < totalChunks; i++ {
		if !isChunkExist(fileMD5, i) {
			return false
		}
	}
	return true
}

2. 核心接口實現(xiàn)

(1)分片查詢接口

客戶端上傳前調(diào)用該接口,查詢指定文件的已上傳分片列表,返回未上傳的分片索引。

// 查詢已上傳分片
func checkChunkHandler(c *gin.Context) {
	// 獲取請求參數(shù)
	fileMD5 := c.Query("file_md5")
	totalChunksStr := c.Query("total_chunks")
	if fileMD5 == "" || totalChunksStr == "" {
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    400,
			"message": "file_md5和total_chunks為必傳參數(shù)",
		})
		return
	}

	// 轉(zhuǎn)換總分片數(shù)為整數(shù)
	totalChunks, err := strconv.Atoi(totalChunksStr)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    400,
			"message": "total_chunks必須為整數(shù)",
		})
		return
	}

	// 收集已上傳的分片索引
	uploadedChunks := make([]int, 0)
	for i := 0; i < totalChunks; i++ {
		if isChunkExist(fileMD5, i) {
			uploadedChunks = append(uploadedChunks, i)
		}
	}

	c.JSON(http.StatusOK, gin.H{
		"code":            200,
		"message":         "查詢成功",
		"uploaded_chunks": uploadedChunks,
	})
}

(2)分片上傳接口

接收客戶端上傳的分片文件,保存至臨時目錄,并返回上傳結(jié)果。

// 上傳分片
func uploadChunkHandler(c *gin.Context) {
	// 解析表單(分片文件+參數(shù))
	file, fileHeader, err := c.Request.FormFile("chunk")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    400,
			"message": "獲取分片文件失?。? + err.Error(),
		})
		return
	}
	defer file.Close()

	// 獲取其他參數(shù)
	fileMD5 := c.PostForm("file_md5")
	chunkIndexStr := c.PostForm("chunk_index")
	fileName := c.PostForm("file_name")
	if fileMD5 == "" || chunkIndexStr == "" || fileName == "" {
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    400,
			"message": "file_md5、chunk_index、file_name為必傳參數(shù)",
		})
		return
	}

	// 轉(zhuǎn)換分片索引為整數(shù)
	chunkIndex, err := strconv.Atoi(chunkIndexStr)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    400,
			"message": "chunk_index必須為整數(shù)",
		})
		return
	}

	// 檢查分片大?。蛇x,防止客戶端上傳過大分片)
	if fileHeader.Size > ChunkSize {
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    400,
			"message": fmt.Sprintf("分片大小不能超過%dMB", ChunkSize/1024/1024),
		})
		return
	}

	// 創(chuàng)建分片文件并寫入內(nèi)容
	chunkPath := getChunkPath(fileMD5, chunkIndex)
	chunkFile, err := os.Create(chunkPath)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"code":    500,
			"message": "創(chuàng)建分片文件失?。? + err.Error(),
		})
		return
	}
	defer chunkFile.Close()

	// 讀取分片內(nèi)容并寫入文件(使用緩沖區(qū),避免內(nèi)存溢出)
	buf := make([]byte, 1024*1024) // 1MB緩沖區(qū)
	_, err = io.CopyBuffer(chunkFile, file, buf)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"code":    500,
			"message": "寫入分片文件失?。? + err.Error(),
		})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"code":    200,
		"message": "分片上傳成功",
		"data": gin.H{
			"file_md5":    fileMD5,
			"chunk_index": chunkIndex,
		},
	})
}

(3)分片合并接口

客戶端確認(rèn)所有分片上傳完成后,調(diào)用該接口,服務(wù)端將所有分片按索引合并為完整文件。

// 合并分片
func mergeChunkHandler(c *gin.Context) {
	// 獲取請求參數(shù)
	fileMD5 := c.PostForm("file_md5")
	totalChunksStr := c.PostForm("total_chunks")
	fileName := c.PostForm("file_name")
	if fileMD5 == "" || totalChunksStr == "" || fileName == "" {
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    400,
			"message": "file_md5、total_chunks、file_name為必傳參數(shù)",
		})
		return
	}

	// 轉(zhuǎn)換總分片數(shù)為整數(shù)
	totalChunks, err := strconv.Atoi(totalChunksStr)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    400,
			"message": "total_chunks必須為整數(shù)",
		})
		return
	}

	// 檢查所有分片是否上傳完成
	if !isAllChunksUploaded(fileMD5, totalChunks) {
		c.JSON(http.StatusBadRequest, gin.H{
			"code":    400,
			"message": "存在未上傳的分片,無法合并",
		})
		return
	}

	// 生成最終文件路徑(避免文件名重復(fù),拼接MD5前綴)
	ext := filepath.Ext(fileName)
	baseName := strings.TrimSuffix(fileName, ext)
	finalFileName := fmt.Sprintf("%s_%s%s", baseName, fileMD5, ext)
	finalFilePath := filepath.Join(UploadDir, finalFileName)

	// 創(chuàng)建最終文件
	finalFile, err := os.Create(finalFilePath)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"code":    500,
			"message": "創(chuàng)建最終文件失?。? + err.Error(),
		})
		return
	}
	defer finalFile.Close()

	// 按索引順序合并所有分片
	buf := make([]byte, 1024*1024) // 1MB緩沖區(qū)
	for i := 0; i < totalChunks; i++ {
		chunkPath := getChunkPath(fileMD5, i)
		chunkFile, err := os.Open(chunkPath)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{
				"code":    500,
				"message": fmt.Sprintf("打開分片%d失敗:%v", i, err),
			})
			return
		}

		// 將分片內(nèi)容寫入最終文件
		_, err = io.CopyBuffer(finalFile, chunkFile, buf)
		chunkFile.Close() // 及時關(guān)閉分片文件
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{
				"code":    500,
				"message": fmt.Sprintf("合并分片%d失?。?v", i, err),
			})
			return
		}

		// 刪除已合并的分片文件(可選,也可定時清理)
		os.Remove(chunkPath)
	}

	c.JSON(http.StatusOK, gin.H{
		"code":    200,
		"message": "文件合并成功",
		"data": gin.H{
			"file_name": finalFileName,
			"file_path": finalFilePath,
		},
	})
}

3. 注冊路由并啟動服務(wù)

將上述接口注冊到Gin路由,并啟動服務(wù):

func main() {
	// 初始化目錄
	if err := initDir(); err != nil {
		fmt.Printf("目錄初始化失敗:%v\n", err)
		return
	}

	// 創(chuàng)建Gin引擎
	r := gin.Default()

	// 注冊路由
	r.GET("/check-chunk", checkChunkHandler)       // 分片查詢
	r.POST("/upload-chunk", uploadChunkHandler)    // 分片上傳
	r.POST("/merge-chunk", mergeChunkHandler)      // 分片合并

	// 啟動服務(wù)
	fmt.Println("服務(wù)啟動成功,監(jiān)聽端口:8080")
	if err := r.Run(":8080"); err != nil {
		fmt.Printf("服務(wù)啟動失?。?v\n", err)
	}
}

四、客戶端實現(xiàn)

為了驗證服務(wù)端功能,編寫一個簡易的Go客戶端,實現(xiàn)文件分片、斷點續(xù)傳、合并請求的邏輯:

// client.go
package main

import (
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
)

const (
	ServerAddr    = "http://127.0.0.1:8080" // 服務(wù)端地址
	ClientChunkSize = 5 * 1024 * 1024      // 分片大?。ㄅc服務(wù)端一致)
)

// 計算文件MD5(同服務(wù)端邏輯)
func calcFileMD5(filePath string) (string, error) {
	file, err := os.Open(filePath)
	if err != nil {
		return "", err
	}
	defer file.Close()

	hash := md5.New()
	if _, err := io.Copy(hash, file); err != nil {
		return "", err
	}

	return hex.EncodeToString(hash.Sum(nil)), nil
}

// 查詢已上傳分片
func checkUploadedChunks(fileMD5 string, totalChunks int) ([]int, error) {
	// 構(gòu)造請求URL
	url := fmt.Sprintf("%s/check-chunk?file_md5=%s&total_chunks=%d", ServerAddr, fileMD5, totalChunks)
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	// 解析響應(yīng)
	var result struct {
		Code            int   `json:"code"`
		Message         string `json:"message"`
		UploadedChunks  []int  `json:"uploaded_chunks"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return nil, err
	}

	if result.Code != 200 {
		return nil, fmt.Errorf("查詢失敗:%s", result.Message)
	}

	return result.UploadedChunks, nil
}

// 上傳單個分片
func uploadChunk(filePath string, fileMD5 string, chunkIndex int, chunkData []byte, fileName string) error {
	// 構(gòu)造multipart/form-data請求
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)

	// 添加分片文件
	part, err := writer.CreateFormFile("chunk", fmt.Sprintf("chunk_%d", chunkIndex))
	if err != nil {
		return err
	}
	_, err = part.Write(chunkData)
	if err != nil {
		return err
	}

	// 添加其他參數(shù)
	_ = writer.WriteField("file_md5", fileMD5)
	_ = writer.WriteField("chunk_index", strconv.Itoa(chunkIndex))
	_ = writer.WriteField("file_name", fileName)

	// 關(guān)閉writer,完成表單構(gòu)造
	writer.Close()

	// 發(fā)送POST請求
	url := fmt.Sprintf("%s/upload-chunk", ServerAddr)
	req, err := http.NewRequest("POST", url, body)
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", writer.FormDataContentType())

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// 解析響應(yīng)
	var result struct {
		Code    int    `json:"code"`
		Message string `json:"message"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return err
	}

	if result.Code != 200 {
		return fmt.Errorf("分片%d上傳失?。?s", chunkIndex, result.Message)
	}

	fmt.Printf("分片%d上傳成功\n", chunkIndex)
	return nil
}

// 請求合并分片
func mergeChunks(fileMD5 string, totalChunks int, fileName string) error {
	// 構(gòu)造請求參數(shù)
	data := strings.NewReader(fmt.Sprintf("file_md5=%s&total_chunks=%d&file_name=%s", fileMD5, totalChunks, fileName))
	url := fmt.Sprintf("%s/merge-chunk", ServerAddr)
	req, err := http.NewRequest("POST", url, data)
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	// 解析響應(yīng)
	var result struct {
		Code    int    `json:"code"`
		Message string `json:"message"`
		Data    struct {
			FileName string `json:"file_name"`
			FilePath string `json:"file_path"`
		} `json:"data"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
		return err
	}

	if result.Code != 200 {
		return fmt.Errorf("合并分片失?。?s", result.Message)
	}

	fmt.Printf("文件合并成功!最終文件:%s,路徑:%s\n", result.Data.FileName, result.Data.FilePath)
	return nil
}

// 大文件上傳主邏輯
func uploadLargeFile(filePath string) error {
	// 1. 打開文件并獲取基本信息
	file, err := os.Open(filePath)
	if err != nil {
		return err
	}
	defer file.Close()

	fileInfo, err := file.Stat()
	if err != nil {
		return err
	}
	fileSize := fileInfo.Size()
	fileName := filepath.Base(filePath)

	// 2. 計算文件MD5
	fmt.Println("正在計算文件MD5...")
	fileMD5, err := calcFileMD5(filePath)
	if err != nil {
		return fmt.Errorf("計算MD5失?。?v", err)
	}
	fmt.Printf("文件MD5:%s,大?。?dMB\n", fileMD5, fileSize/1024/1024)

	// 3. 計算總分片數(shù)
	totalChunks := int(fileSize / ClientChunkSize)
	if fileSize%ClientChunkSize != 0 {
		totalChunks += 1
	}
	fmt.Printf("文件將拆分為%d個分片,每個分片%dMB\n", totalChunks, ClientChunkSize/1024/1024)

	// 4. 查詢已上傳分片
	fmt.Println("查詢已上傳分片...")
	uploadedChunks, err := checkUploadedChunks(fileMD5, totalChunks)
	if err != nil {
		return fmt.Errorf("查詢已上傳分片失?。?v", err)
	}
	fmt.Printf("已上傳分片:%v\n", uploadedChunks)

	// 5. 遍歷所有分片,上傳未完成的分片
	buf := make([]byte, ClientChunkSize)
	for i := 0; i < totalChunks; i++ {
		// 跳過已上傳的分片
		skip := false
		for _, idx := range uploadedChunks {
			if idx == i {
				skip = true
				break
			}
		}
		if skip {
			fmt.Printf("分片%d已上傳,跳過\n", i)
			continue
		}

		// 讀取分片數(shù)據(jù)
		offset := int64(i) * ClientChunkSize
		_, err := file.Seek(offset, io.SeekStart)
		if err != nil {
			return fmt.Errorf("讀取分片%d失?。?v", i, err)
		}

		n, err := file.Read(buf)
		if err != nil && err != io.EOF {
			return fmt.Errorf("讀取分片%d失敗:%v", i, err)
		}

		// 上傳分片
		if err := uploadChunk(filePath, fileMD5, i, buf[:n], fileName); err != nil {
			return err
		}
	}

	// 6. 所有分片上傳完成,請求合并
	fmt.Println("所有分片上傳完成,請求合并...")
	if err := mergeChunks(fileMD5, totalChunks, fileName); err != nil {
		return err
	}

	return nil
}

func main() {
	// 替換為你要上傳的大文件路徑
	filePath := "./test_large_file.mp4"
	if err := uploadLargeFile(filePath); err != nil {
		fmt.Printf("文件上傳失?。?v\n", err)
	}
}

五、功能測試

1. 啟動服務(wù)端

go run main.go

輸出「服務(wù)啟動成功,監(jiān)聽端口:8080」表示服務(wù)端啟動正常。

2. 運(yùn)行客戶端

client.go中的filePath替換為本地的大文件路徑(如視頻、壓縮包),然后運(yùn)行:

go run client.go

客戶端會輸出如下日志,表明上傳流程正常:

正在計算文件MD5...
文件MD5:xxxxxx,大?。?0MB
文件將拆分為4個分片,每個分片5MB
查詢已上傳分片...
已上傳分片:[]
分片0上傳成功
分片1上傳成功
分片2上傳成功
分片3上傳成功
所有分片上傳完成,請求合并...
文件合并成功!最終文件:test_large_file_xxxxxx.mp4,路徑:./upload_files/test_large_file_xxxxxx.mp4

3. 斷點續(xù)傳測試

手動中斷客戶端上傳(如按Ctrl+C),重新運(yùn)行客戶端,會看到客戶端跳過已上傳的分片,僅上傳未完成的分片:

正在計算文件MD5...
文件MD5:xxxxxx,大小:20MB
文件將拆分為4個分片,每個分片5MB
查詢已上傳分片...
已上傳分片:[0 1]
分片0已上傳,跳過
分片1已上傳,跳過
分片2上傳成功
分片3上傳成功
所有分片上傳完成,請求合并...
文件合并成功!最終文件:test_large_file_xxxxxx.mp4,路徑:./upload_files/test_large_file_xxxxxx.mp4

六、進(jìn)階拓展與優(yōu)化

上述實現(xiàn)滿足了基礎(chǔ)的分片上傳和斷點續(xù)傳需求,但在生產(chǎn)環(huán)境中,還需要考慮以下優(yōu)化方向:

1. 分片大小動態(tài)調(diào)整

固定5MB分片大小并非適用于所有場景:

  • 分片過?。涸黾親TTP請求次數(shù),加重服務(wù)端負(fù)擔(dān);
  • 分片過大:單分片上傳超時風(fēng)險高。

可根據(jù)文件大小動態(tài)調(diào)整分片大小(如100MB以內(nèi)用5MB分片,1GB以內(nèi)用10MB分片),或允許客戶端自定義分片大?。ǚ?wù)端做上限限制)。

2. 并發(fā)上傳分片

客戶端可通過goroutine并發(fā)上傳多個分片,提升上傳速度。需注意控制并發(fā)數(shù)(如5-10個),避免請求過多導(dǎo)致服務(wù)端壓力過大:

// 并發(fā)上傳示例(客戶端)
var wg sync.WaitGroup
sem := make(chan struct{}, 5) // 限制5個并發(fā)

for i := 0; i < totalChunks; i++ {
	// 跳過已上傳分片...

	wg.Add(1)
	sem <- struct{}{}
	go func(idx int) {
		defer wg.Done()
		defer func() { <-sem }()

		// 讀取分片并上傳...
	}(i)
}
wg.Wait()

3. 臨時分片文件清理

服務(wù)端的臨時分片文件如果長時間未合并(如客戶端中斷上傳),會占用磁盤空間??商砑佣〞r任務(wù)(如每小時)清理超過24小時未合并的分片文件:

// 定時清理臨時分片文件
func cleanExpiredChunks() {
	ticker := time.NewTicker(1 * time.Hour) // 每小時執(zhí)行一次
	for range ticker.C {
		// 遍歷臨時目錄
		entries, err := os.ReadDir(TempDir)
		if err != nil {
			fmt.Printf("讀取臨時目錄失敗:%v\n", err)
			continue
		}

		// 清理超過24小時的文件
		expireTime := time.Now().Add(-24 * time.Hour)
		for _, entry := range entries {
			info, err := entry.Info()
			if err != nil {
				continue
			}
			if info.ModTime().Before(expireTime) {
				os.Remove(filepath.Join(TempDir, entry.Name()))
				fmt.Printf("清理過期分片:%s\n", entry.Name())
			}
		}
	}
}

// 在main函數(shù)中啟動定時任務(wù)
go cleanExpiredChunks()

4. 分片校驗機(jī)制

為避免分片傳輸過程中數(shù)據(jù)損壞,可在客戶端計算每個分片的MD5,上傳時攜帶分片MD5;服務(wù)端接收后校驗分片MD5,確保數(shù)據(jù)完整性:

// 客戶端:計算分片MD5
chunkMD5 := md5.Sum(buf[:n])
chunkMD5Str := hex.EncodeToString(chunkMD5[:])

// 上傳時添加分片MD5參數(shù)
_ = writer.WriteField("chunk_md5", chunkMD5Str)

// 服務(wù)端:校驗分片MD5
chunkMD5 := c.PostForm("chunk_md5")
// 計算接收到的分片內(nèi)容的MD5,與客戶端傳入的對比

5. 分布式場景適配

如果服務(wù)端部署在多節(jié)點,本地文件系統(tǒng)無法共享分片文件,需將分片存儲至分布式對象存儲(如MinIO、AWS S3),并將分片狀態(tài)記錄至Redis:

  • 分片存儲:將分片上傳至MinIO,而非本地目錄;
  • 狀態(tài)記錄:Redis的Set結(jié)構(gòu)存儲已上傳的分片索引(key為文件MD5,value為分片索引集合);
  • 合并邏輯:從MinIO下載所有分片,合并后再上傳至MinIO。

6. 上傳進(jìn)度反饋

服務(wù)端可新增接口,根據(jù)文件MD5返回已上傳分片數(shù)/總分片數(shù),客戶端據(jù)此計算上傳進(jìn)度并展示:

// 服務(wù)端進(jìn)度查詢接口
func uploadProgressHandler(c *gin.Context) {
	fileMD5 := c.Query("file_md5")
	totalChunksStr := c.Query("total_chunks")
	// ... 校驗參數(shù) ...

	uploadedCount := len(uploadedChunks)
	progress := float64(uploadedCount) / float64(totalChunks) * 100

	c.JSON(http.StatusOK, gin.H{
		"code":     200,
		"progress": progress, // 百分比
	})
}

七、注意事項

  1. 目錄權(quán)限:確保服務(wù)端的TempDirUploadDir有讀寫權(quán)限,避免文件創(chuàng)建/寫入失敗。
  2. 文件命名沖突:通過文件MD5+原文件名的方式命名最終文件,避免不同文件重名覆蓋。
  3. 內(nèi)存占用:讀寫文件時使用緩沖區(qū)(如1MB),避免一次性加載大文件/分片到內(nèi)存,引發(fā)OOM。
  4. 超時設(shè)置:客戶端和服務(wù)端都需設(shè)置合理的HTTP超時時間,避免大分片上傳超時。
  5. 異常處理:增加文件鎖、重試機(jī)制,避免并發(fā)上傳同一分片導(dǎo)致的文件損壞。

八、總結(jié)

本文基于Gin框架實現(xiàn)了大文件分片上傳與斷點續(xù)傳的核心功能,從原理剖析到代碼實現(xiàn),再到進(jìn)階優(yōu)化,覆蓋了從基礎(chǔ)到生產(chǎn)級別的關(guān)鍵要點。該方案解決了傳統(tǒng)單文件上傳的痛點,具備高可用性和可擴(kuò)展性。

在實際項目中,可根據(jù)業(yè)務(wù)需求靈活調(diào)整:前端可替換為WebUploader、Resumable.js等成熟的分片上傳庫,服務(wù)端可對接分布式存儲和緩存,進(jìn)一步提升系統(tǒng)的穩(wěn)定性和性能。Go語言的高性能特性結(jié)合Gin框架的輕量優(yōu)勢,使得該方案能夠輕松應(yīng)對高并發(fā)的大文件上傳場景。

以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • 一文詳解Go語言中切片的底層原理

    一文詳解Go語言中切片的底層原理

    在Go語言中,切片作為一種引用類型數(shù)據(jù),相對數(shù)組而言是一種動態(tài)長度的數(shù)據(jù)類型,使用的場景也是非常多,所以本文主要來和大家聊聊切片的底層原理,需要的可以參考一下
    2023-06-06
  • 使用go語言將單反斜杠改為雙反斜杠的方法

    使用go語言將單反斜杠改為雙反斜杠的方法

    最近開發(fā)的時候遇到這么個問題,就是在window上獲取了文件目錄的字段,然后將這個絕對路徑保存到數(shù)據(jù)庫,但是前端展示的時候路徑的雙反斜杠變成了單反斜杠,本文給大家介紹了使用go語言將單反斜杠改為雙反斜杠的方法,需要的朋友可以參考下
    2024-01-01
  • 解決Go gorm踩過的坑

    解決Go gorm踩過的坑

    這篇文章主要介紹了解決Go gorm踩過的坑,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • k8s容器互聯(lián)flannel?vxlan通信原理

    k8s容器互聯(lián)flannel?vxlan通信原理

    這篇文章主要為大家介紹了k8s容器互聯(lián)flannel?vxlan通信原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-04-04
  • 一文帶你分析Go語言中的http請求過程

    一文帶你分析Go語言中的http請求過程

    在go中開發(fā)后端,最基礎(chǔ)的就是使用net/http包,本文將使用一個hello,world程序來進(jìn)行debug,來探究在Go語言中http請求的過程,希望對大家有所幫助
    2026-02-02
  • Go語言實現(xiàn)猜數(shù)字小游戲

    Go語言實現(xiàn)猜數(shù)字小游戲

    這篇文章主要為大家詳細(xì)介紹了Go語言實現(xiàn)猜數(shù)字小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-10-10
  • GoLang jwt無感刷新與SSO單點登錄限制解除方法詳解

    GoLang jwt無感刷新與SSO單點登錄限制解除方法詳解

    這篇文章主要介紹了GoLang jwt無感刷新與SSO單點登錄限制解除方法,JWT是一個簽名的JSON對象,通常用作Oauth2的Bearer token,JWT包括三個用.分割的部分。本文將利用JWT進(jìn)行認(rèn)證和加密,感興趣的可以了解一下
    2023-03-03
  • Go語言中常用的基礎(chǔ)方法總結(jié)

    Go語言中常用的基礎(chǔ)方法總結(jié)

    這篇文章主要為大家詳細(xì)介紹了Go語言中常用的一些基礎(chǔ)方法,例如:使用正則表達(dá)式驗證字符串、格式化字符串、時間的比較等等,需要的可以參考一下
    2022-09-09
  • Go編譯原理之函數(shù)內(nèi)聯(lián)

    Go編譯原理之函數(shù)內(nèi)聯(lián)

    這篇文章主要為大家介紹了Go編譯原理之函數(shù)內(nèi)聯(lián)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-08-08
  • golang判斷chan channel是否關(guān)閉的方法

    golang判斷chan channel是否關(guān)閉的方法

    這篇文章主要介紹了golang判斷chan channel是否關(guān)閉的方法,結(jié)合實例形式對比分析了Go語言判斷chan沒有關(guān)閉的后果及關(guān)閉的方法,需要的朋友可以參考下
    2016-07-07

最新評論

泗洪县| 赫章县| 正安县| 温州市| 黄陵县| 上思县| 当雄县| 丰原市| 兰坪| 皋兰县| 瓮安县| 黄陵县| 宝丰县| 乌拉特中旗| 哈密市| 温宿县| 梁山县| 仙游县| 涡阳县| 双辽市| 合江县| 兖州市| 志丹县| 永安市| 马龙县| 拜泉县| 彭阳县| 汕头市| 米泉市| 龙山县| 安平县| 龙游县| 札达县| 保德县| 亳州市| 饶平县| 阿图什市| 阳江市| 玛曲县| 专栏| 白玉县|