Golang String原理剖析
本文最后更新于20 天前,其中的信息可能已经过时,如有错误请留言

在src/builtin/builtin.go文件中,有关于string的描述

// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string

翻译如下:

1、字符串是所有8bit字节的集合,但编码方式不一定是UTF-8

2、字符串可以为empty,但不能为nil,empty就是一个没有任何字符的空串

3、字符串不可以被修改,所以字符串类型的值是不可变的

字符串的本质是一串字符数组,每个字符根据编码方式不同,可能对应1个或多个整数

字符串拼接的方法:

1、strings.Builder

底层存储使用[]byte,可以预分配内存,自动扩容,大量字符串拼接场景下性能最好,在转换为string类型时,不会进行拷贝操作(一般[]byte转换为strings类型需要一次拷贝操作)

func Test(t *testing.T) {
	var q strings.Builder        // 指定q的类型为string.Builder
	q.Grow(100)                  // 为q分配大小为100字节的内存
	q.WriteString("hello world") // 往q中写入字符串"hello world"
	q.WriteString(", sincerely") // 紧接着"hello world",继续往字符串中写入", sincerely"
	fmt.Println(q.String()) // q.String(),将q转换为string类型
}

运行结果:

=== RUN   Test
hello world, sincerely
--- PASS: Test (0.00s)
PASS

2、bytes.Buffer

与strings.Builder相似,使用[]byte作为底层存储,但是bytes.Buffer在转换为字符串时,会重新申请一块内存,而strings.Builder直接将底层的[]byte转换成字符串类型返回,性能次于strings.Builder

3、+操作符

“+”在拼接两个字符串时,会开辟一段新的内存空间,新空间的大小是原来两个字符串的大小之和,所以每拼接一次就要开辟一段空间,性能很差。少量字符串拼接时,最方便,性能也最好,不适合大量的字符串拼接操作。

4、append

可以提前预分配内存,如果不分配,程序也会自动扩容

5、fmt.Sprintf

Sprintf会从临时对象池中获取一个对象,然后进行格式化操作,最后转换为string,释放对象,实现很复杂,性能也很差,不是专门用于字符串拼接的

6、strings.Join

strings.Join性能约等于strings.Builder,在已经字符串slice的时候可以使用,否则构建切片时会有性能损耗,不建议使用

Benchmark性能测试

使用Benchmark测试大量字符串拼接操作情况下,性能情况如下

package main

import (
	"bytes"
	"fmt"
	"strings"
	"testing"
)

var loremIpsum = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Maecenas porttitor congue massa. Fusce posuere, magna sed pulvinar ultricies, purus lectus malesuada libero, sit amet commodo magna eros quis urna."

var strSlice = make([]string, LIMIT)

const LIMIT = 1000

func init() {
	for i := 0; i < LIMIT; i++ {
		strSlice[i] = loremIpsum
	}
}

func BenchmarkConcatenationOperator(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var q string
		for _, v := range strSlice {
			q = q + v
		}
	}
	b.ReportAllocs()
}

func BenchmarkFmtSprint(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var q string
		for _, v := range strSlice {
			q = fmt.Sprintf(q, v)
		}
	}
	b.ReportAllocs()
}

func BenchmarkBytesBuffer(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var q bytes.Buffer
		q.Grow(len(loremIpsum) * len(strSlice))
		for _, v := range strSlice {
			q.WriteString(v)
		}
		_ = q.String()
	}
	b.ReportAllocs()
}

func BenchmarkStringBuilder(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var q strings.Builder
		q.Grow(len(loremIpsum) * len(strSlice))

		for _, v := range strSlice {
			q.WriteString(v)
		}
		_ = q.String()
	}
	b.ReportAllocs()
}

func BenchmarkAppend(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var q []byte
		for _, v := range strSlice {
			q = append(q, v...)
		}
		_ = string(q)
	}
	b.ReportAllocs()
}

func BenchmarkAppendPreAlloc(b *testing.B) {
	for i := 0; i < b.N; i++ {
		q := make([]byte, 0, len(loremIpsum)*len(strSlice))
		for _, v := range strSlice {
			q = append(q, v...)
		}
		_ = string(q)
	}
	b.ReportAllocs()
}

func BenchmarkJoin(b *testing.B) {
	for i := 0; i < b.N; i++ {
		var q string
		q = strings.Join(strSlice, " ")
		_ = q
	}
	b.ReportAllocs()
}

测试结果为:

goos: windows
goarch: amd64
pkg: awesomeProject
cpu: 12th Gen Intel(R) Core(TM) i7-12700H
BenchmarkConcatenationOperator
BenchmarkConcatenationOperator-20    	      99	  11393268 ns/op	106185805 B/op	    1006 allocs/op
BenchmarkFmtSprint
BenchmarkFmtSprint-20                	      14	  73365786 ns/op	212506515 B/op	    4944 allocs/op
BenchmarkBytesBuffer
BenchmarkBytesBuffer-20              	   25534	     47791 ns/op	  425986 B/op	       2 allocs/op
BenchmarkStringBuilder
BenchmarkStringBuilder-20            	   49358	     30250 ns/op	  212992 B/op	       1 allocs/op
BenchmarkAppend
BenchmarkAppend-20                   	    9388	    133586 ns/op	 1119601 B/op	      22 allocs/op
BenchmarkAppendPreAlloc
BenchmarkAppendPreAlloc-20           	   19702	     61435 ns/op	  425987 B/op	       2 allocs/op
BenchmarkJoin
BenchmarkJoin-20                     	   35522	     34449 ns/op	  212993 B/op	       1 allocs/op
PASS

对于大量字符串拼接操作的场景下,性能如下:

string.Builder ≈ string.Join > bytes.buffer > append > “+” > fmt.sprintf

对于append方法,在提前分配内存和不提前分配内存的情况下,性能相差一倍有余,所以尽量还是要避免在拼接字符串过程中的内存分配以提高性能

string面试与分析

1、string的底层数据结构是怎样的

string的底层实现是一个结构类型,包含两个字段,一个是指向字节数组([]byte)的指针,一个是字符串的字节长度。[]byte底层实现也是一个结构类型,包含三个字段,一个是指向字节数组的指针,一个是字节长度,一个是数组容量。

2、字符串可以被修改吗

不可以

3、[]byte与string互相转换会发生拷贝吗

在大多数场景下会发生拷贝,在部分临时只读场景下,可能不会发生拷贝。

举例:

a. 字符串比较:string(ss) == “hello”

b. 字符串拼接:”hello” + string(ss) + “world”

c. 用作查找,比如map的key,val := map[string(ss)]

这些情况都是临时读取使用,不会有一个赋值给另外一个变量,所以没有发生内存拷贝

4、字符串拼接的方式有哪几种,性能如何?

fmt.Sprintf、strings.Builder、bytes.Buffer、“+”、strings.Join、append

性能参考上文

感谢阅读!如有疑问请留言
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇