使用 + 号
使用 + 号拼接字符串的方式,每次拼接都会创建一个新的字符串,然后将原来的字符串复制到新的字符串中,这样会导致大量的内存分配和复制操作,性能较差。
字符串格式化函数 fmt.Sprintf 函数
预分配 bytes.Buffer 缓冲区
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 和 bytes.Buffer 底层都是一个字节数组,但是 bytes.Buffer 在转换字符串的时候,需要重新申请内存空间,而strings.Builder 是直接将底层的 bytes 转换成字符串进行返回
对字符进行截取时指向同一块内存空间
如何避免:
定义一个很长的字符串 s := strings.Repeat("a", 1<<20),
① ② ④ 打印的是同一个地址 ③ 打印的是一个 nil
字符串在赋值的时候不会发生拷贝,只是改变底层的指针指向
原始的字符串 s 即使被重新赋值为空字符串,但是 s2 依然指向原来的字符串,所以原始的地址不会被释放
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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 } |
不管是通过引用赋值,还是值赋值,最终都是指向同一个地址
1 2 3 4 5 6 7 8 9 10 11 12 |
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 的地址
1 2 3 4 5 6 7 8 9 10 11 12 |
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 } |
字符串传递到函数内部,不管是指针传递还是值传递,字符串实际内容在内存中的地址是相同的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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 在栈上的地址,每次函数调用都会不同
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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 } |
1.强转
1 2 3 4 5 6 7 |
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 } |
2.改变首字符,就能改变 s1 的地址
1 2 3 4 5 6 7 |
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 } |
3.使用 StringsBuilder 改变 s1 的地址
1 2 3 4 5 6 7 8 9 10 |
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 不会改变地址
所以 s2 的方法是零拷贝转换
1 2 3 4 5 6 7 8 9 10 11 |
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 } |