go 语言 string 类型思考

news2025/1/13 14:05:29

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

文章运行环境: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
}

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

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

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

相关文章

浅析Java中的final关键字

一.final关键字的基本用法 在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。下面就从这三个方面来了解一下final关键字的基本用法。 1.修饰类 当用final修饰一个类时,表明这个类不能被继承。也就是说&a…

ACL访问控制的基本实例

典型案例: 配置需求∶ 在Router上部署基本ACL后,ACL将试图穿越Router的源地址为192.168.1.0/24网段的数据包过滤掉,并放行其他流量,从而禁止192.168.1.0/24网段的用户访问Router右侧的服务器网络。 配置: 1、Router已…

第三章 变量

一、数据类型(P40) 每一种数据都定义了明确的数据类型,在内存中分配了不同大小的内存空间(字节)。二、整数类型 整型的使用细节: (1)Java 各整数类型有固定的范围和字段长度,不受具体OS【操作系…

Java多线程(四)——ThreadPoolExecutor源码解析

ThreadPoolExecutor源码解析 多线程场景下,手动创建线程有许多缺点: 频繁创建、销毁线程会消耗大量 CPU 资源,销毁线程后需要被回收,对 GC 垃圾回收也有一定的压力 使用线程池有许多好处: 降低 CPU 资源消耗。通过…

Linux运维之解决服务器挖矿木马问题

文章目录1 挖矿木马1.1 定义1.2 挖矿特征1.3 解决挖矿木马1.3.1 阻断异常网络通信(非必需)1.3.2 清除定时任务1.3.3 清除启动项1.3.4 清除SSH公钥1.3.5 清除木马进程1.4 其他常见问题1.4.1 清除木马后又100%1.4.2 CPU占用100%却看不到进程1 挖矿木马 1.…

Python OS 文件目录方法 os.walk()

Python OS 文件/目录方法 os.walk() 概述 os.walk() 方法用于通过在目录树中游走输出在目录中的文件名,向上或者向下。 os.walk() 方法是一个简单易用的文件、目录遍历器,可以帮助我们高效的处理文件、目录方面的事情。 在Unix,Windows中…

BFS(三)腐烂的橘子(感染问题)

994. 腐烂的橘子 在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一: 值 0 代表空单元格; 值 1 代表新鲜橘子; 值 2 代表腐烂的橘子。 每分钟,腐烂的橘子 周围 4 个方向上相邻 的…

领导看到我自用的IDEA插件,也回去悄悄安装了...

现在哪有程序员离得开 Idea 啊!没有 Idea 的程序员那还有灵魂吗?那没有!既然我们都用 Idea,如何提高 Idea 的开发效率,在开发工具上,我们就卷掉其他小伙伴呢!今天鸡翅老哥就来给大家介绍几款我一…

函数的认识

文章目录 函数是什么库函数 自定义函数 函数参数 函数调用 函数的嵌套调用和链式访问 函数的声明和定义 函数递归一、函数是什么 维基百科中对函数的定义:子程序在计算机科学中,子程序(英语:Subroutine, procedure, functio…

SpringBoot+Vue项目课程作业管理系统

文末获取源码 开发语言:Java 框架:springboot JDK版本:JDK1.8 服务器:tomcat7 数据库:mysql 5.7/8.0 数据库工具:Navicat11 开发软件:eclipse/myeclipse/idea Maven包:Maven3.3.9 浏…

NodeJS Web 框架 Express 之路由

NodeJS Web 框架 Express 之路由参考描述路由路由匹配规则顺序匹配模块化创建使用前缀参考 项目描述哔哩哔哩黑马程序员搜索引擎Bing 描述 项目描述Edge109.0.1518.61 (正式版本) (64 位)NodeJSv18.13.0nodemon2.0.20Express4.18.2 路由 在 Web 中,路由可以理解为…

体验 micronaut 微服务框架

体验 micronaut 微服务框架谁在使用 MICRONAUT主要特点代码示例展示几点特性原生云原生安装 Micronaut 命令行工具创建一个 MICRONAUT 应用程序MICRONAUT是基于 JVM 的现代全栈框架,用于构建模块化、易于测试的微服务和无服务器应用程序。 谁在使用 MICRONAUT 主要…

数学建模——评价算法

层次分析法(AHP) 步骤 1.建立层次结构模型; 2.构造判断(成对比较)矩阵; 3.层次单排序及其一致性检验; 4.层次总排序及其一致性检验; 建立层次结构模型 将决策的目标、考虑的因素(决策准则)和决策对象按…

【HBase入门】5. 常用 Shell 操作(2)

前言 我们可以以shell的方式来维护和管理HBase。例如:执行建表语句、执行增删改查操作等等。 导入测试数据集 需求 在资料的 数据集/ ORDER_INFO.txt 中,有一份这样的HBase数据集,我们需要将这些指令放到HBase中执行,将数据导入…

申请Moonbeam Accelerator孵化计划申请答题指导

Moonbeam Accelerator是一个为期10 周的孵化计划,由Moonbeam基金会、Arrington Capital和Rokk3r共同推出,旨在帮助初创团队提高技术、业务、营销、金融和融资技能,助力您的Web3创业之梦。 申请孵化计划有任何限制吗?没有&#xff…

BFS(二)二叉树层序遍历(I、II)、二叉树锯齿形层序遍历、N叉树层序遍历

目录 102. 二叉树的层序遍历 107. 二叉树的层序遍历 II 103. 二叉树的锯齿形层序遍历 429. N 叉树的层序遍历 102. 二叉树的层序遍历 给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。…

VUE3/TS/TSX入门手册指北

VUE3入门手册vue3入门首先 查看 官方文档:https://cn.vuejs.org/guide/quick-start.html如果有vue2基础,速成课程:https://www.zhoulujun.co/learning-vue3/component.html(官方文档 还是建议 翻一遍)VUE3深入首先看源…

STM32+python产生三角波

目录任务目标实现方法python制作数表由于项目需要,需要产生一个三角波,需要覆盖4000个点的一个数组,这样的数组点数太多了,肯定不能自己一个一个手写了。最简单的一个方法是在嵌入式程序中用C写一个函数,对一个数组&am…

基于蜣螂优化的Elman神经网络数据预测-附代码

基于蜣螂算法优化的Elman神经网络数据预测 - 附代码 文章目录基于蜣螂算法优化的Elman神经网络数据预测 - 附代码1.Elman 神经网络结构2.Elman 神经用络学习过程3.电力负荷预测概述3.1 模型建立4.基于蜣螂优化的Elman网络5.测试结果6.参考文献7.Matlab代码摘要:针对…

LwIP系列--内存管理(内存池)详解

一、目的在《LwIP系列--内存管理(堆内存)详解》中我们详细介绍了LwIP中内存堆的实现原理,本篇我们介绍LwIP中内存池的实现细节。在LwIP源码中为了满足特定内存分配的需要以及优化内存占用制定了各种尺寸大小的内存池(每种内存池管…