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

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

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

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

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

一、核心原理剖析

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

1. 分片上傳(Chunked Upload)

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

服務端接收所有分片后,按照分片的索引順序將其合并為完整的原始文件。

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

基于分片上傳的基礎,為每個文件生成唯一標識(通常是文件內容的MD5值,避免文件名重復導致的沖突)。

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

3. 核心流程

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

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

1. 技術棧

  • 服務端: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

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

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

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

先定義全局配置(如分片大小、臨時目錄、目標文件目錄)和通用工具函數(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 // 分片大?。?MB
	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(用于文件唯一標識)
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)分片查詢接口

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

// 查詢已上傳分片
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
	}

	// 轉換總分片數(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)分片上傳接口

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

// 上傳分片
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
	}

	// 轉換分片索引為整數(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)建分片文件并寫入內容
	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()

	// 讀取分片內容并寫入文件(使用緩沖區(qū),避免內存溢出)
	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)分片合并接口

客戶端確認所有分片上傳完成后,調用該接口,服務端將所有分片按索引合并為完整文件。

// 合并分片
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
	}

	// 轉換總分片數(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
	}

	// 生成最終文件路徑(避免文件名重復,拼接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
		}

		// 將分片內容寫入最終文件
		_, err = io.CopyBuffer(finalFile, chunkFile, buf)
		chunkFile.Close() // 及時關閉分片文件
		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. 注冊路由并啟動服務

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

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)      // 分片合并

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

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

為了驗證服務端功能,編寫一個簡易的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" // 服務端地址
	ClientChunkSize = 5 * 1024 * 1024      // 分片大?。ㄅc服務端一致)
)

// 計算文件MD5(同服務端邏輯)
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) {
	// 構造請求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()

	// 解析響應
	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 {
	// 構造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)

	// 關閉writer,完成表單構造
	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()

	// 解析響應
	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 {
	// 構造請求參數(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()

	// 解析響應
	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. 啟動服務端

go run main.go

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

2. 運行客戶端

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

go run client.go

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

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

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

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

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

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

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

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

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

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

可根據(jù)文件大小動態(tài)調整分片大小(如100MB以內用5MB分片,1GB以內用10MB分片),或允許客戶端自定義分片大?。ǚ斩俗錾舷尴拗疲?。

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

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

// 并發(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. 臨時分片文件清理

服務端的臨時分片文件如果長時間未合并(如客戶端中斷上傳),會占用磁盤空間。可添加定時任務(如每小時)清理超過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ù)中啟動定時任務
go cleanExpiredChunks()

4. 分片校驗機制

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

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

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

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

5. 分布式場景適配

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

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

6. 上傳進度反饋

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

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

八、總結

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

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

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

相關文章

  • 一文詳解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通信原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-04-04
  • 一文帶你分析Go語言中的http請求過程

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

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

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

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

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

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

    Go語言中常用的基礎方法總結

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

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

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

    golang判斷chan channel是否關閉的方法

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

最新評論

资阳市| 乐至县| 白水县| 大石桥市| 莒南县| 旅游| 天柱县| 余江县| 芜湖县| 金塔县| 黑水县| 巴中市| 富民县| 天津市| 调兵山市| 竹山县| 宁明县| 化州市| 六枝特区| 鄂伦春自治旗| 亚东县| 巫山县| 平和县| 黎川县| 长兴县| 福鼎市| 宁武县| 呼图壁县| 关岭| 玉山县| 新乐市| 武隆县| 句容市| 惠水县| 本溪市| 辉县市| 吴堡县| 建宁县| 黄大仙区| 乐业县| 乐平市|