一文帶你完整了解 Go 語言 IO 基礎庫
作者 | 百度小程序團隊
導讀 introduction
對於剛接觸 Golang 學習的同學,估計比較難掌握的知識點之一就是文件 IO 處理,光在基礎庫裏會發現 golang 除了 io 包提供文件處理外,os 包,http 包,embed 包都有提供類似的處理,由於 Golang 的繼承表達採用的是隱式的表達, 所以他們之間有什麼關係?能否相互轉換處理?是非常難一眼看出來的。本文就幫大家完整地梳理一下這些 io 相關的基礎庫,希望可以幫到大家。
01 IO 庫
首先來看一下 golang 最基礎的 IO 庫,包名爲 "io", 它包括兩大部分功能:
第一部分定義了最基本的流操作接口,包括 Writer, Reader, Seeker, Closer 這幾個以及相關的組合接口。分別表達寫入,讀取, 偏移讀和關閉操作處理。
全局類圖以及關係如下,方便大家更直觀的理解:
以下對接口進行了源碼摘取並進行中文註釋:
/*
Reader 是包裝基本 Read 方法的接口。
Read 將最多 len(p) 個字節讀取到 p 中。它返回讀取的字節數 (0 <= n <= len(p)) 以及遇到的任何錯誤。即使 Read 返回 n < len(p),它也可能在調用期間使用所有 p 作爲暫存空間。如果某些數據可用,但不是 len(p) 個字節,則 Read 通常會返回可用數據,而不是等待更多數據。
當 Read 在成功讀取 n > 0 字節後遇到錯誤或文件結束條件時,它返回讀取的字節數。它可能會從同一調用返回(非零)錯誤,或從後續調用返回錯誤(且 n == 0)。這種一般情況的一個實例是,在輸入流末尾返回非零字節數的 Reader 可能返回 err == EOF 或 err == nil。下一次讀取應返回 0、EOF。
在考慮錯誤 err 之前,調用者應始終處理返回的 n > 0 字節。這樣做可以正確處理讀取一些字節後發生的 I/O 錯誤以及允許的 EOF 行爲。
如果 len(p) == 0,Read 應始終返回 n == 0。如果已知某些錯誤條件(例如 EOF),則可能會返回非零錯誤。
不鼓勵 Read 的實現返回帶有 nil 錯誤的零字節計數,除非 len(p) == 0 時。調用者應將返回 0 和 nil 視爲表示沒有發生任何事情;特別是它不指示 EOF。
*/
type Writer interface {
Write(p []byte) (n int, err error)
}
/*
Writer 是包裝基本 Write 方法的接口。
Write 將 p 中的 len(p) 個字節寫入底層數據流。它返回從 p (0 <= n <= len(p)) 寫入的字節數以及遇到的導致寫入提前停止的任何錯誤。如果 Write 返回 n < len(p),則必須返回非零錯誤。寫入不得修改切片數據,即使是暫時的。
*/
type Reader interface {
Read(p []byte) (n int, err error)
}
/*
Seeker 是包裝基本 Seek 方法的接口。
Seek 將下一次讀取或寫入的偏移量設置爲 offset,根據從何處解釋:SeekStart表示相對於文件開頭, SeekCurrent表示相對於當前偏移量, SeekEnd表示相對於結尾(例如,offset = - 2 指定文件的倒數第二個字節)。Seek 返回相對於文件開頭的新偏移量或錯誤(如果有)。
尋找文件開始之前的偏移量是錯誤的。可以允許尋求任何正偏移量,但如果新偏移量超過底層對象的大小,則後續 I/O 操作的行爲取決於實現。
*/
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
/*
Closer 是包裝基本 Close 方法的接口。
第一次調用後 Close 的行爲未定義。具體的實現可能會記錄它們自己的行爲。
*/
type Closer interface {
Close() error
}
在基本的接口外, io 庫還提供了一些擴展的讀寫處理能力的接口定義,以提升更便捷的使用:
- 讀相關的定義包括**:**ReaderAt, RuneReader, RuneScanner, ByteReader, ByteScanner, ReaderFrom
全局類圖以及關係如下, 方便大家更直觀的理解:
以下對接口進行了源碼摘取並進行中文註釋:
/*
ReaderAt 是包裝基本 ReadAt 方法的接口。
ReadAt 從底層輸入源中的偏移量 off 處開始將 len(p) 個字節讀取到 p 中。它返回讀取的字節數 (0 <= n <= len(p)) 以及遇到的任何錯誤。
當 ReadAt 返回 n < len(p) 時,它返回一個非零錯誤,解釋爲什麼沒有返回更多字節。在這方面,ReadAt比Read更嚴格。
即使 ReadAt 返回 n < len(p),它也可能在調用期間使用所有 p 作爲暫存空間。如果某些數據可用但不是 len(p) 個字節,則 ReadAt 會阻塞,直到所有數據可用或發生錯誤。在這方面,ReadAt 與 Read 不同。
如果 ReadAt 返回的 n = len(p) 字節位於輸入源的末尾,則 ReadAt 可能返回 err == EOF 或 err == nil。
如果 ReadAt 正在從具有尋道偏移的輸入源讀取,則 ReadAt 不應影響底層尋道偏移,也不會受其影響。
ReadAt 的客戶端可以在同一輸入源上執行並行 ReadAt 調用。
*/
type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}
/*
RuneReader 是包裝 ReadRune 方法的接口。
ReadRune 讀取單個編碼的 Unicode 字符並返回符文及其大小(以字節爲單位)。如果沒有可用的字符,則會設置 err。
*/
type RuneReader interface {
ReadRune() (r rune, size int, err error)
}
/*
ByteReader 是包裝 ReadByte 方法的接口。
ReadByte 讀取並返回輸入中的下一個字節或遇到的任何錯誤。如果 ReadByte 返回錯誤,則表示沒有消耗輸入字節,並且返回的字節值未定義。
ReadByte 爲逐字節處理提供了高效的接口。未實現 ByteReader 的 Reader可以使用 bufio.NewReader 進行包裝以添加此方法。
*/
type ByteReader interface {
ReadByte() (byte, error)
}
/*
ReaderFrom 是包裝 ReadFrom 方法的接口。
ReadFrom 從 r 讀取數據,直到 EOF 或出現錯誤。返回值n是讀取的字節數。讀取期間遇到的除 EOF 之外的任何錯誤也會返回。
Copy函數使用ReaderFrom (如果可用)。
*/
type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}
- 寫相關的定義包括:WriterAt, WriterTo, StringWriter 等
全局類圖以及關係如下, 方便大家更直觀的理解:
以下對接口進行了源碼摘取並進行中文註釋:
/*
WriterAt 是包裝基本 WriteAt 方法的接口。
WriteAt 將 p 中的 len(p) 個字節寫入偏移量爲 off 的基礎數據流。它返回從 p (0 <= n <= len(p)) 寫入的字節數以及遇到的導致寫入提前停止的任何錯誤。如果 WriteAt 返回 n < len(p),則它必須返回非零錯誤。
如果 WriteAt 正在寫入具有查找偏移量的目標,則 WriteAt 不應影響底層查找偏移量,也不會受其影響。
如果範圍不重疊,WriteAt 的客戶端可以在同一目標上執行並行 WriteAt 調用。
實現不得保留 p。
*/
type WriterAt interface {
WriteAt(p []byte, off int64) (n int, err error)
}
/*
WriterTo 是包裝 WriteTo 方法的接口。
WriteTo 將數據寫入 w,直到沒有更多數據可寫入或發生錯誤時。返回值n是寫入的字節數。寫入期間遇到的任何錯誤也會返回。
Copy 函數使用 WriterTo(如果可用)。
*/
type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}
/*
StringWriter 是包裝 WriteString 方法的接口。
*/
type StringWriter interface {
WriteString(s string) (n int, err error)
}
io 庫還提供了很實用的工具方法,整理如下:
Copy:
func Copy(dst Writer, src Reader) (written int64, err error)
△注:將副本從 src 複製到 dst,直到 src 達到 EOF 或發生錯誤。它返回複製的字節數以及複製時遇到的第一個錯誤(如果有)。
成功的 Copy 返回 err == nil,而不是 err == EOF。因爲 Copy 被定義爲從 src 讀取直到 EOF,所以它不會將 Read 中的 EOF 視爲要報告的錯誤。
如果 src 實現 WriterTo,則通過調用 src.WriteTo(dst) 實現複製。否則,如果 dst 實現了 ReaderFrom,則通過調用 dst.ReadFrom(src) 來實現複製。
CopyBuffer:
func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)
△注:CopyBuffer 與 Copy 相同,只是它分階段遍歷提供的緩衝區(如果需要)而不是分配臨時緩衝區。如果 buf 爲 nil,則分配 1;否則,如果它的長度爲零,CopyBuffer 就會出現混亂。
如果 src 實現 WriterTo 或 dst 實現 ReaderFrom,則 buf 將不會用於執行復制。
CopyN:
func CopyN(dst Writer, src Reader, n int64) (written int64, err error)
△注:CopyN 將 n 個字節(或直到出現錯誤)從 src 複製到 dst。它返回複製的字節數以及複製時遇到的最早錯誤。返回時,當且僅當 err == nil 時寫爲 == n。
如果 dst 實現 ReaderFrom,則使用它來實現副本。
Pipe:
func Pipe() (*PipeReader, *PipeWriter)
△注:Pipe 創建同步內存管道。它可用於將需要 io.Reader 的代碼 與需要 io.Writer 的代碼連接起來。
_管道上的讀取和寫入是一對一匹配的,除非需要多個讀取來消耗單個寫入。也就是說,對 PipeWriter 的每次寫入都會阻塞,直到滿足來自 PipeReader 的一次或多次讀取(完全消耗寫入數據)爲止。數據直接從 Write 複製到對應的 Read(或 Reads);沒有內部緩衝。
並行調用 Read 和 Write 或與 Close 一起調用是安全的。對 Read 的並行調用和對 Write 的並行調用也是安全的:各個調用將按順序進行門控。
ReadAll:
func ReadAll(r Reader) ([]byte, error)
△注:ReadAll 從 r 讀取直到出現錯誤或 EOF,然後返回讀取的數據。成功地調用返回 err == nil,而不是 err == EOF。因爲 ReadAll 被定義爲從 src 讀取直到 EOF,所以它不會將 Read 中的 EOF 視爲要報告的錯誤。
ReadAtLeast:
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
△注:ReadAtLeast 從 r 讀取到 buf 中,直到讀取至少 min 個字節。它返回複製的字節數,如果讀取的字節數較少,則返回錯誤。僅當未讀取任何字節時,錯誤才爲 EOF。如果讀取少於 min 字節後發生 EOF,則 ReadAtLeast 返回 ErrUnexpectedEOF。如果 min 大於 buf 的長度,則 ReadAtLeast 返回 ErrShortBuffer。返回時,n >= min 當且僅當 err == nil 時。如果 r 在讀取了至少 min 個字節後返回錯誤,則該錯誤將被丟棄。
ReadFull:
func ReadFull(r Reader, buf []byte) (n int, err error)
△注:ReadFull 將 r 中的 len(buf) 個字節準確讀取到 buf 中。它返回複製的字節數,如果讀取的字節數較少,則返回錯誤。僅當未讀取任何字節時,錯誤才爲 EOF。如果在讀取部分字節但不是全部字節後發生 EOF,則 ReadFull 返回 ErrUnexpectedEOF。返回時,n == len(buf) 當且僅當 err == nil 時。如果 r 在讀取至少 len(buf) 個字節後返回錯誤,則該錯誤將被丟棄。
WriteString:
func WriteString(w Writer , s string ) (n int , err error)
△注:WriteString 將字符串 s 的內容寫入 w,它接受字節切片。如果 w 實現 StringWriter,則直接調用 [StringWriter.WriteString] 。否則,[Writer.Write] 只會被調用一次。
文件操作讀寫示例:
// ReadFileExample 讀取文件內容並輸出
func ReadFileExample() {
// 打開文件,第一個參數是文件路徑,第二個參數是文件打開模式
file, err := os.Open("example.txt")
if err != nil {
fmt.Println("Error:", err)
return
}
defer file.Close() // 延遲關閉文件,確保文件在函數執行完畢後被關閉
// 讀取文件內容
data := make([]byte, 100) // 讀取數據的緩衝區
count, err := file.Read(data)
if err != nil {
fmt.Println("Error:", err)
return
}
// 輸出文件內容
fmt.Printf("Read %d bytes: %s\n", count, data[:count])
}
// WriteFileExample 函數演示如何寫入數據到文件中
func WriteFileExample() {
// 創建文件,第一個參數是文件路徑,如果文件已存在則會被截斷清空
file, err := os.Create("example.txt")
if err != nil {
fmt.Println("Error:", err)
return
}
defer file.Close() // 延遲關閉文件,確保文件在函數執行完畢後被關閉
// 寫入數據到文件
data := []byte("Hello, world!\n")
_, err = file.Write(data)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Data has been written to output.txt")
}
io 庫的第二部分,定義了一個子包 "fs", 定義了文件操作相關的接口,包括 File, FS, DirEntry 等。
全局類圖以及關係如下, 方便大家更直觀的理解:
以下對接口進行了源碼摘取並進行中文註釋:
/*
文件提供對單個文件的訪問。File 接口是文件所需的最低實現。目錄文件還應該實現ReadDirFile。文件可以實現io.ReaderAt或io.Seeker作爲優化。
*/
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}
/*
FileInfo 描述一個文件並由Stat返回。
*/
type FileInfo interface {
Name() string // 文件的基本名稱
Size() int64 // 常規文件的長度(以字節爲單位); 其他系統相關
Mode() FileMode // 文件模式位
ModTime() time . Time // 修改時間
IsDir() bool // Mode().IsDir() 的縮寫
Sys() any // 底層數據源(可以返回 nil)
}
/*
FS 提供對分層文件系統的訪問。
FS接口是文件系統所需的最低實現。文件系統可以實現附加接口,例如ReadFileFS,以提供附加或優化的功能。
testing/fstest.TestFS可用於測試 FS 實現的正確性。
*/
type FS interface {
// Open 打開指定的文件。
//
// 當 Open 返回錯誤時,錯誤類型應爲 *PathError
// Op 字段設置爲“open”,Path 字段設置爲 name,
// 以及 Err 字段描述問題。
//
// Open 應拒絕打開不滿足// ValidPath(name) 的名稱的嘗試,返回 *PathError,並將 Err 設置爲
// ErrInvalid 或 ErrNotExist。
Open(name string) (File, error)
}
/*
DirEntry 是從目錄讀取的條目(使用ReadDir函數或ReadDirFile的 ReadDir 方法)。
*/
type DirEntry interface {
// Name 返回條目描述的文件(或子目錄)的名稱。
// 這個名稱只是路徑的最後一個元素(基本名稱),而不是整個路徑。
// 例如,Name 將返回“hello.go”而不是“home/gopher/hello.go”。
Name() string
// IsDir 報告條目是否描述目錄
IsDir() bool
// Type 返回條目的類型位。
// 類型位是通常 FileMode 位的子集,由 FileMode.Type 方法返回
Type() FileMode
// Info 返回條目描述的文件或子目錄的 FileInfo。
// 返回的 FileInfo 可能來自讀取原始目錄的時間
// 或來自調用 Info. //如果自目錄讀取後文件已被刪除或重命名,Info 可能會返回滿足errors.Is(err, ErrNotExist) 的錯誤。
// 如果條目表示符號鏈接,則 Info 報告有關鏈接本身的信息,
// 而不是鏈接目標的信息。
Info() (FileInfo, error)
}
02 OS 庫
至此 io 庫的部分已經介紹結束,但應該有同學會問, 如何使用這些庫,特別是文件操作?那就要是與 os 庫聯合使用了。 下面也針對 os 庫進行了整理,並給出了相關的示例,方便大家掌握。
全局類圖以及關係如下,方便大家更直觀的理解:
類圖上,可以看到 os 庫下也定義了 File 對象,與 fs.File 接口一樣,有一個 Stat 方法,但返回值變成了 os.FileInfo,但類型是直接使用了 fs.FileInfo。
// A FileInfo describes a file and is returned by Stat and Lstat.
type FileInfo = fs.FileInfo
// A FileMode represents a file's mode and permission bits.
// The bits have the same definition on all systems, so that
// information about files can be moved from one system
// to another portably. Not all bits apply to all systems.
// The only required bit is ModeDir for directories.
type FileMode = fs.FileMode
以下是最簡單的文件操作示例:
file, err := os.Open("example.txt")
if err != nil {
fmt.Println("無法打開文件:", err)
return
}
defer file.Close() // 確保在函數退出時關閉文件
bs := make([]byte, s.Size())
file.Read(bs)
// 打印文件內容
log.Println(string(bs))
以下是最簡單的使用 fs.FS 操作目錄的示例:
root := "/usr/local/go/bin"
fileSystem := os.DirFS(root) // 返回 fs.FS
所以總結來講, os 下的 File 是一個獨立的實現,雖然不是直接實現了 fs.File 接口,但是操作行爲,依賴的操作與 fs 包下的是完全一致的類型。
03 http 包
接下來再來整理一下 http 包下的文件相關的定義:
http 包下也定義了,FileSystem 與 File 對象,進行相應的操作處理, 這一塊的使用也比較好掌握,參見一下下面的示例。
示例:使用 http.FileServer 實現靜態文件服務發佈, 使用了 http.FileSystem
root := "/local/xxx/static"
rootfs := os.DirFS(root) // 返回 fs.FS
fsstatic := http.FileServer(http.FS(rootfs))
// 設置路由
http.Handle("/static/", http.StripPrefix("/static/", fsstatic))
04 embed 包
最後再介紹一下 embed 包。Golang1.16 版本引入的 embed 標準庫, 支持把外部資源文件或目錄直接在編譯階段打進編譯包中,實現了應用打包時只需一個可執行包的效果。
embed 支持把外部資源以 string, []byte 或 embed.FS 方式使用。下面是幾個使用示例:
//go:embed hello.txt
var s string
//go:embed hello.txt
var b []byte
//go:embed hello.txt
var f embed.FS
這裏可以看到 embed 也定義了 FS 對象,用於 FileSystem 的操作處理。
從上面的類圖可以看到,embed.FS 提供的讀取文件內容,打開文件以及讀取文件目錄的功能。
以下對相關的方法進行了源碼摘取並進行中文註釋:
/*
Open 打開指定的文件進行讀取,並將其作爲fs.File返回。
當文件不是目錄時, 返回的文件實現io.Seeker和io.ReaderAt 。
*/
func (f FS) Open(name string) (fs.File, error)
/*
ReadDir 讀取並返回整個命名目錄。
*/
func (f FS) ReadDir(name string) ([]fs.DirEntry, error)
/*
ReadFile 讀取並返回指定文件的內容。
*/
func (f FS) ReadFile(name string) ([]byte, error)
示例代碼:從 embed.FS 讀取文件目錄,發佈成 http 靜態資源服務
package main
import (
"embed"
"log"
"net/http"
)
//go:embed static/*
var staticFiles embed.FS
func main() {
// 創建文件服務器
fileServer := http.FileServer(http.FS(staticFiles))
// 設置路由
http.Handle("/static/", http.StripPrefix("/static/", fileServer))
// 啓動HTTP服務器
log.Println("Server started on: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
05 總結
Go 語言的基礎庫裏,針對文件操作這一塊,各個包都有自己的 File, FS 的定義,這給很多剛開始學習的同學帶來了不少困惑,個人也是覺得設計上是有改進的空間的。希望上述的整理內容,可以幫助到大家更好的理解 Go 語言 IO 庫的使用。
參考資料:
[1]. https://pkg.go.dev/io@go1.22.1 官方 IO 類庫
[2]. https://pkg.go.dev/embed@go1.22.1 官方 Embed 類庫
[3]. https://github.com/jhunters/goassist goassist 開源 go 擴展庫,提供了非常多的工具方法封裝
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/B-PlxS-hRzAdw5ZKd50m6g