看完這篇文章,你就會知道 Go 中 Buffer 到底有什麼用

作者: 金刀大菜牙

https://juejin.cn/post/7229193250903507004

作爲一種常見的數據結構,緩衝區(Buffer)在計算機科學中有着廣泛的應用。Go 語言標準庫中提供了一個名爲 bytes.Buffer 的緩衝區類型,它可以方便地進行字符串操作、IO 操作、二進制數據處理等。本文將詳細介紹 Go 中 Buffer 的用法,從多個方面介紹其特性和應用場景。

1. Buffer 是什麼?

在計算機科學中,緩衝區(Buffer)是一種數據結構,它用於臨時存儲數據,以便稍後進行處理。在 Go 語言中,bytes.Buffer 是一個預定義的類型,用於存儲和操作字節序列。bytes.Buffer 類型提供了很多有用的方法,例如:讀寫字節、字符串、整數和浮點數等。

// 創建一個空的緩衝區
var buf bytes.Buffer

// 向緩衝區寫入字符串
buf.WriteString("Hello, World!")

// 從緩衝區讀取字符串
fmt.Println(buf.String()) // 輸出:Hello, World!

2. 創建緩衝區

要使用 Buffer 類型,我們首先需要創建一個緩衝區。可以通過以下兩種方式來創建一個 Buffer 對象。

2.1 使用 NewBuffer 函數創建

可以使用 bytes 包中的 NewBuffer 函數來創建一個新的緩衝區對象。它的方法如下:

func NewBuffer(buf []byte) *Buffer

其中,buf 參數是可選的,它可以用來指定緩衝區的初始容量。如果不指定該參數,則會創建一個默認容量爲 64 字節的緩衝區。

下面是一個使用 NewBuffer 函數創建緩衝區的示例:

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBufferString("hello world")
    fmt.Println(buf.String()) // 輸出:hello world
}

2.2 使用 bytes.Buffer 結構體創建

另一種創建緩衝區對象的方式是直接聲明一個 bytes.Buffer 類型的變量。這種方式比較簡單,但是需要注意,如果使用這種方式創建的緩衝區沒有被初始化,則其初始容量爲 0,需要在寫入數據之前進行擴容。

下面是一個使用 bytes.Buffer 結構體創建緩衝區的示例:

import (
    "bytes"
    "fmt"
)

func main() {
    var buf bytes.Buffer
    buf.WriteString("hello")
    buf.WriteString(" ")
    buf.WriteString("world")
    fmt.Println(buf.String()) // 輸出:hello world
}

3. 寫入數據

創建好緩衝區之後,我們可以向其中寫入數據。Buffer 類型提供了多種方法來寫入數據,其中最常用的是 Write 方法。它的方法如下:

func (b *Buffer) Write(p []byte) (n int, err error)

其中,p 參數是要寫入緩衝區的字節切片,返回值 n 表示實際寫入的字節數,err 表示寫入過程中可能出現的錯誤。

除了 Write 方法之外,Buffer 類型還提供了一系列其他方法來寫入數據,例如 WriteString、WriteByte、WriteRune 等。這些方法分別用於向緩衝區寫入字符串、單個字節、單個 Unicode 字符等。

下面是一個使用 Write 方法向緩衝區寫入數據的示例:

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBuffer(nil)
    n, err := buf.Write([]byte("hello world"))
    if err != nil {
        fmt.Println("write error:", err)
    }
    fmt.Printf("write %d bytes\n", n) // 輸出:write 11 bytes
    fmt.Println(buf.String()) // 輸出:hello world
}

4. 讀取數據

除了寫入數據之外,我們還可以從緩衝區中讀取數據。Buffer 類型提供了多種方法來讀取數據,其中最常用的是 Read 方法。它的方法如下:

func (b *Buffer) Read(p []byte) (n int, err error)

其中,p 參數是用於存放讀取數據的字節切片,返回值 n 表示實際讀取的字節數,err 表示讀取過程中可能出現的錯誤。

除了 Read 方法之外,Buffer 類型還提供了一系列其他方法來讀取數據,例如 ReadString、ReadByte、ReadRune 等。這些方法分別用於從緩衝區讀取字符串、單個字節、單個 Unicode 字符等。

下面是一個使用 Read 方法從緩衝區讀取數據的示例:

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBufferString("hello world")
    data := make([]byte, 5)
    n, err := buf.Read(data)
    if err != nil {
        fmt.Println("read error:", err)
    }
    fmt.Printf("read %d bytes\n", n) // 輸出:read 5 bytes
    fmt.Println(string(data)) // 輸出:hello
}

5. 截取緩衝區

Buffer 類型提供了 Bytes 方法和 String 方法,用於將緩衝區的內容轉換爲字節切片和字符串。另外,還可以使用 Truncate 方法來截取緩衝區的內容。它的方法如下:

func (b *Buffer) Truncate(n int)

其中,n 參數表示要保留的字節數。如果緩衝區的內容長度超過了 n,則會從尾部開始截取,只保留前面的 n 個字節。如果緩衝區的內容長度不足 n,則不做任何操作。

下面是一個使用 Truncate 方法截取緩衝區的示例:

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBufferString("hello world")
    buf.Truncate(5)
    fmt.Println(buf.String()) // 輸出:hello
}

6. 擴容緩衝區

在寫入數據的過程中,如果緩衝區的容量不夠,就需要進行擴容。Buffer 類型提供了 Grow 方法來擴容緩衝區。它的方法如下:

func (b *Buffer) Grow(n int)

其中,n 參數表示要擴容的字節數。如果 n 小於等於緩衝區的剩餘容量,則不做任何操作。否則,會將緩衝區的容量擴大到原來的 2 倍或者加上 n,取兩者中的較大值。

下面是一個使用 Grow 方法擴容緩衝區的示例:

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBufferString("hello")
    buf.Grow(10)
    fmt.Printf("len=%d, cap=%d\n", buf.Len(), buf.Cap()) // 輸出:len=5, cap=16
}

在上面的示例中,我們創建了一個包含 5 個字節的緩衝區,並使用 Grow 方法將其容量擴大到了 16 字節。由於 16 是大於 5 的最小的 2 的整數次冪,因此擴容後的容量爲 16。

需要注意的是,Buffer 類型並不保證擴容後的緩衝區是連續的,因此在將緩衝區的內容傳遞給需要連續內存的接口時,需要先將緩衝區的內容拷貝到一個新的連續內存中。

7. 重置緩衝區

在有些情況下,我們需要重複使用一個緩衝區。此時,可以使用 Reset 方法將緩衝區清空並重置爲初始狀態。它的方法如下:

func (b *Buffer) Reset()

下面是一個使用 Reset 方法重置緩衝區的示例:

import (
    "bytes"
    "fmt"
)

func main() {
    buf := bytes.NewBufferString("hello")
    fmt.Println(buf.String()) // 輸出:hello
    buf.Reset()
    fmt.Println(buf.String()) // 輸出:
}

在上面的示例中,我們首先創建了一個包含 hello 的緩衝區,並使用 Reset 方法將其重置爲空緩衝區。注意,重置後的緩衝區長度和容量都變爲了 0。

8. 序列化和反序列化

由於 bytes.Buffer 類型支持讀寫操作,它可以用於序列化和反序列化結構體、JSON、XML 等數據格式。這使得 bytes.Buffer 類型在網絡通信和分佈式系統中的應用變得更加便捷。

type Person struct {
    Name string
    Age  int
}

// 將結構體編碼爲 JSON
p := Person{"Alice", 25}
enc := json.NewEncoder(&buf)
enc.Encode(p)
fmt.Println(buf.String()) // 輸出:{"Name":"Alice","Age":25}

// 從 JSON 解碼爲結構體
var p2 Person
dec := json.NewDecoder(&buf)
dec.Decode(&p2)
fmt.Printf("Name: %s, Age: %d\n", p2.Name, p2.Age) // 輸出:Name: Alice, Age: 25

9. Buffer 的應用場景

9.1 網絡通信

在網絡通信中,bytes.Buffer 可以用於存儲和處理 TCP/UDP 數據包、HTTP 請求和響應等數據。例如,我們可以使用 bytes.Buffer 類型來構造 HTTP 請求和響應:

// 構造 HTTP 請求
req := bytes.NewBufferString("GET / HTTP/1.0\r\n\r\n")

// 構造 HTTP 響應
resp := bytes.NewBuffer([]byte("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\nHello, World!"))

9.2 文件操作

在文件操作中,bytes.Buffer 可以用於緩存文件內容,以避免頻繁的磁盤讀寫操作。例如,我們可以使用 bytes.Buffer 類型來讀取和寫入文件:

// 從文件中讀取數據
file, err := os.Open("example.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

var buf bytes.Buffer
_, err = io.Copy(&buf, file)
if err != nil {
    log.Fatal(err)
}

fmt.Println(buf.String())

// 將數據寫入文件
out, err := os.Create("output.txt")
if err != nil {
    log.Fatal(err)
}
defer out.Close()

_, err = io.Copy(out, &buf)
if err != nil {
    log.Fatal(err)
}

9.3 二進制數據處理

在處理二進制數據時,bytes.Buffer 可以用於存儲和操作字節數組。例如,我們可以使用 bytes.Buffer 類型來讀寫字節數組、轉換字節數組的大小端序等操作:

// 讀取字節數組
data := []byte{0x48, 0x65,0x6c, 0x6c, 0x6f}
var buf bytes.Buffer
buf.Write(data)

// 轉換大小端序
var num uint16
binary.Read(&buf, binary.BigEndian, &num)
fmt.Println(num) // 輸出:0x4865

// 寫入字節數組
data2 := []byte{0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21}
buf.Write(data2)
fmt.Println(buf.Bytes()) // 輸出:[72 101 108 108 111 87 111 114 108 100 33]

9.4 字符串拼接

在字符串拼接時,如果直接使用 + 運算符會產生大量的中間變量,影響程序的效率。使用 Buffer 類型可以避免這個問題。

import (
    "bytes"
    "strings"
)

func concatStrings(strs ...string) string {
    var buf bytes.Buffer
    for _, s := range strs {
        buf.WriteString(s)
    }
    return buf.String()
}

func main() {
    s1 := "hello"
    s2 := "world"
    s3 := "!"
    s := concatStrings(s1, s2, s3)
    fmt.Println(s) // 輸出:hello world!
}

在上面的示例中,我們使用 Buffer 類型將多個字符串拼接成一個字符串。由於 Buffer 類型會動態擴容,因此可以避免產生大量的中間變量,提高程序的效率。

9.5 格式化輸出

在輸出格式化的字符串時,我們可以使用 fmt.Sprintf 函數,也可以使用 Buffer 類型。

import (
    "bytes"
    "fmt"
)

func main() {
    var buf bytes.Buffer
    for i := 0; i < 10; i++ {
        fmt.Fprintf(&buf, "%d\n", i)
    }
    fmt.Println(buf.String())
}

在上面的示例中,我們使用 Buffer 類型將 10 個整數格式化爲字符串,並輸出到標準輸出。使用 Buffer 類型可以方便地組織格式化的字符串,同時也可以減少系統調用的次數,提高程序的效率。

9.6 圖像處理

import (
    "bytes"
    "image"
    "image/png"
    "os"
)

func combineImages(images []image.Image) image.Image {
    width := images[0].Bounds().Dx()
    height := images[0].Bounds().Dy() * len(images)
    canvas := image.NewRGBA(image.Rect(0, 0, width, height))
    var y int
    for _, img := range images {
        for i := 0; i < img.Bounds().Dy(); i++ {
            for j := 0; j < img.Bounds().Dx(); j++ {
                canvas.Set(j, y+i, img.At(j, i))
            }
        }
        y += img.Bounds().Dy()
    }
    return canvas
}

func main() {
    images := make([]image.Image, 3)
    for i := 0; i < 3; i++ {
        f, _ := os.Open(fmt.Sprintf("image%d.png", i+1))
        img, _ := png.Decode(f)
        images[i] = img
    }
    combined := combineImages(images)
    f, _ := os.Create("combined.png")
    png.Encode(f, combined)
}

在上面的示例中,我們使用 Buffer 類型緩存多個圖像的像素值,並將它們合成爲一個新的圖像。使用 Buffer 類型可以方便地緩存像素值,同時也可以減少系統調用的次數,提高程序的效率。

10. 總結

在 Go 語言中,bytes.Buffer 類型是一個十分實用的數據類型,它可以用於存儲和操作二進制數據、網絡通信數據、文件數據等。在實際開發中,我們經常會使用 bytes.Buffer 類型來緩存數據、序列化和反序列化數據、處理二進制數據等操作,以提高代碼的可讀性、可維護性和可擴展性。

除了 bytes.Buffer 類型之外,Go 語言中還有 bytes.Reader 和 bytes.Writer 類型,它們都是基於 bytes.Buffer 類型實現的,可以用於讀取和寫入數據,但 bytes.Reader 類型只能讀取數據,而 bytes.Writer 類型只能寫入數據。在實際開發中,我們可以根據不同的需求來選擇不同的類型。

Go 開發大全

參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/o-dn6SXsATb7ebvFODtjNw