golang IO包的妙用

golang標準庫對io的抽象非常精巧,各個組件可以隨意組合空繁,可以作為接口設計的典范。這篇文章結合一個實際的例子來和大家分享一下朱庆。

背景

以一個RPC的協(xié)議包來說家厌,每個包有如下結構

type Packet struct {
    TotalSize uint32
    Magic     [4]byte
    Payload   []byte
    Checksum  uint32
}

其中TotalSize是整個包除去TotalSize后的字節(jié)數, Magic是一個固定長度的字串椎工,Payload是包的實際內容饭于,包含業(yè)務邏輯的數據。
Checksum是對MagicPayloadadler32校驗和维蒙。

編碼(encode)

我們使用一個原型為func EncodePacket(w io.Writer, payload []byte) error的函數來把數據打包掰吕,結合encoding/binary我們很容易寫出第一版,演示需要颅痊,錯誤處理方面就簡化處理了殖熟。

var RPC_MAGIC = [4]byte{'p', 'y', 'x', 'i'}

func EncodePacket(w io.Writer, payload []byte) error {
    // len(Magic) + len(Checksum) == 8
    totalsize := uint32(len(payload) + 8)
    // write total size
    binary.Write(w, binary.BigEndian, totalsize)

    // write magic bytes
    binary.Write(w, binary.BigEndian, RPC_MAGIC)

    // write payload
    w.Write(payload)

    // calculate checksum
    var buf bytes.Buffer
    buf.Write(RPC_MAGIC[:])
    buf.Write(payload)
    checksum := adler32.Checksum(buf.Bytes())

    // write checksum
    return binary.Write(w, binary.BigEndian, checksum)
}

在上面的實現(xiàn)中,為了計算checksum斑响,我們使用了一個內存buffer來緩存數據菱属,最后把所有的數據一次性讀出來算checksum钳榨,考慮到計算checksum是一個不斷update地過程,我們應該有方法直接略過內存buffer而計算checksum纽门。

查看hash/adler32我們得知薛耻,我們可以構造一個Hash32的對象,這個對象內嵌了一個Hash的接口赏陵,這個接口的定義如下:

type Hash interface {
    // Write (via the embedded io.Writer interface) adds more data to the running hash.
    // It never returns an error.
    io.Writer

    // Sum appends the current hash to b and returns the resulting slice.
    // It does not change the underlying hash state.
    Sum(b []byte) []byte

    // Reset resets the Hash to its initial state.
    Reset()

    // Size returns the number of bytes Sum will return.
    Size() int

    // BlockSize returns the hash's underlying block size.
    // The Write method must be able to accept any amount
    // of data, but it may operate more efficiently if all writes
    // are a multiple of the block size.
    BlockSize() int
}

這是一個通用的計算hash的接口饼齿,標準庫里面所有計算hash的對象都實現(xiàn)了這個接口,比如md5, crc32等蝙搔。由于Hash實現(xiàn)了io.Writer接口缕溉,因此我們可以把所有要計算的數據像寫入文件一樣寫入到這個對象中,最后調用Sum(nil)就可以得到最終的hash的byte數組吃型。利用這個思路证鸥,第二版可以這樣寫:

func EncodePacket2(w io.Writer, payload []byte) error {
    // len(Magic) + len(Checksum) == 8
    totalsize := uint32(len(RPC_MAGIC) + len(payload) + 4)
    // write total size
    binary.Write(w, binary.BigEndian, totalsize)

    // write magic bytes
    binary.Write(w, binary.BigEndian, RPC_MAGIC)

    // write payload
    w.Write(payload)

    // calculate checksum
    sum := adler32.New()
    sum.Write(RPC_MAGIC[:])
    sum.Write(payload)
    checksum := sum.Sum32()

    // write checksum
    return binary.Write(w, binary.BigEndian, checksum)
}

注意這次的變化,前面寫入TotalSize勤晚,Magic枉层,Payload部分沒有變化,在計算checksum的時候去掉了bytes.Buffer运翼,減少了一次內存申請和拷貝返干。

考慮到sumw都是io.Writer,利用神奇的io.MultiWriter血淌,我們可以這樣寫

func EncodePacket(w io.Writer, payload []byte) error {
    // len(Magic) + len(Checksum) == 8
    totalsize := uint32(len(RPC_MAGIC) + len(payload) + 4)
    // write total size
    binary.Write(w, binary.BigEndian, totalsize)

    sum := adler32.New()
    ww := io.MultiWriter(sum, w)
    // write magic bytes
    binary.Write(ww, binary.BigEndian, RPC_MAGIC)

    // write payload
    ww.Write(payload)

    // calculate checksum
    checksum := sum.Sum32()

    // write checksum
    return binary.Write(w, binary.BigEndian, checksum)
}

注意MultiWriter的使用矩欠,我們把wsum利用MultiWriter綁在了一起創(chuàng)建了一個新的Writer,向這個Writer里面寫入數據就同時向wsum里面都寫入數據悠夯,這樣就完成了發(fā)送數據和計算checksum的同步進行癌淮,而對于binary.Write來說沒有任何區(qū)別,因為它需要的是一個實現(xiàn)了Write方法的對象沦补。

解碼(decode)

基于上面的思想乳蓄,解碼也可以把接收數據和計算checksum一起進行,完整代碼如下

func DecodePacket(r io.Reader) ([]byte, error) {
    var totalsize uint32
    err := binary.Read(r, binary.BigEndian, &totalsize)
    if err != nil {
        return nil, errors.Annotate(err, "read total size")
    }

    // at least len(magic) + len(checksum)
    if totalsize < 8 {
        return nil, errors.Errorf("bad packet. header:%d", totalsize)
    }

    sum := adler32.New()
    rr := io.TeeReader(r, sum)

    var magic [4]byte
    err = binary.Read(rr, binary.BigEndian, &magic)
    if err != nil {
        return nil, errors.Annotate(err, "read magic")
    }
    if magic != RPC_MAGIC {
        return nil, errors.Errorf("bad rpc magic:%v", magic)
    }

    payload := make([]byte, totalsize-8)
    _, err = io.ReadFull(rr, payload)
    if err != nil {
        return nil, errors.Annotate(err, "read payload")
    }

    var checksum uint32
    err = binary.Read(r, binary.BigEndian, &checksum)
    if err != nil {
        return nil, errors.Annotate(err, "read checksum")
    }

    if checksum != sum.Sum32() {
        return nil, errors.Errorf("checkSum error, %d(calc) %d(remote)", sum.Sum32(), checksum)
    }
    return payload, nil
}

上面代碼中夕膀,我們使用了io.TeeReader虚倒,這個函數的原型為func TeeReader(r Reader, w Writer) Reader,它返回一個Reader产舞,這個Reader是參數r的代理魂奥,讀取的數據還是來自r,不過同時把讀取的數據寫入到w里面易猫。

一切皆文件

unix下有一切皆文件的思想耻煤,golang把這個思想貫徹到更遠,因為本質上我們對文件的抽象就是一個可讀可寫的一個對象,也就是實現(xiàn)了io.Writerio.Reader的對象我們都可以稱為文件哈蝇,在上面的例子中無論是EncodePacket還是DecodePacket我們都沒有假定編碼后的數據是發(fā)送到socket棺妓,還是從內存讀取數據解碼,因此我們可以這樣調用EncodePacket

conn, _ := net.Dial("tcp", "127.0.0.1:8000")
EncodePacket(conn, []byte("hello"))

把數據直接發(fā)送到socket炮赦,也可以這樣

conn, _ := net.Dial("tcp", "127.0.0.1:8000")
bufconn := bufio.NewWriter(conn)
EncodePacket(bufconn, []byte("hello"))

對socket加上一個buffer來增加吞吐量怜跑,也可以這樣

conn, _ := net.Dial("tcp", "127.0.0.1:8000")
zip := zlib.NewWriter(conn)
bufconn := bufio.NewWriter(conn)
EncodePacket(bufconn, []byte("hello"))

加上一個zip壓縮,還可以利用加上crypto/aes來個AES加密...

在這個時候眼五,文件已經不再局限于io妆艘,可以是一個內存buffer彤灶,也可以是一個計算hash的對象看幼,甚至是一個計數器,流量限速器幌陕。golang靈活的接口機制為我們提供了無限可能诵姜。

結尾

我一直認為一個好的語言一定有一個設計良好的標準庫,golang的標準庫是作者們多年系統(tǒng)編程的沉淀搏熄,值得我們細細品味

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末棚唆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子心例,更是在濱河造成了極大的恐慌宵凌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件止后,死亡現(xiàn)場離奇詭異瞎惫,居然都是意外死亡,警方通過查閱死者的電腦和手機译株,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門瓜喇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人歉糜,你說我怎么就攤上這事乘寒。” “怎么了匪补?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵伞辛,是天一觀的道長。 經常有香客問我夯缺,道長蚤氏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任喳逛,我火速辦了婚禮瞧捌,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己姐呐,他們只是感情好殿怜,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著曙砂,像睡著了一般头谜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鸠澈,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天柱告,我揣著相機與錄音幽勒,去河邊找鬼苔严。 笑死,一個胖子當著我的面吹牛廉白,可吹牛的內容都是我干的涵妥。 我是一名探鬼主播乖菱,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蓬网!你這毒婦竟也來了窒所?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤帆锋,失蹤者是張志新(化名)和其女友劉穎吵取,沒想到半個月后,有當地人在樹林里發(fā)現(xiàn)了一具尸體锯厢,經...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡皮官,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了哲鸳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片臣疑。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖徙菠,靈堂內的尸體忽然破棺而出讯沈,到底是詐尸還是另有隱情,我是刑警寧澤婿奔,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布缺狠,位于F島的核電站,受9級特大地震影響萍摊,放射性物質發(fā)生泄漏挤茄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一冰木、第九天 我趴在偏房一處隱蔽的房頂上張望穷劈。 院中可真熱鬧笼恰,春花似錦、人聲如沸歇终。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽评凝。三九已至追葡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間奕短,已是汗流浹背宜肉。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留翎碑,地道東北人谬返。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像杈女,于是被迫代替她去往敵國和親朱浴。 傳聞我的和親對象是個殘疾皇子吊圾,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容