[小知识]类型的原地转换

75 阅读5分钟

问题场景

在写代码过程中,总有一些字符串和字节切片互相转换的场景,虽然,这个是一个比较小的转换,但是,但是,仔细看一下,居然也有两种写法

实现方式

常规转换

直接转换,这也是一般的小白的写法

package bytesconv1

// StringToBytes 字符串转字节切片
func StringToBytes(s string) []byte {
   return []byte(s)
}

// BytesToString 字节切片转字符串
func BytesToString(b []byte) string {
   return string(b)
}

原地转换

原地转换,在充分了解到数据类型在内存中的存储后,可以直接给数据“套”上一个类型,然后,就可以用这个类型来解释数据了,优点是显而易见的:没有内存分配,直接原地使用数据

package bytesconv2

import (
   "unsafe"
)

// StringToBytes 字符串转字节切片
func StringToBytes(s string) []byte {
   return *(*[]byte)(unsafe.Pointer(
      &struct {
         string
         Cap int
      }{s, len(s)},
   ))
}

// BytesToString 字节切片转字符串
func BytesToString(b []byte) string {
   return *(*string)(unsafe.Pointer(&b))
}

优异分析

效果: 功能相同

可以针对这个单测,分别跑两种实现,都是OK的,所以,可以说:功能一致

package bytesconv

import (
   "reflect"
   "testing"
)

func TestBytesToString(t *testing.T) {
   type args struct {
      b []byte
   }
   tests := []struct {
      name string
      args args
      want string
   }{
      {"case1", args{b: []byte{72, 101, 108, 108, 111}}, "Hello"},
      {"case2", args{b: []byte{228, 189, 160, 229, 165, 189}}, "你好"},
   }
   for _, tt := range tests {
      t.Run(tt.name, func(t *testing.T) {
         if got := BytesToString(tt.args.b); got != tt.want {
            t.Errorf("BytesToString() = %v, want %v", got, tt.want)
         }
      })
   }
}

func TestStringToBytes(t *testing.T) {
   type args struct {
      s string
   }
   tests := []struct {
      name string
      args args
      want []byte
   }{
      {"case1", args{s: "Hello"}, []byte{72, 101, 108, 108, 111}},
      {"case2", args{s: "你好"}, []byte{228, 189, 160, 229, 165, 189}},
   }
   for _, tt := range tests {
      t.Run(tt.name, func(t *testing.T) {
         if got := StringToBytes(tt.args.s); !reflect.DeepEqual(got, tt.want) {
            t.Errorf("StringToBytes() = %v, want %v", got, tt.want)
         }
      })
   }
}

跑的结果

image

性能: 优化效果明显

写一个比较简陋的压测,可以在数据的制造上,有更多改进,我这里就不多改造了

func BenchmarkStringToBytes(b *testing.B) {
   a := "大撒上大叔大叔大叔大叔"
   for i := 0; i < b.N; i++ {
      StringToBytes(a)
   }
}

func BenchmarkBytesToString(b *testing.B) {
   a := []byte{228, 189, 160, 229, 165, 189}
   for i := 0; i < b.N; i++ {
      BytesToString(a)
   }
}

压测结果

## StringToBytes:第一行为原地转换
BenchmarkStringToBytes-8    1000000000           0.2966 ns/op          0 B/op          0 allocs/op
BenchmarkStringToBytes-8    28809170             39.14 ns/op          48 B/op          1 allocs/op

## BytesToString: 第一行为原地转换
BenchmarkBytesToString-8           1000000000                 0.3390 ns/op               0 B/op               0 allocs/op
BenchmarkBytesToString-8           301927347                   3.677 ns/op               0 B/op               0 allocs/op
  • 原地转换可以在执行速度和内存的分配上,完全吊打常规的转换

分析一下

转换原理

因为数据在内存里的存储方式是非常的相近的

// 字符串的存储
type StringHeader struct {
   Data uintptr
   Len  int
}

// 切片的存储
type SliceHeader struct {
   Data uintptr
   Len  int
   Cap  int                // 这个没有的,需要舍弃或者填充上
}

所以,我们只需要用unsafe.Pointer来进行一下转换就好了,直接用原始的数据,就不用再分配内存了,这也就是原地转换快的原因

数据安全

StringToBytes的安全

因为是原地转换,其实是绕过了go的安全机制的,要是我们在转换string为[]byte之后,再修改[]byte的数据,原来的string是会怎么样呢?我们来试试看

func TestSafe(t *testing.T) {
   a := "123456789"
   t.Logf("string: %v", a)
   b := StringToBytes(a)
   b[2] = 'x'
   t.Logf("string: %v", a)
}

func TestSafe2(t *testing.T) {
   b := StringToBytes("123456789")
   b[2] = 'x'
   t.Logf("string: %v", string(b))
}

对了,会奔溃

第一个测试

image

第二个测试

image

BytesToString的安全

那么,我要是把[]byte的值改了呢?会不会也是崩溃,毕竟,你都转换了,转换了之后成了string,我这相当于是偷偷改string的值啊,string在go里是不可变的

func TestSafeA1(t *testing.T) {
   a := []byte{72, 101, 108, 108, 111}
   b := BytesToString(a)
   t.Logf("string: %v", b)
   a[0] = 73
   t.Logf("string: %v", b)
}

结果是

image

意不意外?能改!

所以,原地转换是不安全的,是有一定的理解成本能的

string为啥不能改

一句话原理:string分配的那段内存是只读数据 SRODATA,你再怎么原地转换,也是没法改只读内存的数据的

用个例子看下

package main

func main() {
   str := "hello"
   println([]byte(str))
}

GOOS=linux GOARCH=amd64 go tool compile -S main.go

结果

"".main STEXT size=170 args=0x0 locals=0x70 funcid=0x0
        0x0000 00000 (main.go:3)        TEXT    "".main(SB), ABIInternal, $112-0
        0x0000 00000 (main.go:3)        MOVQ    (TLS), CX
        0x0009 00009 (main.go:3)        CMPQ    SP, 16(CX)
        0x000d 00013 (main.go:3)        PCDATA  $0, $-2
        0x000d 00013 (main.go:3)        JLS     160
        0x0013 00019 (main.go:3)        PCDATA  $0, $-1
        0x0013 00019 (main.go:3)        SUBQ    $112, SP
        0x0017 00023 (main.go:3)        MOVQ    BP, 104(SP)
        0x001c 00028 (main.go:3)        LEAQ    104(SP), BP
        0x0021 00033 (main.go:3)        FUNCDATA        $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
        0x0021 00033 (main.go:3)        FUNCDATA        $1, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
        0x0021 00033 (main.go:5)        LEAQ    ""..autotmp_2+64(SP), AX
        0x0026 00038 (main.go:5)        MOVQ    AX, (SP)
        0x002a 00042 (main.go:5)        LEAQ    go.string."hello"(SB), AX
        0x0031 00049 (main.go:5)        MOVQ    AX, 8(SP)
        0x0036 00054 (main.go:5)        MOVQ    $5, 16(SP)
        0x003f 00063 (main.go:5)        PCDATA  $1, $0
        0x003f 00063 (main.go:5)        NOP
        0x0040 00064 (main.go:5)        CALL    runtime.stringtoslicebyte(SB)
        0x0045 00069 (main.go:5)        MOVQ    24(SP), AX
        0x004a 00074 (main.go:5)        MOVQ    AX, ""..autotmp_5+96(SP)
        0x004f 00079 (main.go:5)        MOVQ    32(SP), CX
        0x0054 00084 (main.go:5)        MOVQ    CX, ""..autotmp_6+56(SP)
        0x0059 00089 (main.go:5)        MOVQ    40(SP), DX
        0x005e 00094 (main.go:5)        MOVQ    DX, ""..autotmp_7+48(SP)
        0x0063 00099 (main.go:5)        PCDATA  $1, $1
        0x0063 00099 (main.go:5)        CALL    runtime.printlock(SB)
        0x0068 00104 (main.go:5)        MOVQ    ""..autotmp_5+96(SP), AX
        0x006d 00109 (main.go:5)        MOVQ    AX, (SP)
        0x0071 00113 (main.go:5)        MOVQ    ""..autotmp_6+56(SP), AX
        0x0076 00118 (main.go:5)        MOVQ    AX, 8(SP)
        0x007b 00123 (main.go:5)        MOVQ    ""..autotmp_7+48(SP), AX
        0x0080 00128 (main.go:5)        MOVQ    AX, 16(SP)
        0x0085 00133 (main.go:5)        PCDATA  $1, $0
        0x0085 00133 (main.go:5)        CALL    runtime.printslice(SB)
        0x008a 00138 (main.go:5)        CALL    runtime.printnl(SB)
        0x008f 00143 (main.go:5)        CALL    runtime.printunlock(SB)
        0x0094 00148 (main.go:6)        MOVQ    104(SP), BP
        0x0099 00153 (main.go:6)        ADDQ    $112, SP
        0x009d 00157 (main.go:6)        RET
        0x009e 00158 (main.go:6)        NOP
        0x009e 00158 (main.go:3)        PCDATA  $1, $-1
        0x009e 00158 (main.go:3)        PCDATA  $0, $-2
        0x009e 00158 (main.go:3)        NOP
        0x00a0 00160 (main.go:3)        CALL    runtime.morestack_noctxt(SB)
        0x00a5 00165 (main.go:3)        PCDATA  $0, $-1
        0x00a5 00165 (main.go:3)        JMP     0
        0x0000 64 48 8b 0c 25 00 00 00 00 48 3b 61 10 0f 86 8d  dH..%....H;a....
        0x0010 00 00 00 48 83 ec 70 48 89 6c 24 68 48 8d 6c 24  ...H..pH.l$hH.l$
        0x0020 68 48 8d 44 24 40 48 89 04 24 48 8d 05 00 00 00  hH.D$@H..$H.....
        0x0030 00 48 89 44 24 08 48 c7 44 24 10 05 00 00 00 90  .H.D$.H.D$......
        0x0040 e8 00 00 00 00 48 8b 44 24 18 48 89 44 24 60 48  .....H.D$.H.D$`H
        0x0050 8b 4c 24 20 48 89 4c 24 38 48 8b 54 24 28 48 89  .L$ H.L$8H.T$(H.
        0x0060 54 24 30 e8 00 00 00 00 48 8b 44 24 60 48 89 04  T$0.....H.D$`H..
        0x0070 24 48 8b 44 24 38 48 89 44 24 08 48 8b 44 24 30  $H.D$8H.D$.H.D$0
        0x0080 48 89 44 24 10 e8 00 00 00 00 e8 00 00 00 00 e8  H.D$............
        0x0090 00 00 00 00 48 8b 6c 24 68 48 83 c4 70 c3 66 90  ....H.l$hH..p.f.
        0x00a0 e8 00 00 00 00 e9 56 ff ff ff                    ......V...
        rel 5+4 t=17 TLS+0
        rel 45+4 t=16 go.string."hello"+0
        rel 65+4 t=8 runtime.stringtoslicebyte+0
        rel 100+4 t=8 runtime.printlock+0
        rel 134+4 t=8 runtime.printslice+0
        rel 139+4 t=8 runtime.printnl+0
        rel 144+4 t=8 runtime.printunlock+0
        rel 161+4 t=8 runtime.morestack_noctxt+0
go.cuinfo.packagename. SDWARFCUINFO dupok size=0
        0x0000 6d 61 69 6e                                      main
""..inittask SNOPTRDATA size=24
        0x0000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
        0x0010 00 00 00 00 00 00 00 00                          ........
go.string."hello" SRODATA dupok size=5
        0x0000 68 65 6c 6c 6f                                   hello
gclocals·69c1753bd5f81501d95132d08af04464 SRODATA dupok size=8
        0x0000 02 00 00 00 00 00 00 00                          ........

可以看到,这个字符串的存储这一段:是SRODATA

image

SRODATA 标志表示这个数据在只读内存段

因为原地转换,只是在“套个壳”,本质的内存段存储仍然为“只读”,所以,panic是符合预期的