Go语言模型:string的底层数据结构与高效操作详解
Golang的string类型底层数据结构简单,本质也是一个结构体实例,且是const不可变。
string的底层数据结构
通过下面一个例子来看:
packagemain
import(
"fmt"
"unsafe"
)
//from:string.go在GoLandIDE中双击shift快速找到
typestringStructstruct{
arrayunsafe.Pointer//指向一个[len]byte的数组
lengthint//长度
}
funcmain(){
test:="hello"
p:=(*str)(unsafe.Pointer(&test))
fmt.Println(&p,p)//0xc420070018&{0xa3f715}
c:=make([]byte,p.length)
fori:=0;istring,"hello"
test2:=test+"world"//字符串是不可变类型,会生成一个新的string实例
p2:=(*str)(unsafe.Pointer(&test2))
fmt.Println(&p2,p2)//0xc420028030&{0xc42000a2e511}
fmt.Println(test2)//hello,world
}
string的拼接与修改
+操作
string类型是一个不可变类型,那么任何对string的修改都会新生成一个string的实例,如果是考虑效率的场景就要好好考虑一下如何修改了。先说一下最长用的+操作,同样上面的例子,看一下+操作拼接字符串的反汇编:
25 test2:=test+"world" 0x00000000004824d7<+1127>: lea0x105a2(%rip),%rax#0x492a80 0x00000000004824de<+1134>: mov%rax,(%rsp) 0x00000000004824e2<+1138>: callq0x40dda0#调用newobject函数 0x00000000004824e7<+1143>: mov0x8(%rsp),%rax 0x00000000004824ec<+1148>: mov%rax,0xa0(%rsp) 0x00000000004824f4<+1156>: mov0xa8(%rsp),%rax 0x00000000004824fc<+1164>: mov0x8(%rax),%rcx 0x0000000000482500<+1168>: mov(%rax),%rax 0x0000000000482503<+1171>: mov%rax,0x8(%rsp) 0x0000000000482508<+1176>: mov%rcx,0x10(%rsp) 0x000000000048250d<+1181>: movq$0x0,(%rsp) 0x0000000000482515<+1189>: lea0x30060(%rip),%rax#0x4b257c 0x000000000048251c<+1196>: mov%rax,0x18(%rsp) 0x0000000000482521<+1201>: movq$0x6,0x20(%rsp) 0x000000000048252a<+1210>: callq0x43cc00 #调用concatstring2函数
因为当前go[2018.11version:go1.11]的不是遵循默认的x86callingconvention用寄存器传参,而是通过stack进行传参,所以go的反汇编不像c的那么容易理解,不过大概看懂+背后的操作还是没问题的,看一下runtime源码的拼接函数:
funcconcatstring2(buf*tmpBuf,a[2]string)string{
returnconcatstrings(buf,a[:])
}
//concatstringsimplementsaGostringconcatenationx+y+z+...
//Theoperandsarepassedintheslicea.
//Ifbuf!=nil,thecompilerhasdeterminedthattheresultdoesnot
//escapethecallingfunction,sothestringdatacanbestoredinbuf
//ifsmallenough.
funcconcatstrings(buf*tmpBuf,a[]string)string{
idx:=0
l:=0
count:=0
fori,x:=rangea{
n:=len(x)
ifn==0{
continue
}
ifl+n
分析runtime的concatstrings实现,可以看出+最后新申请buf,拷贝原来的string到buf,最后返回新实例。那么每次的+操作,都会涉及新申请buf,然后是对应的copy。如果反复使用+,就不可避免有大量的申请内存操作,对于大量的拼接,性能就会受到影响了。
bytes.Buffer
通过看源码,bytes.Buffer增长buffer时是按照2倍来增长内存,可以有效避免频繁的申请内存,通过一个例子来看:
funcmain(){
varbufbytes.Buffer
fori:=0;i<10;i++{
buf.WriteString("hi")
}
fmt.Println(buf.String())
}
对应的byte包库函数源码
//@file:buffer.go
func(b*Buffer)WriteString(sstring)(nint,errerror){
b.lastRead=opInvalid
m,ok:=b.tryGrowByReslice(len(s))
if!ok{
m=b.grow(len(s))//高效的增长策略->letcapacitygettwiceaslarge
}
returncopy(b.buf[m:],s),nil
}
//@file:buffer.go
//letcapacitygettwiceaslarge!!!
func(b*Buffer)grow(nint)int{
m:=b.Len()
//Ifbufferisempty,resettorecoverspace.
ifm==0&&b.off!=0{
b.Reset()
}
//Trytogrowbymeansofareslice.
ifi,ok:=b.tryGrowByReslice(n);ok{
returni
}
//Checkifwecanmakeuseofbootstraparray.
ifb.buf==nil&&n<=len(b.bootstrap){
b.buf=b.bootstrap[:n]
return0
}
c:=cap(b.buf)
ifn<=c/2-m{
//Wecanslidethingsdowninsteadofallocatinganew
//slice.Weonlyneedm+n<=ctoslide,but
//weinsteadletcapacitygettwiceaslargesowe
//don'tspendallourtimecopying.
copy(b.buf,b.buf[b.off:])
}elseifc>maxInt-c-n{
panic(ErrTooLarge)
}else{
//Notenoughspaceanywhere,weneedtoallocate.
buf:=makeSlice(2*c+n)
copy(buf,b.buf[b.off:])
b.buf=buf
}
//Restoreb.offandlen(b.buf).
b.off=0
b.buf=b.buf[:m+n]
returnm
}
string.join
这个函数可以一次申请最终string的大小,但是使用得预先准备好所有string,这种场景也是高效的,一个例子:
funcmain(){
varstrs[]string
fori:=0;i<10;i++{
strs=append(strs,"hi")
}
fmt.Println(strings.Join(strs,""))
}
对应库的源码:
//Joinconcatenatestheelementsofatocreateasinglestring.Theseparatorstring
//sepisplacedbetweenelementsintheresultingstring.
funcJoin(a[]string,sepstring)string{
switchlen(a){
case0:
return""
case1:
returna[0]
case2:
//Specialcaseforcommonsmallvalues.
//Removeifgolang.org/issue/6714isfixed
returna[0]+sep+a[1]
case3:
//Specialcaseforcommonsmallvalues.
//Removeifgolang.org/issue/6714isfixed
returna[0]+sep+a[1]+sep+a[2]
}
//计算好最终的string的大小
n:=len(sep)*(len(a)-1)//
fori:=0;i
strings.Builder(go1.10+)
看到这个名字,就想到了Java的库,哈哈,这个Builder用起来是最方便的,不过是在1.10后引入的。其高效也是体现在2倍速的内存增长,WriteString函数利用了slice类型对应append函数的2倍速增长。
一个例子:
funcmain(){
varsstrings.Builder
fori:=0;i<10;i++{
s.WriteString("hi")
}
fmt.Println(s.String())
}
对应库的源码
@file:builder.go
//WriteStringappendsthecontentsofstob'sbuffer.
//Itreturnsthelengthofsandanilerror.
func(b*Builder)WriteString(sstring)(int,error){
b.copyCheck()
b.buf=append(b.buf,s...)
returnlen(s),nil
}
总结
Golang的字符串处理还是挺方便的,有垃圾回收和一些内置的语言级写法支持,让复杂字符串操作没有那么繁琐了,比起C/C++高效了不少。
补充:gostring的内部实现
gostring内部实现
这个string的探索
来来个例子
funcboo(aint,bint)(int,string){
returna+b,"abcd"
}
81079000000000044dfa0:
8108044dfa0:>------48c74424180000>--movq$0x0,0x18(%rsp)
8108144dfa7:>------0000-
8108244dfa9:>------0f57c0>--xorps%xmm0,%xmm0
8108344dfac:>------0f11442420>--movups%xmm0,0x20(%rsp)
8108444dfb1:>------488b442408>--mov0x8(%rsp),%rax
8108544dfb6:>------4803442410>--add0x10(%rsp),%rax
8108644dfbb:>------4889442418>--mov%rax,0x18(%rsp)
8108744dfc0:>------488d05d4eb0100>--lea0x1ebd4(%rip),%rax#46cb9b
8108844dfc7:>------4889442420>--mov%rax,0x20(%rsp)
8108944dfcc:>------48c74424280400>--movq$0x4,0x28(%rsp)
8109044dfd3:>------0000-
8109144dfd5:>------c3>--retq---
其中
8108744dfc0:>------488d05d4eb0100>--lea0x1ebd4(%rip),%rax#46cb9b
8108844dfc7:>------4889442420>--mov%rax,0x20(%rsp)
8108944dfcc:>------48c74424280400>--movq$0x4,0x28(%rsp)
8109044dfd3:>------0000-
8109144dfd5:>------c3>--retq---
lea0x1ebd4(%rip),%rax得到char*,mov%rax,0x20(%rsp)复制给返回值,movq$0x4,0x28(%rsp)把长度也填进去,
其实可以看到string就是c里面的char*和len的组合
以上为个人经验,希望能给大家一个参考,也希望大家多多支持毛票票。如有错误或未考虑完全的地方,望不吝赐教。