Go string原理简析

news2024/11/26 2:30:23

引入

当查看string类型的变量所占的空间大小时,会发现是16字节(64位机器)。

    str := "hello"
    fmt.Println(unsafe.Sizeof(str)) // 16

也许你会好奇,为什么是16字节,它的底层存储模型是什么样子的。

源码分析

底层结构

在src/runtime/string.go中,定义了string的结构:

    type stringStruct struct {
    	str unsafe.Pointer
    	len int
    }

string底层结构是一个结构体,它有两个字段:

  1. str unsafe.Pointer: 该字段指向了string底层的byte数组(切片)
  2. len int: 该字段确定了string字符串的长度
    unsafe.Pointer和int类型的字段分别占用8个字节空间,所以string类型的变量占用16字节(内存对齐后)空间

常规内容

在C语言中,使用'\0'表示字符串的结尾。在Go语言中,使用len这个字段指定了字符串的长度,知道字符串的长度,自然也就知道字符串的结尾在哪。

Go语言中,字符串一经定义,可通过指定下标形式进行访问,但不可修改,即string类型变量中的字符是不可以被修改的。

    str := "abcdef"
    fmt.Println(str[0]) // 97
    str[0] = 'A'  // cannot assign to str[0] (value of type byte)

字符串可以被转换成byte切片,可对切片进行修改,但即使修改了byte切片,也不会对原字符串变量造成任何影响。

    str := "hello"
    arr := []byte(str)
    arr[0] = 'H'
    fmt.Println(str) // hello

Go中所有字符都是以utf-8格式进行编码的。UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,也是一种前缀码。它可以用一至四个字节对Unicode字符集中的所有有效编码点进行编码,属于Unicode标准的一部分,最初由肯·汤普逊和罗布·派克提出。在处理字符串时,通常你不知道字符串中每个字符到底占用1个字节还是2~4个字节的空间。在需要索引字符串中的某个字符时,通常会将字符串类型强制转换为[]rune类型。

rune类型

rune类型是int32类型的别名。
定义位置位于src/builtin/builtin.go

// int32 is the set of all signed 32-bit integers.
// Range: -2147483648 through 2147483647.
type int32 int32
...
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

rune用于区分字符值和整数值,当然如果你任性,偏要使用int32类型来保存1个字符,那也不是不可以的,但是显得十分不专业(土、屯、low)。

	str := "hello 你好"
	str1 := []byte(str)
	str2 := []rune(str)
	fmt.Println(len(str1)) // 12
	fmt.Println(len(str2)) // 8
	fmt.Println(string(str1[6]))   // ä
	fmt.Println(string(str2[6]))   // 你

分别打印两个序列

fmt.Println(str1) // [104 101 108 108 111 32 228 189 160 229 165 189]
fmt.Println(str2) // [104 101 108 108 111 32 20320 22909]

str对应的内存结构如下:

stringStruct:    
      str ----            len----12
             |         
        -----------------
        |		h		|  0 <- 104
        |		e		|  1 <- 101 
        |		l		|  2 <- 108
        |		l		|  3 <- 108
        |		o		|  4 <- 111
        |				|  5 <- 32
        |		你	 	|  6 <- 228 ---------- utf-8---------------
        |				|  7 <- 189                               |
        |				|  8 <- 160------------20320---------------
        |		好		|  9 <- 229------------utf-8---------------
        |				|  10 <- 165                              |
        |				|  11 <- 189-----------22909----------------
        -----------------

将byte切片强转成string后,修改byte切片,不会对强转后的string造成影响

	bs := []byte("hello")
	str := string(bs) // hello
	fmt.Println(str)
	bs = []byte("world")
	fmt.Println(str) // hello

string的运行时源码位于 src/runtime/string.go中

// The constant is known to the compiler.
// There is no fundamental theory behind this number.
const tmpStringBufSize = 32

type tmpBuf [tmpStringBufSize]byte

打开该文件,第一眼,你看见了这俩货tmpBuf这个32字节长度的byte数组类型,其实是在string类型变量做拼接(concat)时使用的。如果几个字符串长度加在一起小于等于32字节,那么go运行时就直接将其在栈中拼接(借助tmpBuf指针变量),否则就要去堆中开辟合理的空间,再进行拼接了。

字符串拼接

对于不同个数的字符串拼接,选取不同的字符串拼接函数

func concatstring2(buf *tmpBuf, a0, a1 string) string {
	return concatstrings(buf, []string{a0, a1})
}

func concatstring3(buf *tmpBuf, a0, a1, a2 string) string {
	return concatstrings(buf, []string{a0, a1, a2})
}

func concatstring4(buf *tmpBuf, a0, a1, a2, a3 string) string {
	return concatstrings(buf, []string{a0, a1, a2, a3})
}

func concatstring5(buf *tmpBuf, a0, a1, a2, a3, a4 string) string {
	return concatstrings(buf, []string{a0, a1, a2, a3, a4})
}

他们都调用了concatstrings函数

// concatstrings implements a Go string concatenation x+y+z+...
// The operands are passed in the slice a.
// If buf != nil, the compiler has determined that the result does not
// escape the calling function, so the string data can be stored in buf
// if small enough.
func concatstrings(buf *tmpBuf, a []string) string {
	idx := 0
	l := 0                // 所有字符串的总长度
	count := 0            // 字符串的个数
	for i, x := range a {
		n := len(x)
		if n == 0 {  // 当前字符串长度为0,continue
			continue
		}
		if l+n < l {             // 长度溢出时
			throw("string concatenation too long")
		}
		l += n                  // 长度 + n
		count++                 // 字符串个数+1
		idx = i                 // 当前index
	}
	if count == 0 {
		return ""
	}

	// If there is just one string and either it is not on the stack
	// or our result does not escape the calling frame (buf != nil),
	// then we can return that string directly.
	if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
		return a[idx]   // 如果只有一个字符串,缓冲区不为空,或不在栈上,直接返回
	}
	s, b := rawstringtmp(buf, l)   // 传入默认缓冲区,和字符串总长度, 返回字符串的和字符串中str对应的底层切片
	for _, x := range a {
		copy(b, x)       // 将所有字符拷贝进字符串的底层切片   拼接的核心逻辑
		b = b[len(x):]
	}
	return s
}
func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) {
	if buf != nil && l <= len(buf) {    // 字符串总长度 <= 默认缓冲区大小
		b = buf[:l]                     // 对默认缓冲区进行截取
		s = slicebytetostringtmp(&b[0], len(b)) // 对返回值的s 的len进行赋值,并将s 的str 绑定到buf
	} else {
		s, b = rawstring(l)                    // 对返回值的s的len进行赋值,并将s的str绑定到在堆空间新开辟的地址上
	}
	return
}

// slicebytetostringtmp returns a "string" referring to the actual []byte bytes.
//
// Callers need to ensure that the returned string will not be used after
// the calling goroutine modifies the original slice or synchronizes with
// another goroutine.
//
// The function is only called when instrumenting
// and otherwise intrinsified by the compiler.
//
// Some internal compiler optimizations use this function.
//   - Used for m[T1{... Tn{..., string(k), ...} ...}] and m[string(k)]
//     where k is []byte, T1 to Tn is a nesting of struct and array literals.
//   - Used for "<"+string(b)+">" concatenation where b is []byte.
//   - Used for string(b)=="foo" comparison where b is []byte.
func slicebytetostringtmp(ptr *byte, n int) (str string) {
	if raceenabled && n > 0 {
		racereadrangepc(unsafe.Pointer(ptr),
			uintptr(n),
			getcallerpc(),
			abi.FuncPCABIInternal(slicebytetostringtmp))
	}
	if msanenabled && n > 0 {
		msanread(unsafe.Pointer(ptr), uintptr(n))
	}
	if asanenabled && n > 0 {
		asanread(unsafe.Pointer(ptr), uintptr(n))
	}
	stringStructOf(&str).str = unsafe.Pointer(ptr)
	stringStructOf(&str).len = n
	return
}

在堆内存重新分配空间

// rawstring allocates storage for a new string. The returned
// string and byte slice both refer to the same storage.
// The storage is not zeroed. Callers should use
// b to set the string contents and then drop b.
func rawstring(size int) (s string, b []byte) {
	p := mallocgc(uintptr(size), nil, false)

	stringStructOf(&s).str = p
	stringStructOf(&s).len = size

	*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}

	return
}

画个图描述一下
在这里插入图片描述
字符串拼接的核心逻辑是:计算所要拼接的字符串长度,将长度赋值给待返回字符串底层的len字段;分配合理的存储空间(栈上或堆上),将存储空间的指针赋值给待返回字符串底层的str字段,返回栈上或堆上存储空间的切片,对切片使用copy内置函数进行填充,即对字符串的str字段进行拼接。

用代码模拟一下

	array := [30]byte{}
	str := *(*string)(unsafe.Pointer(&struct {
		str unsafe.Pointer
		len int
	}{
		str: unsafe.Pointer(&array),
		len: len(array),
	}))

	fmt.Println(str)
	fmt.Println(len(str))

	copy(array[:], "hello world") //
	fmt.Println(str)              // 30
	copy(array[12:], "你好世界")   // hello world
	fmt.Println(str)              // hello world你好世界 

强制转换byte切片到string——string([]byte)

// slicebytetostring converts a byte slice to a string.
// It is inserted by the compiler into generated code.
// ptr is a pointer to the first element of the slice;
// n is the length of the slice.
// Buf is a fixed-size buffer for the result,
// it is not nil if the result does not escape.
func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) {
	if n == 0 {
		// Turns out to be a relatively common case.
		// Consider that you want to parse out data between parens in "foo()bar",
		// you find the indices and convert the subslice to string.
		return ""
	}
	if raceenabled {
		racereadrangepc(unsafe.Pointer(ptr),
			uintptr(n),
			getcallerpc(),
			abi.FuncPCABIInternal(slicebytetostring))
	}
	if msanenabled {
		msanread(unsafe.Pointer(ptr), uintptr(n))
	}
	if asanenabled {
		asanread(unsafe.Pointer(ptr), uintptr(n))
	}
	if n == 1 {
		p := unsafe.Pointer(&staticuint64s[*ptr])
		if goarch.BigEndian {   // 大端存储
			p = add(p, 7)
		}
		stringStructOf(&str).str = p   // str 赋指针
		stringStructOf(&str).len = 1   // len 赋字符个数
		return
	}

	var p unsafe.Pointer
	if buf != nil && n <= len(buf) { // slice长度 <= 30 则借助buf空间在栈内赋值
		p = unsafe.Pointer(buf)
	} else {
		p = mallocgc(uintptr(n), nil, false)  // 分配可被gc的空间
	}
	stringStructOf(&str).str = p
	stringStructOf(&str).len = n
	memmove(p, unsafe.Pointer(ptr), uintptr(n)) // 移动ptr指向的底层数组中的n个字符到新的空间p
	return
}

如果只有一个字符,检查大端或小端后,直接赋值返回即可
如果sice的长度<=30,则不用开辟堆空间,直接借助buf在栈内操作
如果slice的长度>30,则分配堆空间
对新分配的空间赋值,返回

string 强转 byte slice —— []byte(string)

func stringtoslicebyte(buf *tmpBuf, s string) []byte {
	var b []byte
	if buf != nil && len(s) <= len(buf) {
		*buf = tmpBuf{}
		b = buf[:len(s)]
	} else {
		b = rawbyteslice(len(s))
	}
	copy(b, s)
	return b
}

string长度 <= 30 借助buf,否则在堆中创建空间,使用 copy 复制原值即可

[]rune(string)

func stringtoslicerune(buf *[tmpStringBufSize]rune, s string) []rune {
	// two passes.
	// unlike slicerunetostring, no race because strings are immutable.
	n := 0
	for range s {  // 统计字符个数
		n++
	}

	var a []rune
	if buf != nil && n <= len(buf) { //
		*buf = [tmpStringBufSize]rune{}
		a = buf[:n]
	} else {
		a = rawruneslice(n)
	}

	n = 0
	for _, r := range s {
		a[n] = r
		n++
	}
	return a
}

不需要竞态检测,因为string是不可变的
统计字符个数
在栈内(借助原有的buf)或堆内完成空间分配
逐个赋值
返回

string([]rune)

func slicerunetostring(buf *tmpBuf, a []rune) string {
	if raceenabled && len(a) > 0 {
		racereadrangepc(unsafe.Pointer(&a[0]),
			uintptr(len(a))*unsafe.Sizeof(a[0]),
			getcallerpc(),
			abi.FuncPCABIInternal(slicerunetostring))
	}
	if msanenabled && len(a) > 0 {
		msanread(unsafe.Pointer(&a[0]), uintptr(len(a))*unsafe.Sizeof(a[0]))
	}
	if asanenabled && len(a) > 0 {
		asanread(unsafe.Pointer(&a[0]), uintptr(len(a))*unsafe.Sizeof(a[0]))
	}
	var dum [4]byte
	size1 := 0
	for _, r := range a {
		size1 += encoderune(dum[:], r)  // 返回该rune类型占用多少byte
	}
	s, b := rawstringtmp(buf, size1+3)
	size2 := 0
	for _, r := range a {
		// check for race     竞态检测
		if size2 >= size1 {
			break
		}
		size2 += encoderune(b[size2:], r)
	}
	return s[:size2]
}

剩下几个函数,都比较简单,暂不做分析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/84000.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

焦脱镁叶绿酸-a修饰量子点/荧光/药物/小分子抑制剂/上转换纳米颗粒/树枝状聚合物

小编在这里为大家分享的科研内容是焦脱镁叶绿酸-a修饰量子点/荧光/药物/小分子抑制剂/上转换纳米颗粒/树枝状聚合物的相关研究&#xff0c;来看&#xff01; 焦脱镁叶绿酸-a简介&#xff1a; 焦脱镁叶绿素-a是产物叶绿素a通过脱甲氧羰基、去植物醇、去Mg后的产物。该类物质具有…

day19【代码随想录】删除字符串中的所有相邻重复项、逆波兰表达式求值、滑动窗口最大值、前 K 个高频元素、数组中的第K个最大元素

文章目录前言一、删除字符串中的所有相邻重复项&#xff08;力扣047&#xff09;二、逆波兰表达式求值&#xff08;力扣150&#xff09;三、滑动窗口最大值&#xff08;力扣239&#xff09;四、前 K 个高频元素&#xff08;力扣347&#xff09;五、数组中的第K个最大元素&#…

MyBatis系列---crud返回值

目录1. service与mapper2. 更新操作3. 查询操作3.1. 返回值存储3.2. 简单映射3.3. ResultSet 的预处理3.4. 确定 ResultMap3.5. 创建映射结果对象3.6. 自动映射3.7. 存储对象3.8. 返回结果为单行数据3.9. 返回结果为多行数据3.10. 结论1. service与mapper mybatis一般与spring…

深度活体模型带交互模型版

🍿*★,*:.☆欢迎您/$:*.★* 🍿

点击Tab标签切换不同查询数据,并选择数据存入缓存实现两个界面带参数跳转

项目场景&#xff1a; 在不同的tab标签页中点击不同的标签页查找不同的内容,然后选中其中一个页面中的一条数据将此数据某个信息选中然后存入session缓存当中然后另一个界面从session中取出,从而达到带参数跳转界面的需求 问题描述 可以做到跳转界面但是数据会显示到地址栏当…

做开发4年了,年薪还不如2年经验的测试。我该适应当下节奏吗...

代码码了这么些年&#xff0c;你年薪达到多少了&#xff1f; 我&#xff0c;4年码龄&#xff0c;薪资最高的时候16k*12薪&#xff0c;年薪不到20W。都说IT行业薪资高&#xff0c;但年薪百万的还是金字塔尖极少数&#xff0c;像我这样的才是普通的大多数&#xff0c;却也还要用…

电脑维护与故障处理

第一章 认识电脑的组成 1.1 硬件组成 1.1.1 CPU 1.1.2 主板 1.1.3 内存 1.1.4 硬盘 1.1.5 电源 1.1.6 显示器 1.1.7 键盘和鼠标 1.1.8 光驱 1.1.9 显卡 1.1.10 其他外部设备 1.2 软件组成 1.2.1 操作系统 Windows XP Windows 7 服务器操作系统 —— Windows Ser…

04-Nginx-conf配置文件基本了解

Nginx负载均衡&#xff0c;反向代理入门配置&#xff1a; nginx.conf整体结构 nginx入门基本配置 Nginx.conf配置文件详解&#xff08;upstream和location负载均衡和反向代理配置&#xff09;&#xff1a; #运行用户 user www-data; #启动进程,通常设置成和cpu的数量相等 wor…

基于边缘智能网关打造智慧体育场

运动健身是民众广泛存在的生活需求&#xff0c;体育场馆作为承载各种体育运动的基础设施&#xff0c;其运营管理效率、服务水平和智能化场景应用等都与用户体验紧密相关。 得益于物联网、边缘计算、AI智能等新技术的广泛应用&#xff0c;当前已有越来越多体育场馆通过部署基于…

数据结构与算法——Java实现稀疏数组和队列

目录 一、基本介绍 1.1 线性结构 1.2 非线性顺序结构 二、稀疏数组 2.1 基本介绍 2.1.1 应用场景 2.1.2 实现思路 2.2 代码实现 2.2.1 原始数组 2.2.2 原始数组转化为稀疏数组 2.2.3 稀疏数组转化为原始数组 三、队列的应用场景和介绍 3.1 数组模拟队列 3.1.1数组模拟队列的…

Find My资讯|Seinxon推出支持苹果 Find My 防丢卡

在美国&#xff0c;平均每个人每年丢失 3,000 件物品。而在 2021 年&#xff0c;Pixie 数据显示&#xff0c;丢失产品的更换成本超过 25 亿美元。每周超过两次&#xff0c;将近 1/4 的美国人丢失房门钥匙、钱包、宠物、电话、眼镜、耳机、遥控器、手提箱或孩子最喜欢的物品。 …

GIT系列(七)切换ssh连接,上传不再输入账号、密码

文章目录前言操作流程前言 使用HTTP连接方式时&#xff0c;上传代码总是需要登录&#xff0c;键盘都打坏了&#xff0c;切换SSH可以无需密码&#xff0c;直接上传。 操作流程 step 1 确保在git服务器已经部署本机公钥。 没有配置SSH的&#xff0c;戳这里 GIT系列&#xff08;…

k8s教程(18)-pod之DaemonSet(每个node上只调度一个pod)

文章目录01 引言02 DaemonSet2.1 应用场景2.2 举例2.3 注意事项03 文末01 引言 声明&#xff1a;本文为《Kubernetes权威指南&#xff1a;从Docker到Kubernetes实践全接触&#xff08;第5版&#xff09;》的读书笔记 DaemonSet是 Kubernetes1.2 版本新增的一种资源对象&#xf…

事件轮询机制 Event Loop、浏览器更新渲染时机、setTimeout VS setInterval

目录 1. 事件轮询机制&#xff08;Event Loop&#xff09;是什么 1.1 宏任务、微任务 1.2 Event Loop 循环过程 1.3 经典题目分析 1.3.1 第一轮事件循环 1.3.2 第二、三次事件循环 1.3.3 参考文章 2. async、await 在事件轮询中的执行时机 3. 浏览器更新渲染时机、Vue…

线上使用雪花算法生成id重复问题

项目中使用的是hutool工具类库提供的雪花算法生成id方式&#xff0c;版本使用的是5.3.1 <dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.3.1</version></dependency>雪花算法生成id…

10分钟数仓实战之kettle整合Hadoop

1.写在前面 很多朋友在做数仓的ETL的动作的时候&#xff0c;还是喜欢比较易上手的kettle 前面章节有介绍过安装kettle&#xff0c;可以参考 ETL工具--安装kettle_老码试途的博客-CSDN博客_spoon.bat 安装 kettle在Windows系统中对数据的转换、表和文件的转换等&#xff0c;…

Blender 3D环境场景创建教程

Blender 3D环境场景创建教程 学习 Blender 3.2&#xff0c;探索几何节点并创建美妙的 3D 环境 课程英文名&#xff1a;Creating 3D Environments in Blender 2.81 by Rob Tuytel (2019) 此视频教程共8.0小时&#xff0c;中英双语字幕&#xff0c;画质清晰无水印&#xff0c;…

腾讯云从业者基础认证完整笔记

腾讯云从业者基础认证完整笔记 就考这些&#xff0c;干就完事儿了&#xff01;不要介意图多哟&#xff0c;ppt能更好的表达意思呀 一、云计算基础 1.1 数据中心 一般企业要么自建数据中心EDC&#xff0c;EDC分层如下&#xff1a; 要么租用或者托管也就是IDC如下&#xff…

ZYNQ之FPGA学习----EEPROM读写测试实验

1 EEPROM简介 EEPROM (Electrically Erasable Progammable Read Only Memory&#xff0c;E2PROM)即电可擦除可编程只读存储器&#xff0c;是一种常用的非易失性存储器(掉电数据不丢失)。ZYNQ开发板上使用的是AT24C64&#xff0c;通过IIC协议实现读写操作。IIC通信协议基础知识…

Oracle 11g---基于CentOS7

Oracle 11g安装教程 以下步骤基于网络配置完成&#xff0c;并且能连接xshell和xftp工具 文章目录Oracle 11g安装教程1.将oracle压缩包拷贝到安装机器&#xff0c;指定目录中2.安装依赖包3.验证依赖包4.创建oracle用户5.创建oradata目录,解压oracle安装6.修改系统配置参数7.创建…