关于golang:Go语言中string和byte的转换原理

4次阅读

共计 5268 个字符,预计需要花费 14 分钟才能阅读完成。

前言

哈喽,大家好,我是 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 = 65
var ch byte = '\x41'
var ch byte = 'A'

[]byte类型

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

// src/runtime/slice.go
type 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:nosplit
func 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))
}
// 运行后果
0xc00001a090
0xc00001a098

咱们能够看进去,指针指向的地位产生了变动,也就说每一个更改字符串,就须要重新分配一次内存,之前调配的空间会被 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.7
const tmpStringBufSize = 32

type tmpBuf [tmpStringBufSize]byte

func 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.go
func 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 0x109d65f
fatal 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 写段代码判断以后零碎的存储形式吗?
  • 面试中如果这样写二分查找

正文完
 0