前言

哈喽,大家好,我是asong。为什么会有明天这篇文章呢?前天在一个群里看到了一份Go语言面试的八股文,其中有一道题就是"字符串转成byte数组,会产生内存拷贝吗?";这道题挺有意思的,实质就是在问你string[]byte的转换原理,考验你的根本功底。明天咱们就来好好的探讨一下两者之间的转换形式。

byte类型

咱们看一下官网对byte的定义:

// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is// used, by convention, to distinguish byte values from 8-bit unsigned// integer values.type byte = uint8

咱们能够看到byte就是uint8的别名,它是用来辨别字节值8位无符号整数值

其实能够把byte当作一个ASCII码的一个字符。

示例:

var ch byte = 65var ch byte = '\x41'var ch byte = 'A'

[]byte类型

[]byte就是一个byte类型的切片,切片实质也是一个构造体,定义如下:

// src/runtime/slice.gotype slice struct {    array unsafe.Pointer    len   int    cap   int}

这里简略阐明一下这几个字段,array代表底层数组的指针,len代表切片长度,cap代表容量。看一个简略示例:

func main()  {    sl := make([]byte,0,2)    sl = append(sl, 'A')    sl = append(sl,'B')    fmt.Println(sl)}

依据这个例子咱们能够画一个图:

string类型

先来看一下string的官网定义:

// string is the set of all strings of 8-bit bytes, conventionally but not// necessarily representing UTF-8-encoded text. A string may be empty, but// not nil. Values of string type are immutable.type string string

string是一个8位字节的汇合,通常但不肯定代表UTF-8编码的文本。string能够为空,然而不能为nil。string的值是不能扭转的

看一个简略的例子:

func main()  {    str := "asong"    fmt.Println(str)}

string类型实质也是一个构造体,定义如下:

type stringStruct struct {    str unsafe.Pointer    len int}

stringStructslice还是很类似的,str指针指向的是某个数组的首地址,len代表的就是数组长度。怎么和slice这么类似,底层指向的也是数组,是什么数组呢?咱们看看他在实例化时调用的办法:

//go:nosplitfunc gostringnocopy(str *byte) string {    ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}    s := *(*string)(unsafe.Pointer(&ss))    return s}

入参是一个byte类型的指针,从这咱们能够看出string类型底层是一个byte类型的数组,所以咱们能够画出这样一个图片:

string和[]byte有什么区别

下面咱们一起剖析了string类型,其实他底层实质就是一个byte类型的数组,那么问题就来了,string类型为什么还要在数组的根底上再进行一次封装呢?

这是因为在Go语言中string类型被设计为不可变的,不仅是在Go语言,其余语言中string类型也是被设计为不可变的,这样的益处就是:在并发场景下,咱们能够在不加锁的管制下,屡次应用同一字符串,在保障高效共享的状况下而不必放心平安问题。

string类型尽管是不能更改的,然而能够被替换,因为stringStruct中的str指针是能够扭转的,只是指针指向的内容是不能够扭转的。看个例子:

func main()  {    str := "song"    fmt.Printf("%p\n",[]byte(str))    str = "asong"    fmt.Printf("%p\n",[]byte(str))}// 运行后果0xc00001a0900xc00001a098

咱们能够看进去,指针指向的地位产生了变动,也就说每一个更改字符串,就须要重新分配一次内存,之前调配的空间会被gc回收。

string和[]byte规范转换

Go语言中提供了规范形式对string[]byte进行转换,先看一个例子:

func main()  {    str := "asong"    by := []byte(str)    str1 := string(by)    fmt.Println(str1)}

规范转换用起来还是比较简单的,那你晓得他们外部是怎么实现转换的吗?咱们来剖析一下:

  • string类型转换到[]byte类型

咱们对下面的代码执行如下指令go tool compile -N -l -S ./string_to_byte/string.go,能够看到调用的是runtime.stringtoslicebyte

// runtime/string.go go 1.15.7const tmpStringBufSize = 32type tmpBuf [tmpStringBufSize]bytefunc stringtoslicebyte(buf *tmpBuf, s string) []byte {    var b []byte    if buf != nil && len(s) <= len(buf) {        *buf = tmpBuf{}        b = buf[:len(s)]    } else {        b = rawbyteslice(len(s))    }    copy(b, s)    return b}// rawbyteslice allocates a new byte slice. The byte slice is not zeroed.func rawbyteslice(size int) (b []byte) {    cap := roundupsize(uintptr(size))    p := mallocgc(cap, nil, false)    if cap != uintptr(size) {        memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))    }    *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}    return}

这里分了两种情况,通过字符串长度来决定是否须要重新分配一块内存。也就是说事后定义了一个长度为32的数组,字符串的长度超过了这个数组的长度,就阐明[]byte不够用了,须要重新分配一块内存了。这也算是一种优化吧,32是阈值,只有超过32才会进行内存调配。

最初咱们会通过调用copy办法实现string到[]byte的拷贝,具体实现在src/runtime/slice.go中的slicestringcopy办法,这里就不贴这段代码了,这段代码的外围思路就是:将string的底层数组从头部复制n个到[]byte对应的底层数组中去

  • []byte类型转换到string类型

[]byte类型转换到string类型实质调用的就是runtime.slicebytetostring

// 以下无关的代码片段func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) {    if n == 0 {        return ""    }    if n == 1 {        p := unsafe.Pointer(&staticuint64s[*ptr])        if sys.BigEndian {            p = add(p, 7)        }        stringStructOf(&str).str = p        stringStructOf(&str).len = 1        return    }    var p unsafe.Pointer    if buf != nil && n <= len(buf) {        p = unsafe.Pointer(buf)    } else {        p = mallocgc(uintptr(n), nil, false)    }    stringStructOf(&str).str = p    stringStructOf(&str).len = n    memmove(p, unsafe.Pointer(ptr), uintptr(n))    return}

这段代码咱们能够看出会依据[]byte的长度来决定是否从新分配内存,最初通过memove能够拷贝数组到字符串。

string和[]byte强转换

规范的转换方法都会产生内存拷贝,所以为了缩小内存拷贝和内存申请咱们能够应用强转换的形式对两者进行转换。在规范库中有对这两种办法实现:

// runtime/string.gofunc slicebytetostringtmp(ptr *byte, n int) (str string) {    stringStructOf(&str).str = unsafe.Pointer(ptr)    stringStructOf(&str).len = n    return}func stringtoslicebytetmp(s string) []byte {    str := (*stringStruct)(unsafe.Pointer(&s))    ret := slice{array: unsafe.Pointer(str.str), len: str.len, cap: str.len}    return *(*[]byte)(unsafe.Pointer(&ret))}

通过这两个办法咱们可晓得,次要应用的就是unsafe.Pointer进行指针替换,为什么这样能够呢?因为stringslice的构造字段是类似的:

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

惟一不同的就是cap字段,arraystr是统一的,len是统一的,所以他们的内存布局上是对齐的,这样咱们就能够间接通过unsafe.Pointer进行指针替换。

两种转换如何取舍

当然是举荐大家应用规范转换形式了,毕竟规范转换形式是更平安的!然而如果你是在高性能场景下应用,是能够思考应用强转换的形式的,然而要留神强转换的应用形式,他不是平安的,这里举个例子:

func stringtoslicebytetmp(s string) []byte {    str := (*reflect.StringHeader)(unsafe.Pointer(&s))    ret := reflect.SliceHeader{Data: str.Data, Len: str.Len, Cap: str.Len}    return *(*[]byte)(unsafe.Pointer(&ret))}func main()  {    str := "hello"    by := stringtoslicebytetmp(str)    by[0] = 'H'}

运行后果:

unexpected fault address 0x109d65ffatal error: fault[signal SIGBUS: bus error code=0x2 addr=0x109d65f pc=0x107eabc]

咱们能够看到程序间接产生严重错误了,即便应用defer+recover也无奈捕捉。起因是什么呢?

咱们后面介绍过,string类型是不能扭转的,也就是底层数据是不能更改的,这里因为咱们应用的是强转换的形式,那么by指向了str的底层数组,当初对这个数组中的元素进行更改,就会呈现这个问题,导致整个程序down掉!

总结

本文咱们一起剖析bytestring类型的根本定义,也剖析了[]bytestring的两种转换形式,应该还差最初一环,也就是大家最关怀的性能测试,这个我没有做,我感觉没有很大意义,通过后面的剖析就能够得出结论,强转换的形式性能必定要比规范转换要好。对于这两种形式的应用,大家还是依据理论场景来抉择,脱离场景的谈性能就是耍流氓!!!

素质三连(分享、点赞、在看)都是笔者继续创作更多优质内容的能源!我是asong,咱们下期见。

创立了一个Golang学习交换群,欢送各位大佬们踊跃入群,咱们一起学习交换。入群形式:关注公众号获取。更多学习材料请到公众号支付。

欢送关注公众号:Golang梦工厂

举荐往期文章:

  • 学习channel设计:从入门到放弃
  • Go语言如何实现可重入锁?
  • Go语言中new和make你应用哪个来分配内存?
  • 源码分析panic与recover,看不懂你打我好了!
  • 空构造体引发的大型打脸现场
  • Leaf—Segment分布式ID生成零碎(Golang实现版本)
  • 面试官:两个nil比拟后果是什么?
  • 面试官:你能用Go写段代码判断以后零碎的存储形式吗?
  • 面试中如果这样写二分查找