這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Go是用於處理位元組的程式設計語言。 無論您有位元組列表,位元組流還是單個位元組,Go都可以輕鬆處理。 從這些簡單的原語,我們構建了我們的抽象和服務。io包是標準庫中最基礎的包之一。 它提供了一組用於處理位元組流的介面和助手。
這篇文章是一系列演練的一部分,可以協助您更好地瞭解標準庫。 雖然官方的文檔提供了大量的資訊,但是在現實世界的環境中還是很難理解庫的意思。 本系列旨在提供如何在每天應用程式中使用標準庫包的上下文。 如果您有任何問題或意見,可以在Twitter上的@benbjohnson與我聯絡。(當然也可以聯絡我,listomebao@gmail.com)
Reading bytes
位元組有兩個最基本的操作,讀和寫。讓我們先看如何讀位元組。
Reader interface
從資料流讀取位元組最基本的結構是Reader介面:
type Reader interface { Read(p []byte) (n int, err error)}
該介面貫穿在整個標準庫中的實現,從網路連接到檔案都是記憶體片的封裝。
讀取器通過將緩衝區p傳遞給Read()方法,以便我們可以重用相同的位元組。 如果Read()返回一個位元組片而不是接受一個參數,那麼讀者將不得不在每個Read()調用上分配一個新的位元組片。 這將對垃圾收集器造成嚴重破壞。
Reader介面的一個問題是它附帶了一些細微的規則。 首先,當流完成時,它返回一個io.EOF錯誤作為使用的正常部分。 這可能會讓初學者感到困惑。 其次,您的緩衝區不能保證填寫。 如果您傳遞8位元組的片段,則可以在0到8個位元組之間的任何地方接收。 處理部分讀取可能是淩亂和容易出錯的。 幸運的是有這些問題的協助函數。
Improving reader guarantees
假設你有一個協議你正在解析,你知道你需要從閱讀器讀取一個8位元組的uint64值。 在這種情況下,最好使用io.ReadFull(),因為你有一個固定的大小讀取:
func ReadFull(r Reader, buf []byte) (n int, err error)
此功能確保您的緩衝區在返回前完全填充資料。 如果您的緩衝區部分讀取,那麼您將收到一個io.ErrUnexpectedEOF。 如果沒有讀取位元組,則返回io.EOF。 這個簡單的保證可以極大地簡化你的代碼。 要讀取8個位元組,您只需要這樣做:
buf := make([]byte, 8)if _, err := io.ReadFull(r, buf); err == io.EOF { return io.ErrUnexpectedEOF} else if err != nil { return err}
還有許多更進階別的解析器,如處理解析特定類型的binary.Read()。 我們將在以後的演練中介紹不同的軟體包。
另一個很有用的函數是ReadAtLeast():
func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)
此函數將讀取附加資料到緩衝區中,如果可用,始終返回最小位元組數。 我沒有發現這個功能的需要,但如果您需要最小化Read()調用,並且您願意緩衝附加資料,我可以看到它是有用的。
Concatenating streams
很多時候,您將遇到需要將多個讀操作組合在一起的執行個體。 您可以使用MultiReader將它們組合成單個讀取器:
func MultiReader(readers ...Reader) Reader
例如,您可能正在發送一個將記憶體中的http頭與磁碟上的資料相結合的HTTP請求體。 許多人會嘗試將標題和檔案複製到記憶體緩衝區中,但速度很慢且使用大量的記憶體。這是一個更簡單的方法:
r := io.MultiReader( bytes.NewReader([]byte("...my header...")), myFile,)http.Post("http://example.com", "application/octet-stream", r)
MultiReader讓http.Post()將兩個讀介面視為一個單獨的串連讀介面。
Duplicating streams
使用讀介面可能遇到的一個問題是,讀取資料後,無法重讀資料。 例如,您的應用程式可能無法解析HTTP請求本文,並且您無法調試問題,因為解析器已經消耗了資料。TeeReader是一個很好的選擇,用於捕獲讀者的資料,而不會干擾讀介面的消費者。
func TeeReader(r Reader, w Writer) Reader
這個函數構造一個新的讀介面,封裝你的讀介面r。 來自新讀介面的任何讀取也將寫入w。 該Writer可以是從記憶體緩衝區到記錄檔,或者STDERR的任何內容。例如,您可以捕獲如下所示的不良請求:
var buf bytes.Bufferbody := io.TeeReader(req.Body, &buf)// ... process body ...if err != nil { // inspect buf return err}
但是,您必須限制要捕獲的請求本文,以免記憶體不足。
Restricting stream length
因為流是無界的,所以在某些情況下可能會導致記憶體或磁碟問題。 最常見的樣本是檔案上傳端點。 端點通常具有大小限制,以防止磁碟被佔滿,但手動執行此操作可能是乏味的。LimitReader通過產生限制讀取的總位元組數的讀介面來提供此功能:
func LimitReader(r Reader, n int64) Reader
LimitReader的一個問題是它不會告訴你你的底層閱讀器是否超過n。 一旦從r讀取n個位元組,它將簡單地返回io.EOF。 您可以使用的一個技巧是將限制設定為n + 1,然後檢查是否在最後讀取超過n個位元組。
Writing bytes
現在我們已經涵蓋了從流中讀取位元組,我們來看看如何將它們寫入流。
Writer interface
Writer介面是Reader的相反操作。 我們提供一個位元組緩衝區來推送到一個流。
type Writer interface { Write(p []byte) (n int, err error)}
一般來說,寫入位元組比閱讀更簡單。 讀者使資料處理複雜化,因為它們允許部分讀取,但部分寫入將始終返回錯誤。
Duplicating writes
有時你會發送寫入多個流。 也許是一個記錄檔或STDERR。 這與TeeReader類似,只是我們想重複寫入,而不是重複讀取。
在這種情況下,MultiWriter派上用場:
func MultiWriter(writers ...Writer) Writer
這個名字有點使人疑惑,因為它和MultiReader是不一樣的邏輯。 MultiReader將幾個讀介面串連成一個,而MultiWriter返回一個寫介面,它將每個寫入複製到多個寫介面。我在單元測試中廣泛使用MultiWriter,我需要斷言服務正在正確記錄:
type MyService struct { LogOutput io.Writer}...var buf bytes.Buffervar s MyServices.LogOutput = io.MultiWriter(&buf, os.Stderr)
使用MultiWriter允許我驗證buf的內容,同時也看到我的終端中的完整日誌輸出進行調試。
Optimizing string writes
標準庫中有很多寫入器具有WriteString()方法,可以通過在將字串轉換為位元組片段時不需要分配來提高寫入效能。 您可以使用io.WriteString()函數來利用此最佳化。這個函數功能簡單。 它首先檢查作者是否實現了WriteString()方法並使用它(如果可用)。 否則,它將返回將字串複製到位元組片並使用Write()方法。
Copying bytes
現在我們可以讀取位元組,我們可以寫入位元組,只有這樣,我們才想把這兩邊插在一起,並在讀介面和寫介面之間複製。
Connecting readers & writers
將讀介面的資料複製到寫介面的最基本方法是Copy()函數:
func Copy(dst Writer, src Reader) (written int64, err error)
此函數使用32KB緩衝區從src讀取,然後寫入dst。 如果在讀取或寫入中出現io.EOF之外的任何錯誤,則複製將被停止,並返回錯誤。Copy()的一個問題是你不能保證最大的位元組數。 例如,您可能希望將記錄檔複製到當前檔案大小。 如果日誌在複製期間繼續增長,那麼最終會出現比預期更多的位元組。 在這種情況下,您可以使用CopyN()函數來指定要寫入的確切位元組數:
func CopyN(dst Writer, src Reader, n int64) (written int64, err error)
Copy()的另一個問題是它需要為每個調用的32KB緩衝區分配一個。 如果您正在執行大量拷貝,那麼可以使用CopyBuffer()來重用自己的緩衝區:
func CopyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error)
我沒有發現Copy()的開銷非常高,所以我個人不使用CopyBuffer()。
Optimizing copy
為避免完全使用中間緩衝區,類型可以實現直接讀寫的介面。 當實現時,Copy()函數將避免中間緩衝區並直接使用這些實現。WriterTo介面適用於要直接寫入資料的類型:
type WriterTo interface { WriteTo(w Writer) (n int64, err error)}
我在BoltDB的Tx.WriteTo()中使用了這一點,允許使用者從事務中快照資料庫。在讀取資料這方面,ReaderFrom允許讀介面直接讀取資料:
type ReaderFrom interface { ReadFrom(r Reader) (n int64, err error)}
Adapting reader & writers
有時你會發現你有一個接受Reader的功能,但是你所有的都是Writer。 也許您需要將資料動態寫入HTTP請求,但http.NewRequest()只接受一個Reader。您可以使用io.Pipe()來反轉寫介面:
func Pipe() (*PipeReader, *PipeWriter)
這為您提供了一個新的讀介面和寫介面。 對新的PipeWriter的任何寫入將轉到PipeReader。我很少直接使用這個功能,但是exec.Cmd使用它來實現Stdin,Stdout和Stderr管道,這在使用命令執行時非常有用。
Closing streams
所有好的事情都必須結束,這在使用位元組流時也不例外。 關閉介面提供關閉流的通用方式:
type Closer interface { Close() error}
對於Closer沒有什麼可說的,因為它很簡單,但是我發現Close()函數總是返回一個錯誤,我的類型可以在需要時實現Closer。 Closer並不總是直接使用,但有時會與其他介面組合使用ReadCloser,WriteCloser和ReadWriteCloser。
Moving around within streams
流通常是從頭到尾的連續位元組流,但是還有一些例外。 例如,一個檔案可以作為一個流來操作,但你也可以跳轉到檔案中的特定位置。提供了Seeker介面,用於在流中跳轉:
type Seeker interface { Seek(offset int64, whence int) (int64, error)}
有三種方式跳躍:從當前位置移動,從一開始移動,從最後移動。 您可以使用whence參數指定移動模式。 offset參數指定要移動的位元組數。如果您在檔案中使用固定長度的塊或者您的檔案包含位移索引,位移可能會很有用。 有時,這些資料存放區在標題中,所以從頭開始是有意義的,但有時這個資料是在預告片中指定的,所以你需要從最後移動。
Optimizing for Data Types
如果你需要的是一個位元組或rune,那麼讀取和寫入塊可能很繁瑣。 Go提供了一些介面,使這更容易。
Working with individual bytes
ByteReader和ByteWriter介面提供了一個用於讀取和寫入單個位元組的簡單介面:
type ByteReader interface { ReadByte() (c byte, err error)}type ByteWriter interface { WriteByte(c byte) error}
您會注意到沒有長度的參數,因為長度將始終為0或1.如果一個位元組未被讀取或寫入,則返回錯誤。ByteScanner介面還提供用於處理緩衝位元組讀取器:
type ByteScanner interface { ByteReader UnreadByte() error}
這允許您將先前讀取的位元組推回讀取器,以便下次讀取。 這在編寫LL(1)解析器時特別有用,因為它允許您窺視下一個可用位元組。
Working with individual runes
如果您正在解析Unicode資料,那麼您需要使用rune而不是單個位元組。 在這種情況下,會使用RuneReader和RuneScanner:
type RuneReader interface { ReadRune() (r rune, size int, err error)}type RuneScanner interface { RuneReader UnreadRune() error}
Conclusion
位元組流對大多數Go程式至關重要。 它們是從網路連接到磁碟上的檔案到使用者從鍵盤輸入的所有內容的介面。 io包為所有這些互動提供了基礎。我們研究了讀取位元組,寫入位元組,複製位元組,最後研究了最佳化這些操作。 這些原語可能看起來很簡單,但它們為所有資料密集型應用程式提供了基礎。 請看看io包,並在應用程式中考慮它的介面。
參考連結
https://medium.com/go-walkthrough/go-walkthrough-io-package-8ac5e95a9fbd