我的编程空间,编程开发者的网络收藏夹
学习永远不晚

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

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

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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

引言

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

  • go字符串存储于内存的哪部分区域?
  • 我们初始化两个"hello world", 这两个"hello world"会放到同一块内存空间吗?
  • go字符串是动态的还是静态的, 修改他的时候是修改原字符串还是新构建一个字符串?

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

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

内容介绍

本文将介绍如下内容

字符串数据结构

字符串中的数据结构如下

type stringStruct struct {
   str unsafe.Pointer
   len int
}
  • str: 大部分情况下指向只读数据段中的一块内存区域, 少部分情况指向堆/栈, unsafe.Pointer类型, 大小8字节.
  • len: 这个字符串的长度, int类型, 在64bit机上大小8字节, 在32bit机上大小4字节.

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

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

我们把字符串分为两种

  • 编译期即可确定的字符串, 如a:="hello"
  • 运行时通过+拼接得到的字符串, 如b:=a+"world"

编译期即可确定的字符串

a := "hello world"

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

  • stringStruct结构体所在的内存
  • unsafe.Pointer类型的str所在的内存

首先是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函数执行流程如下

  • 用for range循环来遍历整个a数组, 计算其中所有非空串的个数count和长度总和l
  • 然后调用func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte)函数来为这个字符串分配内存空间, 并返回字符串和其底层的[]byte数组. 对于该函数来说, 如果buf!=nil则使用buf的内存空间, 否则调用func rawstring(size int) (s string, b []byte) 函数, rawstring函数会调用mallocgc来在堆上分配内存空间, 并返回使用该内存空间的字符串及其底层切片.
  • 此时我们已经拿到了一个字符串及其底层切片, 因为字符串不可变, 所以go通过修改其底层数组来为字符串赋值, 他会再次for range循环a数组, 然后通过copy函数来把a中的字符串拷贝到新串对应的底层数组b中, 从而达到修改新串的目的.
  • 至此, 字符串s的内存分配和初始化已经全部完成, rawstringtmp函数返回

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

因此, 即使运行时多个通过+连接而成的新串有着相同的字面量, 他们的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结构体.

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

  • sp是一个string类型的指针, 他指向一块内存区域, 这块内存区域中全是二进制bit流, 但是我们会安装string的形式解释他, 即前8位被解释成一个指针, 后8位被解释成一个int类型.
  • 我们把sp转换为一个unsafe.Pointer, 此时将只保留起始地址和长度
  • 然后我们再把sp转换为stringStruct, 因此会按stringStruct的方式解释这段二进制bit流, 而因为stringStruct的结构和string一样, 所以也会把前8位解释成一个指针, 后8位解释成一个int类型, 不会出现差错.

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

  • 首先获取到b的地址, 然后把他转换为一个*slice
  • 然后通过取地址运算符来获取slice对应的slice
  • 又因为slice本身就是指针类型, 所以我们让这个slice=slice{p,size,size}的时候只是改变了其指向, 也就等价于让b改变指向, 使其指向p这块内存空间, 也就是str指向的那块内存空间.

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

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

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

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

  • 对于编译期确定的字符串, 他的str指针指向一个.rodata区的字面量, 不会被改变.
  • 而运行时确定的字符串, 他的str指针指向一个堆栈中的空间, 我们可以让一个[]byte指向其底层内存空间从而间接改变其内容

对于编译期确定的字符串, 尝试修改.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

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

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

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

这篇文章主要为大家介绍了深入string理解Golang是怎样实现的原理详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
2023-05-17

深入理解Golang接口的实现原理

深入理解Golang接口的实现原理,需要具体代码示例Golang(又称Go语言)作为一种快速、可靠的编程语言,广受开发者青睐。其中,接口(Interface)是Golang语言中非常重要的概念之一,它提供了一种抽象的方式来定义对象之间的交
深入理解Golang接口的实现原理
2024-03-07

深入理解Golang方法的内部实现

Golang是由Google开发的一种静态类型的编程语言,以其简洁的语法和高效的性能而备受程序员欢迎。在Golang中,方法是一种特殊的函数,用于为结构体添加行为。本文将深入探讨Golang方法的内部实现,通过具体的代码示例帮助读者更好地理
深入理解Golang方法的内部实现
2024-02-23

深入理解Golang接口的底层实现原理

深入理解Golang接口的底层实现原理,需要具体代码示例Go语言(Golang)是一种由Google开发的开源编程语言,因其简洁、高效和并发特性而备受程序员青睐。在Go语言中,接口(interface)是一种非常重要的概念,它使代码更加灵
深入理解Golang接口的底层实现原理
2024-02-23

深入了解Golang中的Slice底层实现

本文主要为大家详细介绍了Golang中slice的底层实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
2023-02-26

怎样深入理解C语言的指针

本篇文章给大家分享的是有关怎样深入理解C语言的指针,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。起源之前在知乎上看了一句话,指针是C的精髓,也是初学者的一个坎。换句话说,内存管
2023-06-26

怎样实现Java线程的深入探讨

这篇文章将为大家详细讲解有关怎样实现Java线程的深入探讨,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。1.什么是线程   一般来说,我们把正在计算机中执行的程序叫做"进程"(Proces
2023-06-03

深入解析Golang锁的底层实现机制

Golang锁的底层实现原理详解,需要具体代码示例概述:并发编程是现代软件开发中非常重要的一部分,而锁是实现并发控制的一种机制。在Golang中,锁的概念被广泛应用于并发编程中。本篇文章将深入探讨Golang锁的底层实现原理,并提供具体的代
深入解析Golang锁的底层实现机制
2023-12-28

深入了解vuex的实现原理

当面试被问vuex的实现原理,你要怎么回答?下面本篇文章就来带大家深入了解一下vuex的实现原理,希望对大家有所帮助!
2023-05-14

Android实现日夜间模式的深入理解

在本篇文章中给出了三种实现日间/夜间模式切换的方案,三种方案综合起来可能导致文章的篇幅过长,请耐心阅读。 1、使用 setTheme 的方法让 Activity 重新设置主题; 2、设置 Android Support Lib
2022-06-06

深入了解Go的interface{}底层原理实现

目录1. interface{}初探2. eface3. iface4. 接口转化1. interface{}初探 Go是强类型语言,各个实例变量的类型信息正是存放在interface{}中的,Go中的反射也与其底层结构有关。 iface
2022-06-07

深入探讨Golang实时更新功能的原理及实现机制

Golang热更新原理浅析:探讨实时更新功能的实现机制,需要具体代码示例随着软件的发展,实时更新功能成为了许多开发者和用户所期望的一个重要特性。Golang作为一门现代化的编程语言,自然也需要具备这样的能力。本文将对Golang热更新的原
深入探讨Golang实时更新功能的原理及实现机制
2024-01-20

深入理解Java编程线程池的实现原理

在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间
2023-05-30

怎样深入理解vue中的虚拟DOM和Diff算法

怎样深入理解vue中的虚拟DOM和Diff算法,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。真实DOM的渲染在讲虚拟DOM之前,先说一下真实DOM的渲染。浏览器真实DOM渲
2023-06-22

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录