Golang - 認識 io package 的原理與操作

在 Golang 中,輸入輸出的操作最先理解的是 io package,因為 io package 定義兩個 io.Reader 和 io.Writer 接口,分別用來抽象化輸入輸出的操作,因此認識 io package 是掌握 Go 中輸入輸出的基礎。

最近因為在工作上有用到 io 相關操作,因此來紀錄一下~

io.Reader 接口

查看原始碼可以發現,io.Reader 定義 Reader interface:

1
2
3
type Reader interface {
Read(p []byte) (n int, err error)
}

在接口裡面定義了 Read 行為,這個行為的意思是 Read 會將資料讀進 p,講白了就是緩衝區的意思,並傳回兩個參數,n 代表是讀取到的位元組數,通常在讀取的過程中回傳的 n 都會是 len§,也就是將數據丟到 p 裡面將其塞滿。而如果傳回的 n 不到 len (n),這代表下一次的讀取到了結尾,會將 err 改成 io.EOF 並回傳,以檔案來說的話就是檔案結尾的意思,因此在這之後 n 都會是 0 而 err 會是 io.EOF

在 io 包裡面有定義了有些於 io.EOF 的相關錯誤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// ErrShortWrite means that a write accepted fewer bytes than requested
// but failed to return an explicit error.
var ErrShortWrite = errors.New("short write")

// ErrShortBuffer means that a read required a longer buffer than was provided.
var ErrShortBuffer = errors.New("short buffer")

// EOF is the error returned by Read when no more input is available.
// Functions should return EOF only to signal a graceful end of input.
// If the EOF occurs unexpectedly in a structured data stream,
// the appropriate error is either ErrUnexpectedEOF or some other error
// giving more detail.
var EOF = errors.New("EOF")

// ErrUnexpectedEOF means that EOF was encountered in the
// middle of reading a fixed-size block or data structure.
var ErrUnexpectedEOF = errors.New("unexpected EOF")

// ErrNoProgress is returned by some clients of an io.Reader when
// many calls to Read have failed to return any data or error,
// usually the sign of a broken io.Reader implementation.
var ErrNoProgress = errors.New("multiple Read calls return no data or error")

根據讀取的情況回傳相對應的 err,如果沒有發生錯誤就會回傳 nil。

來看個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
reader := strings.NewReader("Coding is interesting!")
p := make([]byte, 4)

for {
n, err := reader.Read(p)
if err == io.EOF {
fmt.Printf("n: %d, err: %v\n", n, err)
break
}
fmt.Println(n, string(p[:n]))
}
}

透過 strings package 產生一個字串 Reader,該 Reader 有實現 Read 行為,在透過建立一個長度為 4 的 [] byte,透過 reader.Read§ 將字串讀進去 p 裡面。

來看輸出結果:

1
2
3
4
5
6
7
4 Codi
4 ng i
4 s in
4 tere
4 stin
2 g!
n: 0, err: EOF

可以看到前面都是每 4 個字元讀取,最後因為剩下兩個字元,並沒有將 p 塞滿,而在讀取的話就會回傳 io.EOF 的 err,為的就是告知已經讀取到結尾,回傳的 n 一定是 0。

io.Writer 接口

查看原始碼可以發現,io.Writer 定義 Writer interface:

1
2
3
type Writer interface {
Write(p []byte) (n int, err error)
}

裡面定義了 Write 行為,其意思代表會將 p 裡面的數據輸出,會回傳實際謝入的位元組數,n 同樣會是從 0 到不大於 len§ 的整數,通常回傳的 n 如果小於 len§,err 會回傳 io.ErrShortWrite,而不會是 nil。

來看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
reader := strings.NewReader("Coding is interesting!")
writer := os.Stdout
p := make([]byte, 4)

for {
nr, err := reader.Read(p)
if err == io.EOF {
fmt.Printf("n: %d, err: %v\n", nr, err)
break
}
nw, err := writer.Write(p)
if err == io.ErrShortWrite {
fmt.Printf("n: %d, err: %v\n", nw, io.ErrShortWrite)
break
}
fmt.Println()
fmt.Println(nw, string(p))
}

}

透過字串的 Reader,將字串讀進去 p,接著透過 os.Stdout 產生 Writer,該 os.Stdout 實際上就是 * File,有實現 Write 行為。

透過 writer.Write§,將讀進來的字串用成標準輸出,也就是會輸出在 console 上。

運行結果:

1
2
3
4
5
6
7
8
9
10
11
12
13
Codi
4 Codi
ng i
4 ng i
s in
4 s in
tere
4 tere
stin
4 stin
g!in
4 g!in
n: 0, err: EOF

可以看出每次都是 Write p 長度的字串出來。最後當讀到結尾時,reader.Read 會回傳 io.EOF 的錯誤,因此就不需要 writer.Write 繼續輸出。

io.Copy () 介紹

io 套件裡面也實踐了 Copy (),提供功能就是可以將 Reader 裡面的數據 Copy 到另一個 Writer 上。

看個例子:

1
2
3
4
5
6
7
8
9
10
func main() {
reader := strings.NewReader("Coding is interesting!")
writer := os.Stdout
n, err := io.Copy(writer, reader)
if err != nil {
fmt.Println(err)
}
fmt.Println()
fmt.Println(n)
}

透過 strings.NewReader 讀入字串,建立標準輸出的 writer,透過 Copy 的方式將其字串輸出。而 io.Copy 實際上的原始碼其實就是上面示範的 For 迴圈讀取寫入的包裝,並且幫我們處理 err 的問題。因此這個 n 回傳的是 Writer 總共寫入多少字元數,err 有可能會回傳非 io.EOF 的其他錯誤,裡面實現的方式已經幫我們處理 io.EOF 的錯誤,因此這邊 Copy 不會回傳 io.EOF。

io.WriteString () 介紹

這個函數能夠方便地將字串型態寫入一個 Writer 幫我們做輸出。

來看例子:

1
2
3
4
5
6
7
8
9
func main() {
writer := os.Stdout
n , err := io.WriteString(writer, "Hello World")
if err != nil {
fmt.Println(err)
}
fmt.Println()
fmt.Println(n)
}

輸出結果如下:

1
2
Coding is interesting
21

一樣回傳的 n 是寫入的字節數,這邊底層實現其實就是調用傳進去的 writer 其 write () 實現方法而已,因此會回傳的 err 如同前面。

總結

所有 io 操作,一定都是透過去實現 Reader、Writer 去組合而成的,Golang 中提供許多 IO 操作的標準庫,底層都是運用 io package 的行為。

例如有:bufio 套件、bytes 套件、os 套件的 File (裡面就包含 os.Stdout、os.Stdin、os.Stderr,分別代表標準輸入、標準輸出、標準錯誤)。

io package 裡面還有其他函式提供方便操作,這邊就不多講實際上去看文檔就可以了,主要就是要理解 Golang io 操作原理是源自於此 io 包。

最後最後!請聽我一言!

如果你還沒有註冊 Like Coin,你可以在文章最下方看到 Like 的按鈕,點下去後即可申請帳號,透過申請帳號後可以幫我的文章按下 Like,而 Like 最多可以點五次,而你不用付出任何一塊錢,就能給我寫這篇文章的最大的回饋!