本篇文章主要是教大家如何在 Golang 上處理檔案上傳的操作,那我們的主題會圍繞在 io
和 os
這兩個 package 的介紹。
package main
import (
"fmt"
"io"
"net/http"
"os"
)
func uploadFile(w http.ResponseWriter, r *http.Request) {
file, header, err := r.FormFile("file")
if err != nil {
fmt.Fprintf(w, "Failed to get file: %v", err)
return
}
defer file.Close()
dstPath := "/path/to/destination/" + header.Filename
dst, err := os.Create(dstPath) // 建立目標檔案
if err != nil {
fmt.Fprintf(w, "Failed to create file: %v", err)
return
}
defer dst.Close()
// 使用 io.Copy 將上傳的檔案寫入到目標文件中
if _, err := io.Copy(dst, file); err != nil {
fmt.Fprintf(w, "Failed to write file: %v", err)
return
}
fmt.Fprintf(w, "File uploaded successfully!\n")
// 開啟剛剛上傳的文件進行讀取
openedFile, err := os.Open(dstPath)
if err != nil {
fmt.Fprintf(w, "Failed to open file after upload: %v", err)
return
}
defer openedFile.Close()
// 讀取並顯示文件內容
fileContent := make([]byte, 1024)
n, err := openedFile.Read(fileContent)
if err != nil && err != io.EOF {
fmt.Fprintf(w, "Failed to read file: %v", err)
return
}
fmt.Fprintf(w, "Uploaded file content (first %d bytes): %s", n, string(fileContent[:n]))
}
func main() {
http.HandleFunc("/upload", uploadFile)
http.ListenAndServe(":8080", nil)
}
- 接收上傳的文件:使用
r.FormFile("file")
來讀取上傳的檔案,file
是上傳文件的Reader
,header
是文件的元數據,所以如果我們要測試 api 就需要在 "key" 輸入file
然後 "類型"記得選擇file
。os.Create()
用來創建一個新的文件。如果文件已經存在,則會覆蓋該文件。這個函數返回一個*os.File
,代表新創建的文件對象。程式中的dst
是創建的目標文件,並用於接收上傳的數據。io.Copy()
是 Go 中常見的 I/O 操作,用來將數據從一個Reader
複製到一個Writer
。在這個例子中,它將從上傳的文件(file
,是multipart.File
類型,實現了Reader
接口)讀取數據,並寫入到目標文件(dst
,實現了Writer
接口)。這樣可以快速地將整個文件內容複製到本地。os.Open()
是用來打開一個現有的文件。它返回一個指向該文件的*os.File
,用於進行讀取操作。在這裡,我們打開剛剛上傳並保存的文件,來檢查文件的內容是否正確。Read()
是從打開的文件中讀取數據,這裡將文件內容讀取到一個byte
切片中。在這個範例中,我們最多讀取 1024byte
的內容。Read()
返回的n
是實際讀取到的byte
數。如果文件比較小,可能不會讀滿 1024 字節。當讀到文件末尾時,Read()
會返回io.EOF
錯誤,表示文件已經讀取完畢。
我這裡選擇在我的/Users/imac/Documents/
路徑下去做上傳的動作
仔細看
修改日期
的話會發現我第二次上傳時會對原有的檔案做複寫的動作!
使用管道(Pipe)來進行流式操作,這在處理大文件或需要實時處理數據的場景中特別有用。io.Pipe()
允許在同一個程式中創建一個讀寫管道,一邊寫入,一邊可以即時讀取。
package main
import (
"fmt"
"io"
"os"
"net/http"
)
// 處理上傳過程中的數據
func processData(r io.Reader, w io.Writer) {
buffer := make([]byte, 1024)
for {
n, err := r.Read(buffer)
if err != nil && err != io.EOF {
fmt.Println("Error while reading:", err)
return
}
if n == 0 {
break
}
// 在這裡可以處理讀取的數據,例如進行加密或壓縮
// 現在只是將原始數據寫回去
if _, err := w.Write(buffer[:n]); err != nil {
fmt.Println("Error while writing:", err)
return
}
}
}
func uploadFile(w http.ResponseWriter, r *http.Request) {
file, header, err := r.FormFile("file")
if err != nil {
fmt.Fprintf(w, "Failed to get file: %v", err)
return
}
defer file.Close()
dst, err := os.Create("/path/to/destination/" + header.Filename)
if err != nil {
fmt.Fprintf(w, "Failed to create file: %v", err)
return
}
defer dst.Close()
// 使用 io.Pipe() 建立讀寫管道
reader, writer := io.Pipe()
// 使用 Goroutine 實時處理數據
go func() {
defer writer.Close() // 確保寫入端最後會關閉
processData(file, writer)
}()
// 使用 io.Copy 將處理後的數據寫入目標文件
if _, err := io.Copy(dst, reader); err != nil {
fmt.Fprintf(w, "Failed to copy data: %v", err)
return
}
fmt.Fprintf(w, "File uploaded and processed successfully!")
}
func main() {
http.HandleFunc("/upload", uploadFile)
http.ListenAndServe(":8080", nil)
}
- processData 函數:
- 使用 r.Read(buffer) 讀取數據到緩衝區(buffer),每次讀取最多 1024 字節。
- 檢查是否讀取完成,若讀取完成則跳出循環。
- 使用 w.Write(buffer[:n]) 將讀取到的數據寫入到管道的另一端。
- 這裡的 r 是上傳的文件,w 是 io.PipeWriter,這樣通過管道把數據傳送到目標文件。
- uploadFile 函數:
- 使用
io.Pipe()
創建了一個連接的PipeReader
和PipeWriter
,使得數據可以在不同的 Goroutine 之間進行傳輸。這裡的reader
和writer
對應於這個管道的兩端。- Goroutine 實時處理上傳的文件,並將處理後的數據傳給管道的讀取端(
reader
),然後將這些數據複製到目標文件中。- 這裡
io.Copy(dst, reader)
的作用是從管道的讀取端讀取數據,並寫入到目標文件(dst
)中。dst
是一個打開的文件句柄,reader
是通過io.Pipe()
創建的管道讀取端。這樣的設計可以實現流式的處理數據,無需將整個文件都讀到內存中,可以有效處理大文件。
package main
import (
"fmt"
"io"
"os"
"sync"
)
const (
chunkSize = 5 * 1024 * 1024 // 每個切片大小為 5MB
maxGoroutines = 6 // 限制同時運行的 Goroutine 數量
largeFilePath = "/path/to/fileName" // 替換為實際的檔案路徑
chunkFilePrefix = "largefile_part_" // 切片檔案的前綴名
)
// uploadFileInChunks 將大檔案切成小片段並並發地處理
func uploadFileInChunks(file *os.File, filename string) error {
// 取得檔案大小
fileInfo, err := file.Stat()
if err != nil {
return fmt.Errorf("failed to get file info: %v", err)
}
fileSize := fileInfo.Size()
// 計算總的切片數量
totalChunks := int((fileSize + chunkSize - 1) / chunkSize)
fmt.Printf("Total chunks to process: %d\n", totalChunks)
// 設置同步機制和錯誤通道
var wg sync.WaitGroup
semaphore := make(chan struct{}, maxGoroutines) // 控制並發數量
errorCh := make(chan error, totalChunks) // 錯誤通道
// 啟動 Goroutine 處理每個切片:
for chunkNumber := 0; chunkNumber < totalChunks; chunkNumber++ {
wg.Add(1)
semaphore <- struct{}{} // 佔用一個信號量
go func(chunkNum int) {
defer wg.Done()
defer func() { <-semaphore }() // 釋放信號量
offset := int64(chunkNum) * chunkSize
remaining := fileSize - offset
currentChunkSize := chunkSize
if remaining < chunkSize {
currentChunkSize = int(remaining)
}
// 創建切片檔案
chunkFileName := fmt.Sprintf("%s%d", chunkFilePrefix, chunkNum)
chunkFile, err := os.Create(chunkFileName)
if err != nil {
errorCh <- fmt.Errorf("failed to create chunk file %s: %v", chunkFileName, err)
return
}
// 確保檔案被正確關閉
defer func() {
if err := chunkFile.Close(); err != nil {
fmt.Printf("failed to close chunk file %s: %v\n", chunkFileName, err)
}
}()
// 讀取指定區段的資料
buffer := make([]byte, currentChunkSize)
_, err = file.ReadAt(buffer, offset)
if err != nil && err != io.EOF {
errorCh <- fmt.Errorf("failed to read chunk %d: %v", chunkNum, err)
return
}
// 將資料寫入切片檔案
_, err = chunkFile.Write(buffer)
if err != nil {
errorCh <- fmt.Errorf("failed to write to chunk file %s: %v", chunkFileName, err)
return
}
// 上傳切片到伺服器
// 自定義上傳操作(因為程式太長了所以沒加上)
fmt.Printf("Chunk %s uploaded successfully!\n", chunkFileName)
// 刪除切片檔案以節省空間
err = os.Remove(chunkFileName)
if err != nil {
fmt.Printf("Warning: failed to delete chunk file %s: %v\n", chunkFileName, err)
} else {
fmt.Printf("Chunk file %s deleted successfully.\n", chunkFileName)
}
}(chunkNumber)
}
// 等待所有 Goroutine 完成
wg.Wait()
close(errorCh)
// 檢查是否有錯誤發生
var finalErr error
for err := range errorCh {
if finalErr == nil {
finalErr = err
} else {
finalErr = fmt.Errorf("%v; %v", finalErr, err)
}
}
return finalErr
}
func main() {
// 打開大檔案
file, err := os.Open(largeFilePath)
if err != nil {
fmt.Printf("Failed to open file: %v\n", err)
return
}
defer func() {
if err := file.Close(); err != nil {
fmt.Printf("Failed to close file: %v\n", err)
}
}()
// 將檔案切成小塊並進行並發處理
if err := uploadFileInChunks(file, "largefile"); err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Println("All chunks processed successfully!")
}
}
- 透過
file.Stat()
取得檔案大小,並計算總共需要切割的片段數量。semaphore
是一個有緩衝的channel
,用來限制同時運行的 Goroutine 數量。errorCh
用於收集所有 Goroutine 中發生的錯誤。
對每個切片:- 計算切片的讀取偏移量和大小,確保最後一個切片的大小正確,避免讀取超出檔案末尾。
- 讀取檔案的指定區段並寫入切片檔案,使用
ReadAt
方法在指定偏移量讀取資料,避免多個 Goroutine 互相干擾。- 自定義方法將切片上傳到伺服器。
- 上傳成功後,刪除本地的切片檔案(可選),呼叫
os.Remove
刪除該切片檔案。
本文介紹了在 Golang 中使用 io
和 os
套件實現檔案上傳的多種方法。首先,通過基本上傳範例展示了如何接收並保存上傳的檔案。接著,介紹了使用管道(io.Pipe
)進行實時數據處理,適用於大檔案或需要即時處理的場景。最後,深入探討了大型檔案的處理策略,包括將檔案切割成小片段、利用 Goroutine 進行並發上傳,以及在成功上傳後自動刪除切片檔案以節省空間。通過這些方法,開發者可以構建高效、穩定的檔案上傳系統,滿足各種應用需求。
由於一開始我心血來潮介紹的IDE是GoLand,因為太方便了幾乎用內建的功能就能概括所有案例,所以忘記提到 Golang 的原生工具,因此用VSCode開發的可能會有些不明白,先說個Sorry
工具 | 描述 | 使用案例 |
---|---|---|
go build | 將 Go 代碼編譯為可執行檔 | 建立獨立的程式 |
go install | 編譯並安裝 Go 套件 | 讓套件可以在其他專案中使用 |
go clean | 移除編譯過程中的遺留檔案 | 清理專案目錄 |
go fmt | 格式化 Go 代碼以保持一致性 | 維持整潔的程式碼風格 |
go get | 匯入模組相依性 | 管理模組內的依賴 (從 Go 1.22 起) |
go mod download | 下載專案相依性 | 管理編譯所需的依賴 |
go mod tidy | 整理和清理相依性 | 避免相依性衝突 |
go mod verify | 驗證 go.mod 中列出的相依性 | 確保依賴可下載並完整 |
go mod vendor | 建立一個本地 'vendor' 資料夾,複製所有依賴 | 本地存儲所有依賴 |
go run | 直接編譯並執行 Go 代碼 | 快速測試而無需建立可執行檔 |
go test | 執行單元測試 | 自動化代碼測試 |
go vet | 分析 Go 代碼中的問題 | 提早發現潛在錯誤 |
go link | 鏈接套件以建立自定義編譯 | 進階編譯工作流程 |
go doc | 探索 Go 套件文件 | 查詢函式和型態的相關資訊 |
go version | 檢查已安裝的 Go 版本 | 確保相容性 |
主要特點
- 輕鬆編譯:無需處理複雜的專案配置或外部構建系統,
go build
會智能分析你的 Go 代碼及其依賴。- 跨平台能力:通過設置
GOOS
和GOARCH
環境變量,輕鬆生成適用於不同操作系統和架構(如 Linux、Windows、macOS、ARM)的可執行檔。- 自訂化:控制輸出檔名,管理優化的構建標誌,並使用額外選項將靜態資源嵌入二進位檔中。
# 從當前套件構建可執行檔
go build
# 為 Linux AMD64 架構構建
GOOS=linux GOARCH=amd64 go build
使用時機
- 部署:準備獨立運行的 Go 應用程式於目標系統上。
- 分發:讓他人使用你的 Go 軟體,而無需他們擁有 Go 開發環境。
go install 是 go build 的好夥伴,能無縫地編譯並安裝 Go 套件或命令至你的 Go 工作區,並允許你指定版本以管理專案的相容性。
主要特點
- 套件安裝:下載並安裝第三方 Go 套件,讓其代碼在專案中可用。
- 命令創建:編譯用 Go 編寫的命令行工具,並將生成的可執行檔放置在
$GOPATH/bin
目錄中。- 依賴管理:自動處理安裝套件或命令的依賴關係。
- 開發效率:安裝常用的 Go 命令或本地套件,方便存取。
# 安裝 'goreleaser' 套件(發布自動化工具)
go install github.com/goreleaser/goreleaser@latest
# 檢查是否正確安裝
goreleaser --help
使用時機
- 下載外部套件:讓專案輕鬆使用外部 Go 套件。
- 編譯命令行工具:將自製的 Go 命令行工具添加至
$GOPATH/bin
,方便執行。
go clean
是用來清理 Go 專案目錄的工具,能智能地移除建構過程中產生的物件檔、暫存檔及其他雜項檔案。
主要特點
- 工作區清潔:保持專案目錄整潔,便於導航和管理代碼。
- 構建優化:減少編譯器的工作量,潛在提升大型專案的構建時間。
- 全新開始:在重大構建或排除編譯問題前,確保有一個乾淨的環境。
# 清理當前專案目錄
go clean
# 清理並移除整個 Go 構建快取
go clean -cache
# 顯示將要清理的內容,但不實際移除
go clean -n
使用時機
- 定期清理:作為日常開發流程的一部分,保持專案結構整潔。
- 分發前:準備精簡的專案檔案以供分享或部署。
- 排除故障:面對意外的構建錯誤時,使用
go clean
可能解決由於過時的建構工件所引起的問題。
go fmt(以及其伴隨的 gofmt)提供了統一可讀且易於維護的 Go 代碼基礎。它會自動根據官方的 Go 樣式指南重新格式化你的 Go 原始碼。
主要特點
- 統一性:確保所有 Go 代碼無論作者為誰,都遵循一致的空格、縮排和格式。
- 可讀性:使 Go 代碼更易讀、易理解,促進團隊合作。
- 專注邏輯:省去花在代碼樣式上的時間,讓開發者能專注於核心功能。
# 格式化當前目錄中的檔案
go fmt
# 格式化當前目錄及子目錄中的所有 Go 檔案
go fmt ./...
# 檢查檔案是否已格式化,且不進行修改
gofmt -l ./...
使用時機
- 隨時使用:將
go fmt
整合到開發流程中。許多代碼編輯器和 IDE 可自動在保存時執行。- 提交前:考慮將
go fmt
設為提交鉤子,確保團隊內的代碼一致性。- 舊專案:對舊有的 Go 代碼進行格式化,立即提升可讀性。
go get
命令過去用於下載和安裝套件,但自 Go 1.17 起,其功能有所變更:
主要變更
- Go 1.17 以後:安裝可執行檔已被棄用,改用
go install
。- Go 1.18 及更高版本:在使用 Go 模組時,
go get
主要用於管理go.mod
文件中的依賴。- Go 1.22 以後:在使用 Go 模組時,
go get
只管理模組內的依賴,且在GOPATH
模式下已被棄用。
# 下載並更新所有對應的套件
go get
# 下載 'github.com/sirupsen/logrus' 日誌套件
go get github.com/sirupsen/logrus
go mod download
是 Go 模組依賴管理系統的重要組成部分,負責下載專案 go.mod
和 go.sum
文件中定義的依賴版本。
主要特點
- 模組解析:抓取所需的模組及其依賴,並存儲在模組快取中(通常位於
$GOPATH/pkg/mod
)。- 離線構建:依賴下載後,本地即可使用,無需持續的網路連接。
- 版本一致性:與
go.mod
和go.sum
協同工作,確保使用指定的依賴版本進行可預測的構建。
# 下載當前模組的依賴
go mod download
# 下載特定模組的依賴(適用於多模組設置)
go mod download github.com/some/module
使用時機
- 初始化:在初始化新 Go 模組或修改
go.mod
文件後使用。- 構建前:在執行
go build
前,確保所有必要的依賴已就緒。
go mod tidy
是 Go 模組系統中的維護助手,幫助保持專案依賴結構的清潔和一致。
主要特點
- 模組維護:確保
go.mod
文件準確反映實際使用的依賴。- 移除過時依賴:識別並移除
go.mod
和go.sum
文件中未使用的模組條目。- 依賴更新:抓取缺失的模組並更新現有依賴至最低必要版本。
# 整理當前專案的模組依賴
go mod tidy
使用時機
- 定期使用:將
go mod tidy
整合到開發流程中,保持go.mod
文件精簡。- 重大變更後:在添加或移除重要代碼後使用,確保依賴保持同步。
- 排除故障:有時可協助解決模組相關的衝突或不一致問題。
go mod verify
確保專案 go.mod
文件中列出的依賴是有效的、可下載的,並且內容與 go.sum
中的校驗和相符。
主要特點
- 依賴驗證:檢查所需的依賴是否存在於指定版本,並確認其內容與
go.sum
中的校驗和一致。- 完整性保證:減少外部依賴意外變更的風險,確保專案的可重現性。
# 在專案目錄中執行
go mod verify
# 輸出:
# 如果成功: "all modules verified"
# 如果發現問題:顯示指示具體依賴問題的錯誤訊息。
使用時機
- 修改依賴後:每當更改
go.mod
文件中的版本或添加/移除依賴時使用。- 部署前:作為預部署檢查,確保專案將使用正確的依賴進行構建。
- 定期驗證:考慮將其整合到開發流程或 CI/CD 管道中,進行持續的依賴健康檢查。
go mod vendor
在專案目錄中創建一個 vendor
文件夾,包含所有專案所需依賴的本地副本。
主要特點
- 自包含性:使專案獨立於外部模組倉庫。
- 可重現性:確保構建一致,因為專案依賴於本地 vendored(下載的)依賴,而不受外部變更影響。
- 離線開發:允許在無需網路訪問的情況下進行開發。
# 在專案目錄中執行
go mod vendor
使用時機
- 隔離構建:確保專案總是使用相同的依賴,無論外部倉庫有何變更。
- 離線環境:在網路連接有限或無法連接的環境中開發或部署專案。
- 合規性需求:某些開發環境可能限制使用外部模組來源。
go run
提供了一種簡化的方式,在一步操作中編譯並執行 Go 代碼。非常適合快速測試代碼片段、實驗想法或運行小型 Go 腳本。
主要特點
- 隱藏編譯過程:go run 在內存中臨時編譯代碼,無需創建獨立的可執行檔。
- 快速原型:允許快速迭代代碼變更,無需等待完整的 go build 過程。
- 臨時性:不保存輸出二進位檔,適合用於快速任務。
# 運行單一 Go 檔案
go run main.go
# 運行套件中的代碼
go run ./mypackage
使用時機
- 實驗:嘗試 Go 代碼片段或探索套件功能。
- 簡單腳本:執行小型、獨立的 Go 腳本。
- 開發與除錯:在開發階段進行快速測試時使用。
go test
是 Go 中自動化測試的基石,提供內建框架來編寫和執行單元測試,幫助確保代碼的正確性和穩健性。
主要特點
- 測試發現:自動定位 Go 套件中的測試函數(以
Test
開頭,例如TestAdd
)。- 執行與報告:運行測試,提供成功、失敗及其他相關資訊的清晰輸出。
- 測試覆蓋率:可生成代碼覆蓋率報告(
go test -cover
),識別未充分測試的代碼區域。
# 運行當前目錄中的所有測試
go test
# 運行特定套件的測試
go test ./mypackage
# 運行帶有詳細輸出的測試
go test -v
使用時機
- 整個開發過程中:與代碼一起編寫測試,定期執行
go test
提升代碼質量,降低回歸風險。- 持續整合:將
go test
整合到CI/CD
管道中,維持高標準的代碼可靠性。
go vet
是 Go 工具鏈中內建的靜態分析工具,能細緻掃描 Go 代碼,尋找潛在問題、低效率及樣式不一致。
主要特點
- 問題檢測:識別常見問題,如可疑的代碼結構、格式錯誤及潛在的運行時錯誤。
- 早期修正:在開發周期早期解決問題,防止其演變為更棘手的錯誤。
- 自訂化:支持各種檢查,甚至可撰寫自訂分析器。
# 對當前套件執行推薦的 'vet' 檢查
go vet ./...
使用時機
- 開發流程中:將
go vet
整合到開發過程中,主動捕捉錯誤。- 代碼審查前:在分享代碼前執行
go vet
,進行預先的質量檢查。- 持續整合:將其納入 CI/CD 管道,強制執行代碼質量標準。
go link
是 Go 編譯器內部使用的低級工具,提供對連結過程的精細控制,靈活性超越了標準的 Go 構建工作流程。
主要特點
- 手動連結:允許直接連結物件檔和檔案庫,繞過通常的 Go 編譯器自動化。
- 專業構建:可用於創建優化的可執行檔、靜態連結庫或微調最終輸出。
- 內部操作:主要針對開發 Go 工具鏈本身或有特定構建需求的開發者。
# 將物件檔 'main.o' 和 'utils.o' 連結成可執行檔 'myprogram'
go link -o myprogram main.o utils.o
使用時機
- 極少數情況:大多數 Go 開發者不需要直接控制
go link
提供的功能,標準 Go 工具通常能有效處理構建和連結過程。- 工具鏈開發:更可能被那些開發 Go 編譯器或構建系統的人員使用。
- 高級優化:有潛在(但風險較高)的用例,如手動調整性能優化或高度自訂的構建。
go doc
提供了一種方便的方式,直接從終端機存取並閱讀 Go 套件的文件。它會提取並格式化代碼中的註釋,保持文件始終最新。
主要特點
- 隨時獲取文件:無需頻繁切換代碼編輯器和外部網站查看套件參考資料。
- 清晰簡潔:呈現套件、類型、函數、常數等的格式良好且易讀的文件。
- 代碼範例:通常包含嵌入於代碼註釋中的使用範例。
# 顯示 'fmt' 套件的文件
go doc fmt
# 查看 'fmt' 套件中 `Println` 函數的文件
go doc fmt.Println
# 查詢專案中自訂類型的文件
go doc mypackage.MyCustomType
使用時機
- 學習新套件:快速理解你尚未使用過的套件的目的和用法。
- 回憶:詳細查詢特定函數或類型,即使是你自己的代碼中。
- 離線訪問:在無法連接網路時,理想地參考 Go 文件。
- 提示:go doc 也可將輸出作為 HTML,提供可瀏覽的體驗。欲獲得更強大的導航和搜尋功能,考慮使用專用的文件工具,如 godoc.org(或本地運行的 godoc 服務器)。
go version
提供了一種快速查詢系統上安裝的 Go 版本的方法。這對於檢查相容性和故障排除至關重要。
主要特點
- 版本細節:顯示具體的 Go 發行版本(例如 go1.19.5),以及操作系統和架構的詳細資訊。
- 相容性管理:確保代碼在生產環境中運行的 Go 版本與開發和測試時使用的版本一致。
- 故障排除輔助:在尋求幫助或報告錯誤時提供有用的版本資訊。
# 檢查 Go 版本
go version
使用時機
- 專案設置:驗證是否擁有特定專案所需的 Go 版本。
- 共享環境:在向合作者描述設置或尋求支援時,包含
go version
的輸出。- 更新後:確認成功安裝或升級 Go。