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

GoLangstring类型深入分析

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

北京

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

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

看不清楚,换张图片

免费获取短信验证码

GoLangstring类型深入分析

文章运行环境:go version go1.16.6 darwin/amd64

并发不安全

看下面的代码,大家觉得会输出什么?大多数人应该都会觉得输出""、abc、neoj 这三种情况,但真实的情况并不是这样,真实情况是只输出 “” 空字符串。

结合日常的工作,类似这种并发操作同一个变量的情况也比较常见,为什么业务没有发生异常问题?

var name string = ""
func main() {
	go func() {
		for {
			name = "abc"
		}
	}()
	go func() {
		for {
			name = "neoj"
		}
	}()
	for {
		fmt.Println(name)
	}
}

1.14 之后引入了 G 抢占式调度,那为什么代码中的两个协程没有执行呢?其实是编译器做了优化,这两个协程被省略掉了。

我们对代码做一点调整,在协程中加一行空的输出,输出结果中出现了一些特例,比如:neo、abca。其中,neo 字符串长度等于 abc 的长度,而 abca 的长度等于 neoj 的长度。

var name string = ""
func main() {
	go func() {
		for {
			name = "abc"
			fmt.Printf("")
		}
	}()
	go func() {
		for {
			name = "neoj"
			fmt.Printf("")
		}
	}()
	for {
		if name != "abc" && name != "neoj" {
			fmt.Println(name)
		}
	}
}

例子说明,string 的赋值并不是原子的。

Go 语言中 string 的内存结果如下,它包含两部分:Data 表示实际的数据部分,而 Len 表示字符串的长度。

所以,通过方法 len 来计算字符串的长度并不会有性能开销,len 方法会直接返回结构体的 Len 属性;而传递字符串类型的参数,使用指针类型和值类型,性能上也不会有太大差别。

type StringHeader struct {
	Data uintptr
	Len  int
}

字符串的并发不安全,主要就是给这两个字段的赋值,没有办法保证原子性。参考 runtime/string.go 中的源码,我们可以了解字符串生成过程。

并发赋值的情况下,Data 指向的地址和 Len 无法保证一一对应。所以,通过 Data 获取到内存的首地址,通过 Len 去读取指定长度的内存时,就会出现内存读取异常的情况。

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
}

rawstring 函数在字符串拼接的时候被调用,我们代码中创建一个字符串类型,每次都生成一份新的内存空间。特别强调,创建和字符串赋值需要区分开来。赋值的过程其实是值拷贝,拷贝的便是 StringHeader 结构体。

var name string = ""
func main() {
	blog := name
	fmt.Println(blog)
}

上面的变量 blog 是 name 的值拷贝,底层指向的字符串是同一块内存空间。这个赋值过程中,发生拷贝的只是外层的 StringHeader 对象。

Go 中通过 unsafe 包可以强制对内存数据做类型转换,我们将 blog 和 name 的内存地址打印出来比较一下。最终打印输出两个变量的地址和Data地址。可以看出,赋值前后,Data指向的地址并没有发生变化。

type StringHeader struct {
	Data uintptr
	Len  int
}
var name string = "g"
func main() {
	blog := name
	n := (*StringHeader)(unsafe.Pointer(&name))
	b := (*StringHeader)(unsafe.Pointer(&blog))
	fmt.Println(&n, n.Data)    // 0xc00018a020 17594869
	fmt.Println(&b, b.Data)    // 0xc00018a028 17594869
}

string 并发不安全读写,会导致线上服务偶发 panic。比如使用 json 对内存异常的 string 做序列化的时候。下面的例子中,其中一个协程用来赋值为空,非常容易复现 panic。

type People struct {
	Name string
}
var p *People = new(People)
func main() {
	go func() {
		for {
			p.Name = ""
		}
	}()
	go func() {
		for {
			p.Name = "neoj"
		}
	}()
	for {
		_, _ = json.Marshal(p)
	}
}

下面是 panic 的堆栈信息,空字符串的 Data 指向的是 nil 的地址,而并发导致 Len 字段有值,最终导致发生 panic。

竞态竞争

对同一个变量并发读写,如果没有使用辅助的同步操作,就会出现不符合预期的情况。直白的讲,我们开发完一个程序之后,针对同样的输入,会输出什么结果,我们是不确定的。

可以参考 The Go Memory Model 的介绍,强调一下数据竞争的概念:

A data race is defined as a write to a memory location happening concurrently with another read or write to that same location, unless all the accesses involved are atomic data accesses as provided by the sync/atomic package

幸运的是,Go 已经集成了现成的工具来诊断数据竞争:-race。在 go build、或者直接执行的时候,指定 -race 属性,系统会做数据竞争检测,并打印输出。

以最近的代码为例,如果你使用的也是 goland 编译器,只需要在 Run Configurations / Go tool arguments 中指定 -race 属性,运行程序,就会出现下面的检测结果:

面对生产环境,-race 有比较严重的性能开销,我们最好是开发环境做竞态检测。

-race 是通过编译器注入代码来执行检测的,在函数执行前、执行后都会做内存统计。也就是说:只有被执行到的代码才能被检测到。所以,如果开发阶段做竞态检测的话,一定要保证代码被执行到了。

再加上埋点的内存统计也是有策略的,也不可能保证存在数据竞争的代码就一定会被检测出来,最好可以多执行几次来避免这种情况。

字符串优化

因字符串并发读写导致的 panic,很容易被 Go 的字符串优化带偏。

我在第一次遇到这种情况的时候,想到的居然是:会不会是底层优化导致的。因为发生 panic 的代码用到了 map 的数据结构。这种想法很快被我用测试用例排除了。

[]byte 到 string 类型转换是比较常规的操作,正常情况下,转换都会申请了一份新的内存空间。但 Go 为了提高性能,在某些场景下 string 和 []byte 会共用一份内存空间,这种场景下也能写乱内存。

// slicebytetostringtmp returns a "string" referring to the actual []byte bytes.
//
func slicebytetostringtmp(ptr *byte, n int) (str string) {
	if raceenabled && n > 0 {
		racereadrangepc(unsafe.Pointer(ptr),
			uintptr(n),
			getcallerpc(),
			funcPC(slicebytetostringtmp))
	}
	if msanenabled && n > 0 {
		msanread(unsafe.Pointer(ptr), uintptr(n))
	}
	stringStructOf(&str).str = unsafe.Pointer(ptr)
	stringStructOf(&str).len = n
	return
}

程序中出现问题,还是要先充分审查自己开发的代码

到此这篇关于GoLang string类型深入分析的文章就介绍到这了,更多相关Go string内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

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

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

GoLangstring类型深入分析

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

下载Word文档

猜你喜欢

GoLangstring类型深入分析

string作为go语言中的基础类型,其实有一些需要反复揣摩的,可能是我们使用的场景太简单,也可能是我们不需要那可怜的一点优化来提高性能,对它也就没那么上心了
2023-01-28

深入分析MySQL数据类型 DECIMAL

前言: 当我们需要存储小数,并且有精度要求,比如存储金额时,通常会考虑使用DECIMAL字段类型,可能大部分同学只是对DECIMAL类型略有了解,其中的细节还不甚清楚,本篇文章将从零开始,为你讲述DECIMAL字段类型的使用场景及方法。
2022-05-24

深入解析静态定位类型的分类与特点

静态定位类型是CSS中一种常用的定位方式,它允许我们将元素相对于其正常的文档流位置进行精确的定位。本文将详细介绍静态定位类型所包含的种类。CSS中的静态定位类型包括:块级元素,行内元素,浮动元素,绝对定位和固定定位。每种定位类型都有其特点
深入解析静态定位类型的分类与特点
2024-01-29

深入剖析Python数据类型:从基础类型到复杂类型

Python数据类型全解析:从基本类型到复合类型,需要具体代码示例概述:在Python编程中,数据类型是非常重要的概念。Python提供了丰富的数据类型,包括基本类型和复合类型。本文将对Python的数据类型进行全面解析,讲解它们的特点、
深入剖析Python数据类型:从基础类型到复杂类型
2024-01-20

深入浅析Java8中的类型注解

这篇文章将为大家详细讲解有关深入浅析Java8中的类型注解,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。什么是类型注解在java 8之前,注解只能是在声明的地方所使用,比如类,方法,属性;j
2023-05-31

如何进行void类型深层的分析

今天就跟大家聊聊有关如何进行void类型深层的分析,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。下面将对void关键字的深刻说明,并详述void及void指针类型的使用方法与技巧,初
2023-06-17

深入分析C语言存储类型与用户空间内部分布

这篇文章主要介绍了C语言存储类型与用户空间内部分布,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
2022-12-26

深入浅析Java8中的目标类型推断

这篇文章将为大家详细讲解有关深入浅析Java8中的目标类型推断,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。简单理解泛型泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说
2023-05-31

PHP 高级特性解析:深入了解动态类型和弱类型

php动态类型允许变量在运行时确定其类型,提供灵活性和弱类型比较不同类型表达式。实际案例包括表单数据处理、数组处理和数据库查询。注意事项包括匹配比较类型、使用严格比较运算符和类型标注。通过理解和谨慎使用,开发者可以利用这些特性编写强大可靠的
PHP 高级特性解析:深入了解动态类型和弱类型
2024-05-08

探索Python数据类型:深入剖析Python数据类型的特点

Python数据类型解析:深入研究Python的数据类型,需要具体代码示例导言:在Python编程中,数据类型是非常重要的概念。了解不同的数据类型及其特性,可以帮助我们更好地处理和操作数据。本文将深入研究Python的各种数据类型,并提供
探索Python数据类型:深入剖析Python数据类型的特点
2024-01-20

深入源码解析Python中的对象与类型

对象 对象, 在C语言是如何实现的 Python中对象分为两类: 定长(int等), 非定长(list/dict等) 所有对象都有一些相同的东西, 源码中定义为PyObject和PyVarObject, 两个定义都有一个共同的头部定义PyO
2022-06-04

深入浅析Java中 class文件的数据类型

深入浅析Java中 class文件的数据类型?相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。CONSTANT_Integer_info一个常量池中的CONSTANT_Intege
2023-05-31

编程热搜

  • 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动态编译

目录