[Golang] string vs. []byte 底层实现与性能分析

string和\[\]byte之间可以互相通过 s = string(bs)​, bs = []byte(s)​ 这样的语法强制转换类型,标准库的strings​和bytes​ 两个包内的函数能提供的能力也几乎一模一样。但是诛如compress​, io​ 这样的包里所用的变量总是[]byte​。那两者之间究竟有何异同?

底层实现

[]byte​

首先我们都知道slice​和array​之间的关系。一个array​是不可变的,slice相当于是一个指向array上某一部分的指针。slice的底层实现如下

golang 复制代码
type slice struct {
  array unsafe.Pointer
  len int
  cap int
}

array: 指向实际array​的指针

len, cap: 当前slice的长度, 当前slice的最大长度。

上述是slice与array的基本知识,更详细的讲解可自行搜索相关资料。

一个[]byte​ 自然也没有什么特殊的, 也是这样的一个slice​结构, 其中的array​指向一个byte array​。

由上述结构我们也可知,len​和cap​ 都是预先计算好的,另外由于golang在编译时会进行inline优化,len(ss)​ 这样的调用会被内联掉,实际上连一次函数调用的成本都没有, 就像直接读取了 slice.len​ 一样。因此对一个slice​调用 len(ss)​, cap(ss)​ 是个性能非常高的操作。 有些人担心多次对同一个slice调用len()​会性能差,有了如下这种写法,实在没什么必要:

golang 复制代码
data := []int{....}
lenOfData := len(data)
print(lenOfData)
n := lenOfData * 10
n2 := lenOfData + 10
....

string​

string的底层结构如下:

golang 复制代码
type strStruct struct {
  str unsafe.Pointer
  len int
}

​str​ 是指向一个[]byte​的指针, len​表示当前string的长度(可见len(s)​也是性能非常高的操作)。

异同

类型转换

根据底层实现,两者的关系如下图所示:

或许你有看过网上的 ​**[]byte​ 强转 ​​ string​ 的奇技淫巧**​s := *(*string)(unsafe.Pointer(&b))​​, 比标准库的 s := string(b)​​性能高得多,其原理就来自于此。

为什么Golang标准库还提供另一种慢得多的方法呢?因为在Golang的设计里,​ ​ ​**string​ 是不可变的**。我们对一个string​进行拼接操作,实际上是复制了一份新的string​。 而使用这个方法,就会把string​底层的[]byte​暴露出来,破坏了"string不可变"的约定。

golang 复制代码
b := []byte("123456")
s := *(*string)(unsafe.Pointer(&b))
b[0] = 'a'
// 这时s变成了 "a23456

性能

"既然两者能花式互转,那我不在乎他俩有什么区别,只关心谁快"

别急,即使不对每个操作进行benchmark, 从上面的底层实现中,我们已经可以推理得到两者在各种操作下的性能差异了。

从上面的实现可知,对string的所有操作,最终都是操作在其底层的byte slice​中。因此,我们得到这两个结论:

  1. 纯查询检索类的操作,比如Index, LastIndex, Contains, 两者的性能几乎一样 , string略慢一点点,因为要多一层指针。
  2. 会改变内容的操作,比如Replace, 拼接, bytes​要明显快。因为string是不可变的(前文提过),进行此类操作时总是需要复制内容到一个新变量中。

也要注意一个小例外:

​strings.Index(s, subs)​ 和 bytes.Index(b, []byte(subs))​ 谁快? 这时前者要快一点了。因为后者多出一个 []byte(s)​的操作。

使用总结

  • 从http, file, buffer, reader 等处读取到的原始数据往往是[]byte的。它更符合字节的语义的同时,能提供更好的性能。
  • 从上面拿到原始的[]byte 后不要急着第一时间就转string, 先将各种必要的操作都做完, 要输出给人看,或者需要作为string传参时,再转也不迟
  • 如果第一时间拿到的变量就是string, 是否要转成[]byte再进行后续各种操作则需要进行一些权衡。
  • 如果系统不在意这点性能(99%的系统可能都是),或者有一些团队内部规范,不需要考虑这点差异。

‍

‍

相关推荐
FungLeo1 小时前
成为全栈·Go 后端篇·分类树与标签关系:目录变更为何需要协调
golang·go·gorm·数据库事务·分类树·成为全栈
Sayai1 小时前
Flink 日志解析任务性能优化实战:正则/Grok 预编译、Guava 缓存权重陷阱与 ES 写入隔离
java·大数据·性能优化·flink·实时计算
FungLeo1 小时前
成为全栈·Go 后端篇·文章查询与展示投影:安全分页、排序和软删除
golang·go·gorm·分页查询·成为全栈·sql安全
raindayinrain11 小时前
c++高性能--原子操作,无锁编程,操作顺序
开发语言·c++·性能优化
洞见前行19 小时前
点击图标到内容出现,你的 App 在这几秒里做了什么:Android 冷启动全链路优化实战
android·性能优化·android studio
shenlongproxy19 小时前
代理IP速度测试怎么做:响应耗时分级与吞吐基准
性能优化
KevinAshen6721 小时前
用 Go 修复旧下载工具:从接口 403 到凭证截断的完整复盘
go·github
半杯咖啡半行码1 天前
QML进阶实战(二):异步网络、性能优化与工程化落地
网络·qt·性能优化·qml
m0_564876841 天前
GPU编程与性能优化(1)
性能优化
胖纸不争1 天前
EnergyStarZ:让 Windows 后台进程给前台让路的能效管理器(v1.2 发布)
windows·性能优化·c#·win32·ef core