前面提到過,Go原生支持通過+/+=操作符來連接多個字符串以構(gòu)造一個更長的字符串,并且通過+/+=操作符的字符串連接構(gòu)造是最自然、開發(fā)體驗最好的一種。
但Go還提供了其他一些構(gòu)造字符串的方法,比如:
● 使用fmt.Sprintf;
● 使用strings.Join;
● 使用strings.Builder;
● 使用bytes.Buffer。
在這些方法中哪種方法最為高效呢?我們使用基準(zhǔn)測試的數(shù)據(jù)作為參考:
var sl []string = []string{
"Rob Pike ",
"Robert Griesemer ",
"Ken Thompson ",
}
func concatStringByOperator(sl []string) string {
var s string
for _, v := range sl {
s += v
}
return s
}
func concatStringBySprintf(sl []string) string {
var s string
for _, v := range sl {
s = fmt.Sprintf("%s%s", s, v)
}
return s
}
func concatStringByJoin(sl []string) string {
return strings.Join(sl, "")
}
func concatStringByStringsBuilder(sl []string) string {
var b strings.Builder
for _, v := range sl {
b.WriteString(v)
}
return b.String()
}
func concatStringByStringsBuilderWithInitSize(sl []string) string {
var b strings.Builder
b.Grow(64)
for _, v := range sl {
b.WriteString(v)
}
return b.String()
}
func concatStringByBytesBuffer(sl []string) string {
var b bytes.Buffer
for _, v := range sl {
b.WriteString(v)
}
return b.String()
}
func concatStringByBytesBufferWithInitSize(sl []string) string {
buf := make([]byte, 0, 64)
b := bytes.NewBuffer(buf)
for _, v := range sl {
b.WriteString(v)
}
return b.String()
}
func BenchmarkConcatStringByOperator(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByOperator(sl)
}
}
func BenchmarkConcatStringBySprintf(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringBySprintf(sl)
}
}
func BenchmarkConcatStringByJoin(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByJoin(sl)
}
}
func BenchmarkConcatStringByStringsBuilder(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByStringsBuilder(sl)
}
}
func BenchmarkConcatStringByStringsBuilderWithInitSize(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByStringsBuilderWithInitSize(sl)
}
}
func BenchmarkConcatStringByBytesBuffer(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByBytesBuffer(sl)
}
}
func BenchmarkConcatStringByBytesBufferWithInitSize(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByBytesBufferWithInitSize(sl)
}
}
運行該基準(zhǔn)測試:
$go test -bench=. -benchmem ./string_concat_benchmark_test.go
goos: darwin
goarch: amd64
BenchmarkConcatStringByOperator-8 11744653 89.1 ns/op 80 B/op 2 allocs/op
BenchmarkConcatStringBySprintf-8 2792876 420 ns/op 176 B/op 8 allocs/op
BenchmarkConcatStringByJoin-8 22923051 49.1 ns/op 48 B/op 1 allocs/op
BenchmarkConcatStringByStringsBuilder-8 11347185 96.6 ns/op 112 B/op 3 allocs/op
BenchmarkConcatStringByStringsBuilderWithInitSize-8 26315769 42.3 ns/op 64 B/op 1 allocs/op
BenchmarkConcatStringByBytesBuffer-8 14265033 82.6 ns/op 112 B/op 2 allocs/op
BenchmarkConcatStringByBytesBufferWithInitSize-8 24777525 48.1 ns/op 48 B/op 1 allocs/op
PASS
ok command-line-arguments 8.816s
從基準(zhǔn)測試的輸出結(jié)果的第三列,即每操作耗時的數(shù)值來看:
● 做了預(yù)初始化的strings.Builder連接構(gòu)建字符串效率最高;
● 帶有預(yù)初始化的bytes.Buffer和strings.Join這兩種方法效率十分接近,分列二三位;
● 未做預(yù)初始化的strings.Builder、bytes.Buffer和操作符連接在第三檔次;
● fmt.Sprintf性能最差,排在末尾。
由此可以得出一些結(jié)論:
● 在能預(yù)估出最終字符串長度的情況下,使用預(yù)初始化的strings.Builder連接構(gòu)建字符串效率最高;
● strings.Join連接構(gòu)建字符串的平均性能最穩(wěn)定,如果輸入的多個字符串是以[]string承載的,那么strings.Join也是不錯的選擇;
● 使用操作符連接的方式最直觀、最自然,在編譯器知曉欲連接的字符串個數(shù)的情況下,使用此種方式可以得到編譯器的優(yōu)化處理;
● fmt.Sprintf雖然效率不高,但也不是一無是處,如果是由多種不同類型變量來構(gòu)建特定格式的字符串,那么這種方式還是最適合的。
轉(zhuǎn)換
在前面的例子中,我們看到了string到[]rune以及string到[]byte的轉(zhuǎn)換,這兩個轉(zhuǎn)
換也是可逆的,也就是說string和[]rune、[]byte可以雙向轉(zhuǎn)換。下面就是從[]rune或
[]byte反向轉(zhuǎn)換為string的例子:
func main() {
rs := []rune{
0x4E2D,
0x56FD,
0x6B22,
0x8FCE,
0x60A8,
}
s := string(rs)
fmt.Println(s)
sl := []byte{
0xE4, 0xB8, 0xAD,
0xE5, 0x9B, 0xBD,
0xE6, 0xAC, 0xA2,
0xE8, 0xBF, 0x8E,
0xE6, 0x82, 0xA8,
}
s = string(sl)
fmt.Println(s)
}
$go run string_slice_to_string.go
中國歡迎您
中國歡迎您
無論是string轉(zhuǎn)slice還是slice轉(zhuǎn)string,轉(zhuǎn)換都是要付出代價的,這些代價的根源
在于string是不可變的,運行時要為轉(zhuǎn)換后的類型分配新內(nèi)存。我們以byte slice與
string相互轉(zhuǎn)換為例,看看轉(zhuǎn)換過程的內(nèi)存分配情況:
func byteSliceToString() {
sl := []byte{
0xE4, 0xB8, 0xAD,
0xE5, 0x9B, 0xBD,
0xE6, 0xAC, 0xA2,
0xE8, 0xBF, 0x8E,
0xE6, 0x82, 0xA8,
0xEF, 0xBC, 0x8C,
0xE5, 0x8C, 0x97,
0xE4, 0xBA, 0xAC,
0xE6, 0xAC, 0xA2,
0xE8, 0xBF, 0x8E,
0xE6, 0x82, 0xA8,
}
_ = string(sl)
}
func stringToByteSlice() {
s := "中國歡迎您,北京歡迎您"
_ = []byte(s)
}
func main() {
fmt.Println(testing.AllocsPerRun(1, byteSliceToString))
fmt.Println(testing.AllocsPerRun(1, stringToByteSlice))
}
運行這個例子:
$go run string_mallocs_in_convert.go
1
1
我們看到,針對“中國歡迎您,北京歡迎您”這個長度的字符串,在string與byte
slice互轉(zhuǎn)的過程中都要有一次內(nèi)存分配操作。
在Go運行時層面,字符串與rune slice、byte slice相互轉(zhuǎn)換對應(yīng)的函數(shù)如下:
slicebytetostring: []byte -> string
slicerunetostring: []rune -> string
stringtoslicebyte: string -> []byte
stringtoslicerune: string -> []rune
以byte slice為例,看看slicebytetostring和stringtoslicebyte的實現(xiàn):
const tmpStringBufSize = 32
type tmpBuf [tmpStringBufSize]byte
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
b = rawbyteslice(len(s))
}
copy(b, s)
return b
}
func slicebytetostring(buf *tmpBuf, b []byte) (str string) {
l := len(b)
if l == 0 {
return ""
}
// 此處省略一些代碼
if l == 1 {
stringStructOf(&str).str = unsafe.Pointer(&staticbytes[b[0]])
stringStructOf(&str).len = 1
return
}
var p unsafe.Pointer
if buf != nil && len(b) <= len(buf) {
p = unsafe.Pointer(buf)
} else {
p = mallocgc(uintptr(len(b)), nil, false)
}
stringStructOf(&str).str = p
stringStructOf(&str).len = len(b)
memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b)))
return
}
想要更高效地進(jìn)行轉(zhuǎn)換,唯一的方法就是減少甚至避免額外的內(nèi)存分配操作。我們看
到運行時實現(xiàn)轉(zhuǎn)換的函數(shù)中已經(jīng)加入了一些避免每種情況都要分配新內(nèi)存操作的優(yōu)化(如
tmpBuf的復(fù)用)。
slice類型是不可比較的,而string類型是可比較的,因此在日常Go編碼中,我們會經(jīng)
常遇到將slice臨時轉(zhuǎn)換為string的情況。Go編譯器為這樣的場景提供了優(yōu)化。在運行時中
有一個名為slicebytetostringtmp的函數(shù)就是協(xié)助實現(xiàn)這一優(yōu)化的:
func slicebytetostringtmp(b []byte) string {
if raceenabled && len(b) > 0 {
racereadrangepc(unsafe.Pointer(&b[0]),
uintptr(len(b)),
getcallerpc(),
funcPC(slicebytetostringtmp))
}
if msanenabled && len(b) > 0 {
msanread(unsafe.Pointer(&b[0]), uintptr(len(b)))
}
return *(*string)(unsafe.Pointer(&b))
}
該函數(shù)的“秘訣”就在于不為string新開辟一塊內(nèi)存,而是直接使用slice的底層存
儲。當(dāng)然使用這個函數(shù)的前提是:在原slice被修改后,這個string不能再被使用了。因此
這樣的優(yōu)化是針對以下幾個特定場景的。
1)string(b)用在map類型的key中
b := []byte{'k', 'e', 'y'}
m := make(map[string]string)
m[string(b)] = "value"
m[[3]string{string(b), "key1", "key2"}] = "value1"
(2)string(b)用在字符串連接語句中
b := []byte{'t', 'o', 'n', 'y'}
s := "hello " + string(b) + "!"
(3)string(b)用在字符串比較中
s := "tom"
b := []byte{'t', 'o', 'n', 'y'}
if s < string(b) {
...
}
Go編譯器對用在for-range循環(huán)中的string到[]byte的轉(zhuǎn)換也有優(yōu)化處理,它不會為
[]byte進(jìn)行額外的內(nèi)存分配,而是直接使用string的底層數(shù)據(jù)。
看下面的例子:
func convert() {
s := "中國歡迎您,北京歡迎您"
sl := []byte(s)
for _, v := range sl {
_ = v
}
}
func convertWithOptimize() {
s := "中國歡迎您,北京歡迎您"
for _, v := range []byte(s) {
_ = v
}
}
func main() {
fmt.Println(testing.AllocsPerRun(1, convert))
fmt.Println(testing.AllocsPerRun(1, convertWithOptimize))
}
運行這個例子程序:文章來源:http://www.zghlxwxcb.cn/news/detail-703952.html
$go run string_for_range_covert_optimize.go
1
0
從結(jié)果我們看到,convertWithOptimize函數(shù)將string到[]byte的轉(zhuǎn)換放在for-range
循環(huán)中,Go編譯器對其進(jìn)行了優(yōu)化,節(jié)省了一次內(nèi)存分配操作。文章來源地址http://www.zghlxwxcb.cn/news/detail-703952.html
到了這里,關(guān)于go基礎(chǔ)10 -字符串的高效構(gòu)造與轉(zhuǎn)換的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!