目 录CONTENT

文章目录
Go

Go 拼接字符串的方式以及性能

Sakura
2024-08-16 / 0 评论 / 0 点赞 / 34 阅读 / 0 字 / 正在检测是否收录...
温馨提示:
部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

1. 拼接字符串的方式

1.1 + 号拼接

因为 Go 中字符串不可更改的,所以 Go 底层每次拼接的时候要重新申请内存空间,将原来的字符串拷贝进去,然后再追加新的字符串。

func TestPlusConcat(t *testing.T) {
		s1 := "FF14"
		s2 := "World"
		s3 := "FF15"
		// go 底层会分配一个内存空间,然后把 s1 和 s2 的内容拷贝进去
		newString := s1 + s2
		// s1 拷贝了两次, s2 也是拷贝了两次, s3 拷贝了一次
		s4 := newString + s3
		fmt.Println(s3)
		fmt.Println(s4)
}

1.2 字符串格式化函数 fmt.Sprintf

fmt.Sprintf 接受一个格式化字符串作为第一个参数,这个字符串包含了占位符(如 %d, %s, %f 等),这些占位符会被后续的参数替换。

func TestSprintfConcat(t *testing.T) {
		userName := "Sakura"
		website := "www.sakurasss.top"
		email := "1808479176@qq.com"
		newString := fmt.Sprintf("用户名:%s\\n网站:%s\\n邮箱:%s", userName, website, email)
		fmt.Println(newString)
}

fmt.Sprintf 主要用于格式化字符串而不是拼接字符串

1.3 Strings.builder

Go 官方也提供了strings.Builder 包来操作字符串。我们采用和上面一样的操作方式,来拼接字符串。

func TestBuilderConcat(t *testing.T) {
		var builder strings.Builder
		// builder也可以采用 Grow 的方式提前分配内存,以减少内存的分配次数
		//builder.Grow(1024)
		builder.WriteString("FF14")
		builder.WriteString("Sakura")
		fmt.Println(builder.Len()) // 10
		s := builder.String()
	
		fmt.Println(s)
}

💡 另外,builder 是不允许进行拷贝的

func TestBuilderConcat(t *testing.T) {
		var builder strings.Builder
		builder.WriteString("Sakura")
	
		builder2 := builder
		builder2.WriteString("S")
		fmt.Println()
}

panic: strings: illegal use of non-zero Builder copied by value [recovered]
	panic: strings: illegal use of non-zero Builder copied by value

💡 strings.Builder 通过一个指针指向实际保存数据的底层 byte 数组,拷贝 strings.Builder 时同时也拷贝了它的的指针, 但是拷贝过来的指针仍然指向之前的底层数组 (等于两者共享了一个底层数组) ,如果此时写入数据,那么被拷贝的 strings.Builder 也会受到影响。

strings.builder 的实现原理很简单,结构如下:

type Builder struct {
    addr *Builder // of receiver, to detect copies by value
    buf  []byte // 1
}

addr字段主要是做 copycheckbuf 字段是一个 byte 类型的切片,这个就是用来存放字符串内容的,提供的 writeString() 方法就是像切片 buf 中追加数据:

func (b *Builder) WriteString(s string) (int, error) {
 b.copyCheck()
 b.buf = append(b.buf, s...)
 return len(s), nil
}

提供的 String 方法就是将 []]byte 转换为 string 类型,这里为了避免内存拷贝的问题,使用了强制转换来避免内存拷贝:

func (b *Builder) String() string {
 return *(*string)(unsafe.Pointer(&b.buf))
}

1.4 bytes.buffer

bytes.Buffer 底层是提供一个缓冲区,实现内容的操作。它的零值是一个随时可用的空字节缓冲区,通过以下方法操作缓冲区:

func (b *Buffer) Write(p []byte) (int, error) 
func (b *Buffer) WriteByte(c byte) error
func (b *Buffer) WriteRune(r rune) (int, error)
func (b *Buffer) WriteString(s string) (int, error)

💡 strings.Builder 和 bytes.Buffer 底层都是一个 []byte,但是 bytes.Buffer 转换字符串时重新申请了内存空间用来存放, 而 strings.Builder 直接将底层的 []byte 转换为字符串返回。

另外,bytes.Buffer 的源代码中写到:

To build strings more efficiently, see the strings.Builder type. (构建字符串更高效的方法是 strings.Builder)

💡 bytes.buffer 实现了io.Writerio.Reader 接口,内部维护了一个切片 buf 以及两个重要的字段 off 和 ptr 来追踪读写位置。

func TestBufferConcat(t *testing.T) {
	var buf bytes.Buffer
	// 向 buffer 中写入数据
	buf.WriteString("FF14")
	buf.WriteString("World")
	// buf.Reset() 用于清空 buffer,之后就读不到了
	
	// 从 buffer 中读取数据
	readDate := make([]byte, 4)
	n, err := buf.Read(readDate)
	if err != nil {
		log.Println(err)
		return
	}
	// 输出读取的数据
	fmt.Println(string(readDate[:n]))

	s := buf.String() // 获取 buffer 中的数据
	fmt.Println(s) // 因为 FF14 已经被读取, 所以这里只会打印 World
}

1.5 strings.join

💡 Join 主要用于现成切片、数组的(毕竟拼接成数组也要时间)

func Join(elems []string, sep string) string
func BenchmarkPlusConcat2(b *testing.B) {
		baseSlice := []string{"FF14", "World", "FF15"}
		for i := 0; i < b.N; i++ {
				strings.Join(baseSlice, "Sakura")
		}
}

strings.join 也是基于 strings.builder 来实现的,内部调用了 b.Grow(n) 方法,这个是进行初步的容量分配,而前面计算的n的长度就是我们要拼接的 slice 的长度,因为我们传入切片长度固定,所以提前进行容量分配可以减少内存分配,很高效。

func Join(elems []string, sep string) string {
		switch len(elems) {
		case 0:
				return ""
		case 1:
				return elems[0]
		}
	
		var n int
		if len(sep) > 0 {
				if len(sep) >= maxInt/(len(elems)-1) {
						panic("strings: Join output length overflow")
				}
				n += len(sep) * (len(elems) - 1)
		}
		for _, elem := range elems {
				if len(elem) > maxInt-n {
						panic("strings: Join output length overflow")
				}
				n += len(elem)
		}
	
		var b Builder
		b.Grow(n)
		b.WriteString(elems[0])
		for _, s := range elems[1:] {
				b.WriteString(sep)
				b.WriteString(s)
		}
		return b.String()
}

1.5 切片 append

func byteConcat(n int, str string) string {
		buf := make([]byte, 0)
		// 如果长度是可预知的,那么创建 []byte 时,我们还可以预分配切片的容量(cap)。
		for i := 0; i < n; i++ {
			buf = append(buf, str...)
		}
		return string(buf)
}

2. 性能测试

go test -bench=Conacat -v -benchmem

const letterBytes = "SakurasssFF14246810GoFlutter"

// 生成随机字符串
func randomString(n int) string {
	// n 表示生成多长的字符串
	b := make([]byte, n)
	for i := range b {
		// 每次循环随机中间随机一个字符,构成字符串
		b[i] = letterBytes[rand.Intn(len(letterBytes))]
	}
	return string(b)
}

func plusConcat(n int, str string) string {
	s := ""
	for i := 0; i < n; i++ {
		s += str
	}
	return s
}

func sprintfConcat(n int, str string) string {
	s := ""
	for i := 0; i < n; i++ {
		s = fmt.Sprintf("%s%s", s, str)
	}
	return s
}

func joinConcat(n int, str []string) string {
	newStr := ""
	for i := 0; i < n; i++ {
		newStr = strings.Join(str, "")
	}
	return newStr
}

func builderConcat(n int, str string) string {
	var builder strings.Builder
	for i := 0; i < n; i++ {
		builder.WriteString(str)
	}
	return builder.String()
}

func bufferConcat(n int, s string) string {
	buf := new(bytes.Buffer)
	for i := 0; i < n; i++ {
		buf.WriteString(s)
	}
	return buf.String()
}

func byteConcat(n int, str string) string {
	buf := make([]byte, 0)
	for i := 0; i < n; i++ {
		buf = append(buf, str...)
	}
	return string(buf)
}

func preByteConcat(n int, str string) string {
	buf := make([]byte, 0, n*len(str))
	for i := 0; i < n; i++ {
		buf = append(buf, str...)
	}
	return string(buf)
}

func BenchmarkPlusConcat(b *testing.B) {
	s := randomString(10)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		plusConcat(10000, s)
	}
}
func BenchmarkSprintfConcat(b *testing.B) {
	s := randomString(10)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		sprintfConcat(10000, s)
	}
}

func BenchmarkBuilderConcat(b *testing.B) {
	s := randomString(10)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		builderConcat(10000, s)
	}
}

func BenchmarkJoinConcat(b *testing.B) {
	s := randomString(10)
	strs := []string{s}
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		joinConcat(10000, strs)
	}
}

func BenchmarkBufferConcat(b *testing.B) {
	s := randomString(10)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		bufferConcat(10000, s)
	}
}
func BenchmarkByteConcat(b *testing.B) {
	s := randomString(10)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		byteConcat(10000, s)
	}
}
func BenchmarkPreByteConcat(b *testing.B) {
	s := randomString(10)
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		preByteConcat(10000, s)
	}
}

本文引用:

Go 高性能之字符串拼接 | Go 语言必知必会 (dbwu.tech) Go 字符串拼接6种,最快的方式 -- strings.builder - 技术颜良 - 博客园 (cnblogs.com) 字符串拼接性能及原理 | Go 语言高性能编程 | 极客兔兔 (geektutu.com)

0

评论区