GO语言中string和[]byte的区别及转换

区别

在我们日常的开发中经常需要处理字符串,而在GO语言中,字符串和[]byte是两种不同的类型。

  • 首先来看string的底层定义(src/runtime/string.go):

    type stringStruct struct {
      str unsafe.Pointer
      len int
    }
  • []byte的底层定义(src/runtime/slice.go):

    type slice struct {
      array unsafe.Pointer
      len   int
      cap   int
    }

    二者都包含一个指向底层数组的指针,和底层数组的长度。不同点在于:

  • string是不可变的,一旦创建就不能修改,因此适合用于只读场景;
  • []byte是可变的,可以修改,且包含一个容量信息(cap);

(注:这里就不展开slice的扩容机制了,可以参考网上其他信息)
什么叫string是不可变的呢?举个例子:

s := "hello world"
s[0] = 'H' // 编译错误:cannot assign to s[0]

(注:这里提一嘴go语言中单引号用来表示byte类型,双引号用来表示string类型)
string不可变的含义是不能修改string底层数组的某个元素,但我们可以修改string引用的底层数组:

s := "hello world"
s = "another string"

这时候s的底层数组已经发生了变化,我们创建了一个新的底层数组(another string)并将s的指针指向它。原先的底层数组(hello world)将等待gc回收。

[]byte是可变的,我们可以修改它的元素:

b := []byte{'h', 'e', 'l', 'l', 'o'}
b[0] = 'H'

这时候b的底层数组中第一个元素已经变成了'H'。

转换

在实际使用时,我们可能需要将string与[]byte互相转换,有以下两种常见的方式:

普通转换

string转[]byte:

s := "hello world"
b := []byte(s)

[]byte转string:

b := []byte{'h', 'e', 'l', 'l', 'o'}
s := string(b)

强转换 (有风险 谨慎使用)

  • 在go版本<1.20中
    通过unsafe包和reflect包实现,其主要原理是拿到底层数组的指针,然后转换成[]byte或string。
func String2Bytes(s string) []byte {
    sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
    bh := reflect.SliceHeader{
        Data: sh.Data,
        Len:  sh.Len,
        Cap:  sh.Len,
    }
    return *(*[]byte)(unsafe.Pointer(&bh))
}

func Bytes2String(b []byte) string {
    return *(*string)(unsafe.Pointer(&b))
}

注:refelct包中的SliceHeader和StringHeader是切片、string的运行时表现

type StringHeader struct {
    Data uintptr
    Len  int
}

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}
  • 在go版本>=1.20中 由于安全性问题reflect包中的StringHeader和SliceHeader已被标注为deprecated,建议使用unsafe包来实现转换。
    // Deprecated: Use unsafe.String or unsafe.StringData instead.
    // Deprecated: Use unsafe.Slice or unsafe.SliceData instead.

    func String2Bytes(s string) []byte {
      // StringData获取string的底层数组指针,unsafe.Slice通过指针和长度构建切片
      return unsafe.Slice(unsafe.StringData(s), len(s))
    }
    
    func Bytes2String(b []byte) string {
      // SliceData获取切片的底层数组指针,unsafe.String通过指针和长度构建string
      return unsafe.String(unsafe.SliceData(b), len(b))
    }

    注:强转换可能出现重大问题!!!如下:

    str := "hello world"
    bs := String2Bytes(str)
    bs[0] = 'H'
    // str作为string不可修改,bs作为[]byte可修改,通过强转换二者指向同一个底层数组
    // 修改bs时会出现严重错误,通过 defer + recover 也不能捕获
    /*
    unexpected fault address 0x1dc1f8
    fatal error: fault
    [signal 0xc0000005 code=0x1 addr=0x1dc1f8 pc=0x1a567b]
    */

    两种转换的性能对比

    转换函数:

    func String2Bytes(s string) []byte {
      return unsafe.Slice(unsafe.StringData(s), len(s))
    }
    
    func Bytes2String(b []byte) string {
      return unsafe.String(unsafe.SliceData(b), len(b))
    }
    
    func String2Bytes_basic(s string) []byte {
      return []byte(s)
    }
    
    func Bytes2String_basic(b []byte) string {
      return string(b)
    }

    测试代码:

    func BenchmarkS2B(b *testing.B) {
      str := "hello world hello world hello world hello world "
      for i := 0; i < b.N; i++ {
          _ = String2Bytes(str)
      }
    }
    
    func BenchmarkB2S(b *testing.B) {
      bs := []byte("hello world hello world hello world hello world ")
      for i := 0; i < b.N; i++ {
          _ = Bytes2String(bs)
      }
    }
    
    func BenchmarkS2Bbasic(b *testing.B) {
      str := "hello world hello world hello world hello world "
      for i := 0; i < b.N; i++ {
          _ = String2Bytes_basic(str)
      }
    }
    
    func BenchmarkB2Sbasic(b *testing.B) {
      bs := []byte("hello world hello world hello world hello world ")
      for i := 0; i < b.N; i++ {
          _ = Bytes2String_basic(bs)
      }
    }

    测试结果:

    goos: windows
    goarch: amd64
    pkg: hello/convert
    cpu: AMD Ryzen 7 6800H with Radeon Graphics
    BenchmarkS2B-16                  1000000000             0.3371 ns/op        0 B/op        0 allocs/op
    BenchmarkB2S-16               1000000000             0.5940 ns/op        0 B/op        0 allocs/op
    BenchmarkS2Bbasic-16          48838329              24.82 ns/op         48 B/op        1 allocs/op
    BenchmarkB2Sbasic-16          50250835              22.35 ns/op         48 B/op        1 allocs/op

    显然使用强转换的性能更高,原因在于对于标准转换,无论是从 []byte 转 string 还是 string 转 []byte 都会涉及底层数组的拷贝。而强转换是直接替换指针的指向,从而使得 string 和 []byte 指向同一个底层数组。当数据长度大于 32 个字节时,标准转换需要通过 mallocgc 申请新的内存,之后再进行数据拷贝工作。所以,当转换数据较大时,两者性能差距会愈加明显。

    两种转换方式的选择:

  • 在你不确定安全隐患的条件下,尽量采用标准方式进行数据转换。
  • 当程序对运行性能有高要求,同时满足对数据仅仅只有读操作的条件,且存在频繁转换(例如消息转发场景),可以使用强转换。

CLoud11y
1 声望0 粉丝

« 上一篇
Mysql日志