[]byte与string的两种转换方式和底层实现

小许公众号点了关注的朋友,应该都看过小许之前的文章《fasthttp是如何做到比net/http快十倍的》,相信你们还对极致的优化方式意犹未尽。

不过你发现没fasthttp关于string和[]byte的转换方式和大家平常普遍使用的方式不一样,fasthttp转换实现如下:

go 复制代码
//[]byte转string
func b2s(b []byte) string {
	return *(*string)(unsafe.Pointer(&b))
}
 
//string转[]byte
func s2b(s string) (b []byte) {
	bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
	bh.Data = sh.Data
	bh.Cap = sh.Len
	bh.Len = sh.Len
	return b
}

为什么不用我们常见的string和[]byte的转换方式呢?这样做是怎么提高性能的呢?...

带着这些疑问,今天将分享下并总结string和[]byte的转换方式,不同的转换方式之间的实现和区别!

小贴士:

欢迎朋友们关注我的公众号📢📢:【小许code】!

两种转换方式

如果此时此刻你刚好遇到面试官问你string和[]byte如何进行转换,有几种方式?你能答上来吗

反正在写这篇文章之前小许估计是答不出来的,哈哈!

毕竟知道的越多,不知道的也越多嘛

那今天我们就来聊聊,继续往下读之前,我们先了解下这两种数据类型:

string和[]byte

🔔上图中可以看出 stringStruct和slice还是有一些相似之处,str和array指针指向底层数组的地址,len代表的就是数组长度。

关于string类型,在go标准库中官方说明如下:

go 复制代码
// 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位字节的集合,string的定义在上图中左侧,通常但不一定代表UTF-8编码的文本。string可以为空,但是不能为nil,并且string的值是不能改变的。

🚩为什么string类型没有cap字段

string的不可变性,也就不能直接向底层数组追加元素,所以不需要Cap。

而[]byte就是一个byte类型的切片,切片本质也是一个结构体。

📢 这里我们先记住下这两种数据类型的特点,对后面的了解两者的转换有帮助!

标准方式

Golang中string与[]byte的互换,这是我们常用的,也是立马能想到的转换方式,这种方式称为标准方式。

go 复制代码
// string 转 []byte
s1 := "xiaoxu"
b := []byte(s1)

// []byte 转 string
s2 := string(b)

那还有其他方式吗?当然有的,那就是强转换

强转换方式

强转换方式是通过unsafe和reflect包来实现的,代码如下:

go 复制代码
//[]byte转string
func b2s(b []byte) string {
	return *(*string)(unsafe.Pointer(&b))
}
 
//string转[]byte
func s2b(s string) (b []byte) {
	bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
	bh.Data = sh.Data
	bh.Cap = sh.Len
	bh.Len = sh.Len
	return b
}

可以看出利用reflect.SliceHeader(代表一个运行时的切片) 和 unsafe.Pointer进行指针替换。

🚩为什么可以这么做呢?

前面我们在讲string和[]byte类型的时候就提了,因为两者的底层结构的字段相似!

array和str的len是一致的,而唯一不同的就是cap字段,所以他们的内存布局上是对齐的。

分析

我们看下这两种转换方式底层是如何实现的,这些实现代码在标准库中都是有的,下面底层实现的代码来自Go 1.18.6版本。

标准方式底层实现

string转[]byte底层实现

先看string转[]byte的实现,(实现源码在 src/runtime/string.go 中)

go 复制代码
const tmpStringBufSize = 32

//长度32的数组
type tmpBuf [tmpStringBufSize]byte

//时间函数
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
	var b []byte
    //判断字符串长度是否小于等于32
	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.
//rawbyteslice函数 分配一个新的字节片。字节片未归零
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
}

上面代码可以看出string转[]byte是,会根据字符串长度来决定是否需要重新分配一块内存。

  • 预先定义了一个长度为32的数组
  • 若字符串的长度不超过这个长度32的数组,copy函数实现string到[]byte的拷贝
  • 若字符串的长度超过了这个长度32的数组,重新分配一块内存了,再进行copy

[]byte转string底层实现

再看[]byte转string的实现,(实现源码在 src/runtime/string.go 中)

scss 复制代码
const tmpStringBufSize = 32

//长度32的数组
type tmpBuf [tmpStringBufSize]byte

//实现函数
func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) {
    ...
	if n == 1 {
		p := unsafe.Pointer(&staticuint64s[*ptr])
		if goarch.BigEndian {
			p = add(p, 7)
		}
		stringStructOf(&str).str = p
		stringStructOf(&str).len = 1
		return
	}

	var p unsafe.Pointer
    //判断字符串长度是否小于等于32
	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
    //拷贝byte数组至字符串
	memmove(p, unsafe.Pointer(ptr), uintptr(n))
	return
}

跟string转[]byte一样,当数组长度超过32时,同样需要调用mallocgc分配一块新内存

强转换底层实现

从标准的转换方式中,我们知道如果字符串长度超过32的话,会重新分配一块新内存,进行内存拷贝。

go 复制代码
//string转[]byte
func s2b(s string) (b []byte) {
	bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
	sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
	bh.Data = sh.Data
	bh.Cap = sh.Len
	bh.Len = sh.Len
	return b
}

强转换过程中,通过 神奇的unsafe.Pointer指针

  • 任何类型的指针 *T 都可以转换为unsafe.Pointer类型的指针,可以存储任何变量的地址
  • unsafe.Pointer 类型的指针也可以转换回普通指针,并且可以和类型*T不相同

🚩 refletc包的 reflect.SliceHeader 和 reflect.StringHeader分别代表什么意思?

reflect.SliceHeader:slice类型的运行时表示形式

reflect.StringHeader:string类型的运行时表示形式

go 复制代码
//slice在运行时的描述符
type SliceHeader struct {      
 	Data uintptr
 	Len  int
	Cap  int
}

//string在运行时的描述符
type StringHeader struct {
	Data uintptr
	Len  int
}

**(reflect.SliceHeader)(unsafe.Pointer(&b))* 的目的就是通过unsafe.Pointer 把它们转换为 *reflect.SliceHeader 指针。

而运行时表现形式 SliceHeader 和 StringHeader,而这两个结构体都有一个 Data 字段,用于存放指向真实内容的指针。

✏️[]byte 和 string之间的转换,就可以理解为是通过 unsafe.Pointer 把 *SliceHeader 转为 *StringHeader,也就是 *[]byte 和 *string之间的转换。

那么我们就可以理解相对于标准转换方式,强转换方式的优点在哪了!

直接替换指针的指向,避免了申请新内存(零拷贝),因为两者指向的底层字段Data地址相同

总结

今天小许和大家一起了解了[]byte和string类型,以及[]byte和string的两种转换方式。

不过Go语言提供给我们使用的还是标准转换方式,主要是因为在你不确定安全隐患的情况下,使用强转化方式可能不必要的问题。

不过像fasthttp那样,对程序对运行性能有高要求,那就可以考虑使用强转换方式!

欢迎点赞 👍、收藏 💙、关注 💡 三连支持一下~

知道的越多,不知道的也越多,我是小许,下期见~🙇💻

相关推荐
拖孩20 分钟前
微信群太多,管理麻烦?那试试接入AI助手吧~
前端·后端·微信
Humbunklung29 分钟前
Rust枚举:让数据类型告别单调乏味
开发语言·后端·rust
radient37 分钟前
Golang-GMP 万字洗髓经
后端·架构
Code季风37 分钟前
Gin Web 层集成 Viper 配置文件和 Zap 日志文件指南(下)
前端·微服务·架构·go·gin
蓝倾37 分钟前
如何使用API接口实现淘宝商品上下架监控?
前端·后端·api
舂春儿39 分钟前
如何快速统计项目代码行数
前端·后端
Pedantic40 分钟前
我们什么时候应该使用协议继承?——Swift 协议继承的应用与思
前端·后端
Codebee41 分钟前
如何利用OneCode注解驱动,快速训练一个私有的AI代码助手
前端·后端·面试
martinzh41 分钟前
用Spring AI搭建本地RAG系统:让AI成为你的私人文档助手
后端
MMJC61 小时前
Playwright MCP Batch:革命性的批量自动化工具,让 Web 操作一气呵成
前端·后端·mcp