一文帶你完整了解Go語言IO基礎(chǔ)庫(kù)
1.IO庫(kù)
首先來看一下golang最基礎(chǔ)的IO庫(kù),包名為"io", 它包括兩大部分功能:
第一部分定義了最基本的流操作接口,包括Writer, Reader, Seeker, Closer這幾個(gè)以及相關(guān)的組合接口。分別表達(dá)寫入,讀取, 偏移讀和關(guān)閉操作處理。
全局類圖以及關(guān)系如下,方便大家更直觀的理解:
圖片
以下對(duì)接口進(jìn)行了源碼摘取并進(jìn)行中文注釋:
/*
Reader 是包裝基本 Read 方法的接口。
Read 將最多 len(p) 個(gè)字節(jié)讀取到 p 中。它返回讀取的字節(jié)數(shù) (0 <= n <= len(p)) 以及遇到的任何錯(cuò)誤。即使 Read 返回 n < len(p),它也可能在調(diào)用期間使用所有 p 作為暫存空間。如果某些數(shù)據(jù)可用,但不是 len(p) 個(gè)字節(jié),則 Read 通常會(huì)返回可用數(shù)據(jù),而不是等待更多數(shù)據(jù)。
當(dāng) Read 在成功讀取 n > 0 字節(jié)后遇到錯(cuò)誤或文件結(jié)束條件時(shí),它返回讀取的字節(jié)數(shù)。它可能會(huì)從同一調(diào)用返回(非零)錯(cuò)誤,或從后續(xù)調(diào)用返回錯(cuò)誤(且 n == 0)。這種一般情況的一個(gè)實(shí)例是,在輸入流末尾返回非零字節(jié)數(shù)的 Reader 可能返回 err == EOF 或 err == nil。下一次讀取應(yīng)返回 0、EOF。
在考慮錯(cuò)誤 err 之前,調(diào)用者應(yīng)始終處理返回的 n > 0 字節(jié)。這樣做可以正確處理讀取一些字節(jié)后發(fā)生的 I/O 錯(cuò)誤以及允許的 EOF 行為。
如果 len(p) == 0,Read 應(yīng)始終返回 n == 0。如果已知某些錯(cuò)誤條件(例如 EOF),則可能會(huì)返回非零錯(cuò)誤。
不鼓勵(lì) Read 的實(shí)現(xiàn)返回帶有 nil 錯(cuò)誤的零字節(jié)計(jì)數(shù),除非 len(p) == 0 時(shí)。調(diào)用者應(yīng)將返回 0 和 nil 視為表示沒有發(fā)生任何事情;特別是它不指示 EOF。
*/
type Writer interface {
Write(p []byte) (n int, err error)
}
/*
Writer 是包裝基本 Write 方法的接口。
Write 將 p 中的 len(p) 個(gè)字節(jié)寫入底層數(shù)據(jù)流。它返回從 p (0 <= n <= len(p)) 寫入的字節(jié)數(shù)以及遇到的導(dǎo)致寫入提前停止的任何錯(cuò)誤。如果 Write 返回 n < len(p),則必須返回非零錯(cuò)誤。寫入不得修改切片數(shù)據(jù),即使是暫時(shí)的。
*/
type Reader interface {
Read(p []byte) (n int, err error)
}
/*
Seeker 是包裝基本 Seek 方法的接口。
Seek 將下一次讀取或?qū)懭氲钠屏吭O(shè)置為 offset,根據(jù)從何處解釋:SeekStart表示相對(duì)于文件開頭, SeekCurrent表示相對(duì)于當(dāng)前偏移量, SeekEnd表示相對(duì)于結(jié)尾(例如,offset = - 2 指定文件的倒數(shù)第二個(gè)字節(jié))。Seek 返回相對(duì)于文件開頭的新偏移量或錯(cuò)誤(如果有)。
尋找文件開始之前的偏移量是錯(cuò)誤的。可以允許尋求任何正偏移量,但如果新偏移量超過底層對(duì)象的大小,則后續(xù) I/O 操作的行為取決于實(shí)現(xiàn)。
*/
type Seeker interface {
Seek(offset int64, whence int) (int64, error)
}
/*
Closer 是包裝基本 Close 方法的接口。
第一次調(diào)用后 Close 的行為未定義。具體的實(shí)現(xiàn)可能會(huì)記錄它們自己的行為。
*/
type Closer interface {
Close() error
}
在基本的接口外, io庫(kù)還提供了一些擴(kuò)展的讀寫處理能力的接口定義,以提升更便捷的使用:
- 讀相關(guān)的定義包括:ReaderAt, RuneReader, RuneScanner, ByteReader, ByteScanner, ReaderFrom
全局類圖以及關(guān)系如下, 方便大家更直觀的理解:
圖片
以下對(duì)接口進(jìn)行了源碼摘取并進(jìn)行中文注釋:
/*
ReaderAt 是包裝基本 ReadAt 方法的接口。
ReadAt 從底層輸入源中的偏移量 off 處開始將 len(p) 個(gè)字節(jié)讀取到 p 中。它返回讀取的字節(jié)數(shù) (0 <= n <= len(p)) 以及遇到的任何錯(cuò)誤。
當(dāng) ReadAt 返回 n < len(p) 時(shí),它返回一個(gè)非零錯(cuò)誤,解釋為什么沒有返回更多字節(jié)。在這方面,ReadAt比Read更嚴(yán)格。
即使 ReadAt 返回 n < len(p),它也可能在調(diào)用期間使用所有 p 作為暫存空間。如果某些數(shù)據(jù)可用但不是 len(p) 個(gè)字節(jié),則 ReadAt 會(huì)阻塞,直到所有數(shù)據(jù)可用或發(fā)生錯(cuò)誤。在這方面,ReadAt 與 Read 不同。
如果 ReadAt 返回的 n = len(p) 字節(jié)位于輸入源的末尾,則 ReadAt 可能返回 err == EOF 或 err == nil。
如果 ReadAt 正在從具有尋道偏移的輸入源讀取,則 ReadAt 不應(yīng)影響底層尋道偏移,也不會(huì)受其影響。
ReadAt 的客戶端可以在同一輸入源上執(zhí)行并行 ReadAt 調(diào)用。
*/
type ReaderAt interface {
ReadAt(p []byte, off int64) (n int, err error)
}
/*
RuneReader 是包裝 ReadRune 方法的接口。
ReadRune 讀取單個(gè)編碼的 Unicode 字符并返回符文及其大?。ㄒ宰止?jié)為單位)。如果沒有可用的字符,則會(huì)設(shè)置 err。
*/
type RuneReader interface {
ReadRune() (r rune, size int, err error)
}
/*
ByteReader 是包裝 ReadByte 方法的接口。
ReadByte 讀取并返回輸入中的下一個(gè)字節(jié)或遇到的任何錯(cuò)誤。如果 ReadByte 返回錯(cuò)誤,則表示沒有消耗輸入字節(jié),并且返回的字節(jié)值未定義。
ReadByte 為逐字節(jié)處理提供了高效的接口。未實(shí)現(xiàn) ByteReader 的 Reader可以使用 bufio.NewReader 進(jìn)行包裝以添加此方法。
*/
type ByteReader interface {
ReadByte() (byte, error)
}
/*
ReaderFrom 是包裝 ReadFrom 方法的接口。
ReadFrom 從 r 讀取數(shù)據(jù),直到 EOF 或出現(xiàn)錯(cuò)誤。返回值n是讀取的字節(jié)數(shù)。讀取期間遇到的除 EOF 之外的任何錯(cuò)誤也會(huì)返回。
Copy函數(shù)使用ReaderFrom (如果可用)。
*/
type ReaderFrom interface {
ReadFrom(r Reader) (n int64, err error)
}
- 寫相關(guān)的定義包括:WriterAt, WriterTo, StringWriter等
全局類圖以及關(guān)系如下, 方便大家更直觀的理解:
圖片
以下對(duì)接口進(jìn)行了源碼摘取并進(jìn)行中文注釋:
/*
WriterAt 是包裝基本 WriteAt 方法的接口。
WriteAt 將 p 中的 len(p) 個(gè)字節(jié)寫入偏移量為 off 的基礎(chǔ)數(shù)據(jù)流。它返回從 p (0 <= n <= len(p)) 寫入的字節(jié)數(shù)以及遇到的導(dǎo)致寫入提前停止的任何錯(cuò)誤。如果 WriteAt 返回 n < len(p),則它必須返回非零錯(cuò)誤。
如果 WriteAt 正在寫入具有查找偏移量的目標(biāo),則 WriteAt 不應(yīng)影響底層查找偏移量,也不會(huì)受其影響。
如果范圍不重疊,WriteAt 的客戶端可以在同一目標(biāo)上執(zhí)行并行 WriteAt 調(diào)用。
實(shí)現(xiàn)不得保留 p。
*/
type WriterAt interface {
WriteAt(p []byte, off int64) (n int, err error)
}
/*
WriterTo 是包裝 WriteTo 方法的接口。
WriteTo 將數(shù)據(jù)寫入 w,直到?jīng)]有更多數(shù)據(jù)可寫入或發(fā)生錯(cuò)誤時(shí)。返回值n是寫入的字節(jié)數(shù)。寫入期間遇到的任何錯(cuò)誤也會(huì)返回。
Copy 函數(shù)使用 WriterTo(如果可用)。
*/
type WriterTo interface {
WriteTo(w Writer) (n int64, err error)
}
/*
StringWriter 是包裝 WriteString 方法的接口。
*/
type StringWriter interface {
WriteString(s string) (n int, err error)
}
io庫(kù)還提供了很實(shí)用的工具方法,整理如下:
Copy:
func Copy(dst Writer, src Reader) (written int64, err error)
成功的 Copy 返回 err == nil,而不是 err == EOF。因?yàn)?Copy 被定義為從 src 讀取直到 EOF,所以它不會(huì)將 Read 中的 EOF 視為要報(bào)告的錯(cuò)誤。
如果 src 實(shí)現(xiàn)WriterTo,則通過調(diào)用 src.WriteTo(dst) 實(shí)現(xiàn)復(fù)制。否則,如果 dst 實(shí)現(xiàn)了ReaderFrom,則通過調(diào)用 dst.ReadFrom(src) 來實(shí)現(xiàn)復(fù)制。
CopyBuffer:
func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)
如果 src 實(shí)現(xiàn)WriterTo或 dst 實(shí)現(xiàn)ReaderFrom,則 buf 將不會(huì)用于執(zhí)行復(fù)制。
CopyN:
func CopyN(dst Writer, src Reader, n int64) (written int64, err error)
如果 dst 實(shí)現(xiàn)ReaderFrom,則使用它來實(shí)現(xiàn)副本。
Pipe:
func Pipe() (*PipeReader, *PipeWriter)
管道上的讀取和寫入是一對(duì)一匹配的,除非需要多個(gè)讀取來消耗單個(gè)寫入。也就是說,對(duì) PipeWriter 的每次寫入都會(huì)阻塞,直到滿足來自PipeReader的一次或多次讀取(完全消耗寫入數(shù)據(jù))為止。數(shù)據(jù)直接從Write復(fù)制到對(duì)應(yīng)的Read(或Reads);沒有內(nèi)部緩沖。
并行調(diào)用 Read 和 Write 或與 Close 一起調(diào)用是安全的。對(duì) Read 的并行調(diào)用和對(duì) Write 的并行調(diào)用也是安全的:各個(gè)調(diào)用將按順序進(jìn)行門控。
ReadAll:
func ReadAll(r Reader) ([]byte, error)
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
ReadFull:
func ReadFull(r Reader, buf []byte) (n int, err error)
WriteString:
func WriteString(w Writer , s string ) (n int , err error)
文件操作讀寫示例:
// ReadFileExample 讀取文件內(nèi)容并輸出
func ReadFileExample() {
// 打開文件,第一個(gè)參數(shù)是文件路徑,第二個(gè)參數(shù)是文件打開模式
file, err := os.Open("example.txt")
if err != nil {
fmt.Println("Error:", err)
return
}
defer file.Close() // 延遲關(guān)閉文件,確保文件在函數(shù)執(zhí)行完畢后被關(guān)閉
// 讀取文件內(nèi)容
data := make([]byte, 100) // 讀取數(shù)據(jù)的緩沖區(qū)
count, err := file.Read(data)
if err != nil {
fmt.Println("Error:", err)
return
}
// 輸出文件內(nèi)容
fmt.Printf("Read %d bytes: %s\n", count, data[:count])
}
// WriteFileExample 函數(shù)演示如何寫入數(shù)據(jù)到文件中
func WriteFileExample() {
// 創(chuàng)建文件,第一個(gè)參數(shù)是文件路徑,如果文件已存在則會(huì)被截?cái)嗲蹇? file, err := os.Create("example.txt")
if err != nil {
fmt.Println("Error:", err)
return
}
defer file.Close() // 延遲關(guān)閉文件,確保文件在函數(shù)執(zhí)行完畢后被關(guān)閉
// 寫入數(shù)據(jù)到文件
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庫(kù)的第二部分,定義了一個(gè)子包"fs", 定義了文件操作相關(guān)的接口,包括 File, FS, DirEntry等。
全局類圖以及關(guān)系如下, 方便大家更直觀的理解:
圖片
以下對(duì)接口進(jìn)行了源碼摘取并進(jìn)行中文注釋:
/*
文件提供對(duì)單個(gè)文件的訪問。File 接口是文件所需的最低實(shí)現(xiàn)。目錄文件還應(yīng)該實(shí)現(xiàn)ReadDirFile。文件可以實(shí)現(xiàn)io.ReaderAt或io.Seeker作為優(yōu)化。
*/
type File interface {
Stat() (FileInfo, error)
Read([]byte) (int, error)
Close() error
}
/*
FileInfo 描述一個(gè)文件并由Stat返回。
*/
type FileInfo interface {
Name() string // 文件的基本名稱
Size() int64 // 常規(guī)文件的長(zhǎng)度(以字節(jié)為單位); 其他系統(tǒng)相關(guān)
Mode() FileMode // 文件模式位
ModTime() time . Time // 修改時(shí)間
IsDir() bool // Mode().IsDir() 的縮寫
Sys() any // 底層數(shù)據(jù)源(可以返回 nil)
}
/*
FS 提供對(duì)分層文件系統(tǒng)的訪問。
FS接口是文件系統(tǒng)所需的最低實(shí)現(xiàn)。文件系統(tǒng)可以實(shí)現(xiàn)附加接口,例如ReadFileFS,以提供附加或優(yōu)化的功能。
testing/fstest.TestFS可用于測(cè)試 FS 實(shí)現(xiàn)的正確性。
*/
type FS interface {
// Open 打開指定的文件。
//
// 當(dāng) Open 返回錯(cuò)誤時(shí),錯(cuò)誤類型應(yīng)為 *PathError
// Op 字段設(shè)置為“open”,Path 字段設(shè)置為 name,
// 以及 Err 字段描述問題。
//
// Open 應(yīng)拒絕打開不滿足// ValidPath(name) 的名稱的嘗試,返回 *PathError,并將 Err 設(shè)置為
// ErrInvalid 或 ErrNotExist。
Open(name string) (File, error)
}
/*
DirEntry 是從目錄讀取的條目(使用ReadDir函數(shù)或ReadDirFile的 ReadDir 方法)。
*/
type DirEntry interface {
// Name 返回條目描述的文件(或子目錄)的名稱。
// 這個(gè)名稱只是路徑的最后一個(gè)元素(基本名稱),而不是整個(gè)路徑。
// 例如,Name 將返回“hello.go”而不是“home/gopher/hello.go”。
Name() string
// IsDir 報(bào)告條目是否描述目錄
IsDir() bool
// Type 返回條目的類型位。
// 類型位是通常 FileMode 位的子集,由 FileMode.Type 方法返回
Type() FileMode
// Info 返回條目描述的文件或子目錄的 FileInfo。
// 返回的 FileInfo 可能來自讀取原始目錄的時(shí)間
// 或來自調(diào)用 Info. //如果自目錄讀取后文件已被刪除或重命名,Info 可能會(huì)返回滿足errors.Is(err, ErrNotExist) 的錯(cuò)誤。
// 如果條目表示符號(hào)鏈接,則 Info 報(bào)告有關(guān)鏈接本身的信息,
// 而不是鏈接目標(biāo)的信息。
Info() (FileInfo, error)
}
2.OS庫(kù)
至此io庫(kù)的部分已經(jīng)介紹結(jié)束,但應(yīng)該有同學(xué)會(huì)問, 如何使用這些庫(kù),特別是文件操作?那就要是與os庫(kù)聯(lián)合使用了。 下面也針對(duì)os庫(kù)進(jìn)行了整理,并給出了相關(guān)的示例,方便大家掌握。
全局類圖以及關(guān)系如下,方便大家更直觀的理解:
圖片
類圖上,可以看到os庫(kù)下也定義了File對(duì)象,與fs.File接口一樣,有一個(gè)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
以下是最簡(jiǎn)單的文件操作示例:
file, err := os.Open("example.txt")
if err != nil {
fmt.Println("無法打開文件:", err)
return
}
defer file.Close() // 確保在函數(shù)退出時(shí)關(guān)閉文件
bs := make([]byte, s.Size())
file.Read(bs)
// 打印文件內(nèi)容
log.Println(string(bs))
以下是最簡(jiǎn)單的使用fs.FS操作目錄的示例:
root := "/usr/local/go/bin"
fileSystem := os.DirFS(root) // 返回 fs.FS
所以總結(jié)來講, os下的File是一個(gè)獨(dú)立的實(shí)現(xiàn),雖然不是直接實(shí)現(xiàn)了fs.File接口,但是操作行為,依賴的操作與fs包下的是完全一致的類型。
GEEK TALK
3.http包
接下來再來整理一下 http包下的文件相關(guān)的定義:
圖片
http包下也定義了,F(xiàn)ileSystem與File對(duì)象,進(jìn)行相應(yīng)的操作處理, 這一塊的使用也比較好掌握,參見一下下面的示例。
示例:使用http.FileServer實(shí)現(xiàn)靜態(tài)文件服務(wù)發(fā)布, 使用了 http.FileSystem
root := "/local/xxx/static"
rootfs := os.DirFS(root) // 返回 fs.FS
fsstatic := http.FileServer(http.FS(rootfs))
// 設(shè)置路由
http.Handle("/static/", http.StripPrefix("/static/", fsstatic))
4.embed包
最后再介紹一下 embed包。Golang1.16 版本引入的embed標(biāo)準(zhǔn)庫(kù), 支持把外部資源文件或目錄直接在編譯階段打進(jìn)編譯包中,實(shí)現(xiàn)了應(yīng)用打包時(shí)只需一個(gè)可執(zhí)行包的效果。
embed支持把外部資源以 string, []byte或embed.FS方式使用。下面是幾個(gè)使用示例:
//go:embed hello.txt
var s string
//go:embed hello.txt
var b []byte
//go:embed hello.txt
var f embed.FS
這里可以看到 embed也定義了 FS對(duì)象,用于FileSystem的操作處理。
圖片
從上面的類圖可以看到,embed.FS提供的讀取文件內(nèi)容,打開文件以及讀取文件目錄的功能。
以下對(duì)相關(guān)的方法進(jìn)行了源碼摘取并進(jìn)行中文注釋:
/*
Open 打開指定的文件進(jìn)行讀取,并將其作為fs.File返回。
當(dāng)文件不是目錄時(shí), 返回的文件實(shí)現(xiàn)io.Seeker和io.ReaderAt 。
*/
func (f FS) Open(name string) (fs.File, error)
/*
ReadDir 讀取并返回整個(gè)命名目錄。
*/
func (f FS) ReadDir(name string) ([]fs.DirEntry, error)
/*
ReadFile 讀取并返回指定文件的內(nèi)容。
*/
func (f FS) ReadFile(name string) ([]byte, error)
示例代碼:從embed.FS讀取文件目錄,發(fā)布成http靜態(tài)資源服務(wù)
package main
import (
"embed"
"log"
"net/http"
)
//go:embed static/*
var staticFiles embed.FS
func main() {
// 創(chuàng)建文件服務(wù)器
fileServer := http.FileServer(http.FS(staticFiles))
// 設(shè)置路由
http.Handle("/static/", http.StripPrefix("/static/", fileServer))
// 啟動(dòng)HTTP服務(wù)器
log.Println("Server started on: http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
GEEK TALK
5.總結(jié)
Go語言的基礎(chǔ)庫(kù)里,針對(duì)文件操作這一塊,各個(gè)包都有自己的File, FS的定義,這給很多剛開始學(xué)習(xí)的同學(xué)帶來了不少困惑,個(gè)人也是覺得設(shè)計(jì)上是有改進(jìn)的空間的。希望上述的整理內(nèi)容,可以幫助到大家更好的理解Go語言IO庫(kù)的使用。