广告位联系
返回顶部
分享到

Go语言中字符串赋值中的问题与解决方法

Golang 来源:互联网 作者:佚名 发布时间:2024-12-16 22:35:49 人浏览
摘要

字符串的拼接方式 使用+号 使用+号拼接字符串的方式,每次拼接都会创建一个新的字符串,然后将原来的字符串复制到新的字符串中,这样会导致大量的内存分配和复制操作,性能较差。 字符

字符串的拼接方式

使用 + 号

使用 + 号拼接字符串的方式,每次拼接都会创建一个新的字符串,然后将原来的字符串复制到新的字符串中,这样会导致大量的内存分配和复制操作,性能较差。

字符串格式化函数 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 构建器
  • 预分配 []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 依然指向原来的字符串,所以原始的地址不会被释放

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 不会改变地址

  • s1 强转为字符串的指针类型
  • s2:先对 s 进行取指,取指之后将它转成字符串切片指针类型,然后在获取指针的内容

所以 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

}


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 :
相关文章
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计