字符串的拼接方式
- 使用
+号- 使用
+号拼接字符串的方式,每次拼接都会创建一个新的字符串,然后将原来的字符串复制到新的字符串中,这样会导致大量的内存分配和复制操作,性能较差。
- 使用
- 字符串格式化函数
fmt.Sprintf函数 - 预分配
bytes.Buffer缓冲区func BufferCapResize() { var str = "abcd" var buf bytes.Buffer // 预分配内存 buf.Grow(4 * 10000) // 如果没有这一行,当长度不够了就会扩容 cap := 0 for i := 0; i < 10000; i++ { if buf.Cap() != cap { println("cap:", buf.Cap()) cap = buf.Cap() } buf.WriteString(str) } } - 预分配
strings.Builder构建器 - 预分配
[]byte - 使用
strings.Join函数
strings.Builder 和 bytes.Buffer 底层都是一个字节数组,但是 bytes.Buffer 在转换字符串的时候,需要重新申请内存空间,而strings.Builder 是直接将底层的 bytes 转换成字符串进行返回
string(b.buf[b.off:])直接强转unsafe.String(unsafe.Slice(b.buf), len(b.buf))零拷贝转换
字符串内存泄露
对字符进行截取时指向同一块内存空间
如何避免:
- 将子字符串转换成字节切片,在转成
string - 截取后再前面拼接一个新字符串
- 使用
strings.Builder对新字符串进行重新构造
定义一个很长的字符串 s := strings.Repeat("a", 1<<20),
赋值
① ② ④ 打印的是同一个地址 ③ 打印的是一个 nil
字符串在赋值的时候不会发生拷贝,只是改变底层的指针指向
原始的字符串 s 即使被重新赋值为空字符串,但是 s2 依然指向原来的字符串,所以原始的地址不会被释放
func main() {
ptr := (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Println("s pointer:", unsafe.Pointer(ptr.Data)) // ① 0xc000180000
Assign()
}
func Assign() {
s2 := s
ptr := (*reflect.StringHeader)(unsafe.Pointer(&s2))
fmt.Println("Assign:", unsafe.Pointer(ptr.Data)) // ② 0xc000180000
s := ""
ptr = (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Println("s pointer", unsafe.Pointer(ptr.Data)) // ③ nil
ptr = (*reflect.StringHeader)(unsafe.Pointer(&s2))
fmt.Println("Assign", unsafe.Pointer(ptr.Data)) // ④ 0xc000180000
_ = s2
}
通过引用赋值
不管是通过引用赋值,还是值赋值,最终都是指向同一个地址
func main() {
ptr := (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Println("s pointer:", unsafe.Pointer(ptr.Data)) // ① 0xc000180000
AssignPointer()
}
func AssignPointer() {
s2 := &s // 通过引用赋值
ptr := (*reflect.StringHeader)(unsafe.Pointer(s2))
fmt.Println("AssignPointer:", unsafe.Pointer(ptr.Data)) // ② 0xc000180000
_ = s2
}
字符串截取
s2 是截取 s 字符串的前 20 位,这样 s2 和 s 的起始地址是一样的
这种解决很容易导致内存泄露,因为字符串 s 申请的空间是非常大的,s 在不使用的情况下,也是不会被回收的,因为 s2 指向了 s 的地址
func main() {
ptr := (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Println("s pointer:", unsafe.Pointer(ptr.Data)) // ① 0xc000100000
StringSlice()
}
func StringSlice() {
s2 := s[:20]
ptr := (*reflect.StringHeader)(unsafe.Pointer(&s2))
fmt.Println("StringSlice:", unsafe.Pointer(ptr.Data)) // ② 0xc000100000
_ = s2
}
字符串传递
字符串传递到函数内部,不管是指针传递还是值传递,字符串实际内容在内存中的地址是相同的
func main() {
ptr := (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Println("s pointer:", unsafe.Pointer(ptr.Data)) // ① 0x49b7ba
f1(s)
f2(&s)
}
func f1(s string) string {
ptr := unsafe.StringData(s)
fmt.Println("f1:", ptr) // ② 0x49b7ba
return s
}
func f2(s *string) *string {
ptr := unsafe.StringData(*s)
fmt.Println("f2:", ptr) // ③ 0x49b7ba
return s
}
你可能会发现,网上说传递指针才不会发生拷贝,传递值是会发生拷贝,但为什么现在无论是传递指针还是传递值,字符串都没有发生拷贝
这是因为 &s 打印的是函数参数 s 在栈上的地址,每次函数调用都会不同
func main() {
ptr := (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Println("s pointer:", unsafe.Pointer(ptr.Data)) // ① 0x49b7ba
fmt.Println("s 地址:", &s) // 0x528650
f1(s)
f2(&s)
}
func f1(s string) string {
fmt.Println("f1 s:", &s) // 0xc000014070
ptr := unsafe.StringData(s)
fmt.Println("f1:", ptr) // 0x49b7ba
return s
}
func f2(s *string) *string {
fmt.Println("f2 s:", s) // 0x528650
ptr := unsafe.StringData(*s)
fmt.Println("f2:", ptr) // 0x49b7ba
return s
}
改变字符串地址的方式
- 强转
func StringSlice1(s string) string { fmt.Println("string:", unsafe.StringData(s)) // 0xc000100000 s1 := string([]byte(s[:20])) ptr := unsafe.StringData(s1) fmt.Println("StringSlice1:", ptr) // 0xc0000bc000 return s1 } - 改变首字符,就能改变
s1的地址func StringSlice2(s string) string { fmt.Println("string:", unsafe.StringData(s)) // 0xc000100000 s1 := (" " + s[:20])[1:] ptr := unsafe.StringData(s1) fmt.Println("StringSlice2:", ptr) // 0xc000018199 return s1 } - 使用
StringsBuilder改变s1的地址func StringSliceUseBuilder(s string) string { fmt.Println("string:", unsafe.StringData(s)) // 0xc000100000 var b strings.Builder b.Grow(20) b.WriteString(s[:20]) s1 := b.String() ptr := unsafe.StringData(s1) fmt.Println("StringSliceUseBuilder:", ptr) // 0xc0000b0000 return s1 }
字符切换零拷贝转换
虽然同是切片操作,但是 s1 会改变地址,而 s2 不会改变地址
s1强转为字符串的指针类型s2:先对s进行取指,取指之后将它转成字符串切片指针类型,然后在获取指针的内容
所以 s2 的方法是零拷贝转换
func main() {
ptr := (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Println("s pointer:", unsafe.Pointer(ptr.Data)) // 0xc000180000
}
func stringToBytes() {
s1 := []byte(s)
fmt.Println("s1: ", unsafe.SliceData(s1)) // 0xc000280000
s2 := *(*[]byte)(unsafe.Pointer(&s))
fmt.Println("s2: ", unsafe.SliceData(s2)) // 0xc000180000
}