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

 更新时间:2024年12月15日 10:13:06   作者:uccs  
这篇文章主要为大家详细介绍了Go语言中字符串赋值会出现的一些问题以及解决方法,文中的示例代码讲解详细,感兴趣的小伙伴可以参考一下

字符串的拼接方式

使用 +

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

字符串格式化函数 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.Builderbytes.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 位,这样 s2s 的起始地址是一样的

这种解决很容易导致内存泄露,因为字符串 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
}

改变字符串地址的方式

1.强转

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 的地址

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 的地址

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
}

到此这篇关于Go语言中字符串赋值中的问题与解决方法的文章就介绍到这了,更多相关Go字符串赋值内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Golang反射修改变量值的操作代码

    Golang反射修改变量值的操作代码

    这篇文章主要介绍了Golang反射修改变量值,也就是Golang反射三大定律中的前两个,即从interface{}到反射对象和从反射对象到interface{},需要的朋友可以参考下
    2022-12-12
  • Go 为什么不支持可重入锁原理解析

    Go 为什么不支持可重入锁原理解析

    这篇文章主要为大家介绍了Go 为什么不支持可重入锁原理解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • golang 对象深拷贝的常见方式及性能

    golang 对象深拷贝的常见方式及性能

    这篇文章主要介绍了golang 对象深拷贝的常见方式及性能,Go语言中所有赋值操作都是值传递,如果结构中不含指针,则直接赋值就是深度拷贝,文章围绕主题展开更多相关资料,需要的小伙伴可以参考一下
    2022-06-06
  • golang实现一个简单的websocket聊天室功能

    golang实现一个简单的websocket聊天室功能

    这篇文章主要介绍了golang实现一个简单的websocket聊天室功能,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-10-10
  • Golang程序漏洞检测器govulncheck的安装和使用

    Golang程序漏洞检测器govulncheck的安装和使用

    govulncheck 是一个命令行工具,可以帮助 Golang 开发者快速找到项目代码和依赖的模块中的安全漏洞,该工具可以分析源代码和二进制文件,识别代码中对这些漏洞的任何直接或间接调用,本文就给大家介绍一下govulncheck安装和使用,需要的朋友可以参考下
    2023-09-09
  • Golang压缩Jpeg图片和PNG图片的操作

    Golang压缩Jpeg图片和PNG图片的操作

    这篇文章主要介绍了Golang压缩Jpeg图片和PNG图片的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-12-12
  • 关于go语言载入json可能遇到的一个坑

    关于go语言载入json可能遇到的一个坑

    Go 语言从新手到大神,每个人多少都会踩一些坑,那么下面这篇文章主要给大家介绍了关于go语言载入json可能遇到的一个坑,文中通过示例代码介绍的非常详细,对大家具有一定的参考学习价值,需要的朋友们下面来一起看看吧。
    2017-07-07
  • Go 结构体、数组、字典和 json 字符串的相互转换方法

    Go 结构体、数组、字典和 json 字符串的相互转换方法

    今天小编就为大家分享一篇Go 结构体、数组、字典和 json 字符串的相互转换方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-08-08
  • go语言csrf库使用实现原理示例解析

    go语言csrf库使用实现原理示例解析

    这篇文章主要为大家介绍了go语言csrf库使用实现原理示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-10-10
  • Go语言规范context 类型的key用法示例解析

    Go语言规范context 类型的key用法示例解析

    这篇文章主要为大家介绍了Go语言规范context 类型的key用法示例解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08

最新评论