Go 字串組合研究

字串的組合對程式設計師來說是最常用到的基本語法之一。在Go裡面有數種字串組合方式,對效能的差異也有很明顯的不同,好的程式設計師必須要對效率特別注重,有時候同樣的邏輯,採用不同方式撰寫,效能差異會很大。

底下將針對字串組合的方式來做比較,希望了解原理之後,大家不要因為偷懶,就採用效率不佳的程式撰寫方式。

Go 提供的字串組合方式

  • +
  • fmt.Sprintf
  • bytes.Buffer
  • strings.Builder

雖然Golang提供了 + 讓你可以直接將字串串街,但因為字串是唯讀的,因此用+會導致大量的string被創建、銷毀與記憶體分配,因此1.10版以前比較好的作法是使用 bytes.Buffer,然後轉成字串,效能會差上很多

bytes.Buffer
package main

import (
    "bytes"
    "fmt"
)

func randString() string {
    // Pretend to return a random string
    return "abc-123-"
}

func main() {
    var b bytes.Buffer

    for i := 0; i < 1000; i++ {
        b.WriteString(randString())
    }
    fmt.Println(b.String())
}

上述做法雖然可以避免+的一些缺點,但最後會有一次將[]byte轉為string的型別轉換動做,此動作會進行一次記憶體alloc與字串內容複製的動作。因此,1.10以後版本就可以改用:strings.Builder來解決上述問題。

package main

import (
    "strings"
    "fmt"
)
func randString() string {
    // Pretend to return a random string
    return "abc-123-"
}

func main() {
    var b strings.Builder

    for i := 0; i < 1000; i++ {
        fmt.Fprint(&b, randString())
    }
    print(b.String())
}

為了解決 bytes.Buffer.String() 的問題,其解法就是採用 unsafe.Pointer 指標,直接將 buf []byte 轉為string,避免記憶體重新分配問題,同時增加 copyCheck 來避免buff over flow 問題。

strings.Builder 相關程式碼
type Builder struct {
    addr *Builder // of receiver, to detect copies by value
    buf  []byte // 1
}

// Write appends the contents of p to b's buffer.
// Write always returns len(p), nil.
func (b *Builder) Write(p []byte) (int, error) {
    b.copyCheck()
    b.buf = append(b.buf, p...) // 2
    return len(p), nil
}

// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))  // 3
}

func (b *Builder) copyCheck() {
    if b.addr == nil {
        // 4
        // This hack works around a failing of Go's escape analysis
        // that was causing b to escape and be heap allocated.
        // See issue 23382.
        // TODO: once issue 7921 is fixed, this should be reverted to
        // just "b.addr = b".
        b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
    } else if b.addr != b {
        panic("strings: illegal use of non-zero Builder copied by value")
    }
}