文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

深入string理解Golang是怎样实现的

2023-05-17 05:25

关注

引言

本身打算先写完sync包的, 但前几天在复习以前笔记的时候突然发现与字符串相关的寥寥无几. 同时作为一个Java选手, 很轻易的想到了几个问题

在网上搜索后发现目前网上对go语言字符串的介绍相关甚少, 因此我在仔细阅读源码后产出了这批文章.

ps: 本文虽由Java中问题引出, 但后续内容和Java无关, 码字不易, 对你有帮助的话麻烦帮忙点个赞^_^.

内容介绍

本文将介绍如下内容

字符串数据结构

字符串中的数据结构如下

type stringStruct struct {
   str unsafe.Pointer
   len int
}

字符串会分配到内存中的哪块区域

我们先看下这张图, 下面内容结合本图理解

我们把字符串分为两种

编译期即可确定的字符串

a := "hello world"

我们这里把字符串占用的内存分为两部分

首先是stringStruct, 他是一个16字节大小的结构体, 因此他和一个普通结构体一样, 根据逃逸分析判断是否可以分配在栈上, 如果不行, 也会根据分级分配的方式分配到堆中.

而str则是指向了.rodata(只读数据段)中的存放的字符串字面量, 因此字符串字面量是在.rodata中

综上: string的数据结构stringStruct分配在堆/栈中, 而他对应的字符串字面量则是在只读数据段中

如果我们创建两个hello world字符串, 他们会放到同一内存区域吗?

根据上面的分析, 我们可以很容易的得到答案, 他们的数据结构stringStruct会分配在堆/栈的不同内存空间中, 而unsafe.Pointer则指向.rodata中的同一块内存区域

我们可以做出如下验证方式

//因为stringStruct是runtime包下一个不对外暴露的数据结构,
//所以我们新建一个结构相同的数据结构来接收string的内容
type Reception struct {
   p unsafe.Pointer
   len int
}
func main(){
   a := "hello world"
   b := "hello world"
   //用新建的Reception接收字符串内容, 本质上就是把a/b对应的二进制数据重新解析为Reception,
   //而Reception和stringStruct的结构相同, 所以不会出问题.
   rA := *(*Reception)(unsafe.Pointer(&a))
   rB := *(*Reception)(unsafe.Pointer(&b))
   //输出a,b的地址
   fmt.Println(&a)
   fmt.Println(&b)
   //输出stringStruct的str指向的地址
   fmt.Println(rA.p)
   fmt.Println(rB.p)
}

我们得到了如下结果

0xc000050260
0xc000050270
0x595700
0x595700

a,b两个stringStruct被分配到不同地址, 而他们的str则指向了同一地址.

运行时通过+拼接的字符串会放到那块内存中

字面量是否会在编译器合并

func main(){
   he := "hello"
   //编译期"li","hua"未能合并
   str1 := he+"li"+"hua"
   //编译期被合并为"nihao"
   str2 := "ni"+"hao"
   fmt.Println(str1)
}

网上有的文章说, 字符串字面量会在编译期进行合并, 但我在SDK1.18.9下测试的结果是只有右值为纯字面量时, 才会合并.

我们使用go tool compile -m main.go命令分析, 结果如下

main.go:8:13: inlining call to fmt.Println
//如果合并的话, 应该是he+"lihua"
main.go:7:17: he + "li" + "hua" escapes to heap
main.go:8:13: ... argument does not escape
main.go:8:13: str1 escapes to heap

大家可以自己用上述命令分析下自己SDK版本是否会合并.

不过重要的是, 我们知道右值为纯字面量拼接的字符串会在编译期合并, 等价于右值为纯字面量的字符串, 他的分配方式和编译期可确定的字符串一致.

接下来我们讨论右值表达式中存在变量的情况下是如何进行内存分配的

当我们用+连接多个字符串时, 会发生什么

我们先说结论, 运行时通过+连接多个字符串构成新串, 新串的stringStruct结构体和str指向的字面量都会被分配到堆/栈空间中.

在go语言编译期, 会把字符串的"+"替换为func concatstrings(buf *tmpBuf, a []string) string函数.

分配到栈上还是堆上

我们看下concatstrings的两个参数, 其中buf是一个栈空间的内存, go语言会通过所有要拼接的字符串总长度以及逃逸分析确定这个字符串会不会分配到栈上, 如果要分配到栈上, 则会传来buf参数.

栈上分配和堆上分配的流程几乎一致, 只不过在内存分配的时候会根据buf!=nil来判断该存放到哪块内存空间而已, 因此下文中我们统一按堆分配介绍.

而第二个参数a中存储有全部需要通过"+"连接的字符串

concatstrings函数执行流程如下

这样我们就得到了一个全部内存空间都分配在堆/栈中的字符串.

因此, 即使运行时多个通过+连接而成的新串有着相同的字面量, 他们的str也会指向不同的内存空间

验证

我们可以继续把字符串转换为Reception来看看他的str执行的地址

//因为stringStruct是runtime包下一个不对外暴露的数据结构,
//所以我们新建一个结构相同的数据结构来接收string的内容
type Reception struct {
   p unsafe.Pointer
   len int
}
func main(){
   h := "hello"
   a := h+" world"
   b := h+" world"
   //用新建的Reception接收字符串内容, 本质上就是把a/b对应的二进制数据重新解析为Reception,
   //而Reception和stringStruct的结构相同, 所以不会出问题.
   rA := *(*Reception)(unsafe.Pointer(&a))
   rB := *(*Reception)(unsafe.Pointer(&b))
   //输出a,b的地址
   fmt.Println(&a)
   fmt.Println(&b)
   //输出stringStruct的str指向的地址
   fmt.Println(rA.p)
   fmt.Println(rB.p)
}

结果如下

0xc000050260
0xc000050270
0xc00000a0e0
0xc00000a0f0

a和b字符串的str字段指向堆中不同的内存区域.

rawstring函数

rawstring真的是一个十分有趣的函数, 因此我决定对他进行详细的分析, 但他相对有点难度, 如果静下心来读懂, 定能让您有所收获. 我们直接上源码逐行分析

func rawstring(size int) (s string, b []byte) {
   //在堆中申请内存
   p := mallocgc(uintptr(size), nil, false)
   //把string转换为stringStruct数据结构
   stringStructOf(&s).str = p
   stringStructOf(&s).len = size
   //最重要的部分, 让b重新指向p空间
   *(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}
   return
}
func stringStructOf(sp *string) *stringStruct {
   return (*stringStruct)(unsafe.Pointer(sp))
}

stringStructOf函数十分简单, 因为string和stringStruct的结构完全相同, 因此他直接通过把(*stringStruct)(unsafe.Pointer(sp))来把字符串指针sp转换为stringStruct指针, 然后通过stringStruct指针来获取stringStruct结构体.

我们可以这样理解下转换方式.

接下来我们按同样的思路看下*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}

只会我们就可以通过b来修改这块内存空间, 从而间接修改字符串的ne

go中字符串是不可变的吗, 我们如何得到一个可变的字符串

go中字符串在语义中是不可变的, 并且咱们对字符串进行+操作时也是新开辟一块内存空间来存放修改后的字符串, 真的没有什么办法改变一个字符串中的数据吗?

回顾下我们之前分析的结论

对于编译期确定的字符串, 尝试修改.rodata区中的字面量会panic

//尝试修改.rodata区中数据, painic
func main(){
   str := "hello world"
   byteArr := *(*[]byte)(unsafe.Pointer(&str))
   byteArr[0] = 'w'
   fmt.Println(str)
}

而对于运行时通过+拼接得到的新串, 修改堆栈中存放的字面量则可以成功

//输出wello world
func main(){
   str := "hello"
   //此时字符串str的unsafe.Pointer指针str会重新指向堆中内存
   str += "world"
   //让[]byte也指向堆中内存
   byteArr := *(*[]byte)(unsafe.Pointer(&str))
   //修改
   byteArr[0] = 'w'
   fmt.Println(str)
}

[]byte和string的更高效转换

一般情况下我们使用的强制类型的方式进行[]bytestring的互相转换都会被替换为stringtoslicebyteslicebytetostring函数, 这两个函数都会新申请一个内存空间, 然后将原本[]byte或string中的数据拷贝到新内存空间中, 涉及一次内存copy.

我们可以采用unsafe.Pointer当作一个中介来进行更高效的类型转换, 事实上, 这个方式咱们之前已多次使用.

string->byte[]

func main(){
   str := "hello"
   //注意下面这一行, 是核心
   byteArr := *(*[]byte)(unsafe.Pointer(&str))
   fmt.Println(byteArr)
}

个人强烈不推荐这种写法, 因为此时我们对byteArr的修改将导致超出预期的行为.

且因为stringStruct的数据结构中只有unsafe.Pointer和一个int型变量len, 而切片的数据结构slice则是有着unsafe.Pointer, int型变量len, 和int型变量cap, 所以我们通过上述方法把一个string强制转换为一个[]byte时, 这个[]byte的cap将是一个完全不可控的值(取决于这部分内存中的数据, 且访问这块内存本身就是非法的)

[]byte->string

func main(){
   //hello
   byteArr := []byte{104,101,108,108,111}
   str := *(*string)(unsafe.Pointer(&byteArr))
   fmt.Println(str)
}

相比起string->[]byte来说, []byte->string相对要安全很多, 我们只需要确保原始的[]byte不会被改变即可, 事实上, 这其实也是strings.Builder的实现原理之一

//string.Builder的String()函数本质上就是把string.Builder中维护的[]byte转换为string返回
func (b *Builder) String() string {
   return *(*string)(unsafe.Pointer(&b.buf))
}

结尾

我相信大家对字符串已经有了一个比较不错的认知了, 如果你之前是一名Java选手, 不要把字符串常量池等概念代入go中, 虽然Java和go中的字符串外在表现确实有些类似.

以上就是深入string理解Golang是怎样实现的的详细内容,更多关于Golang string实现的资料请关注编程网其它相关文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯