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

Golang 语言高效使用字符串的方法

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

北京

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

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

看不清楚,换张图片

免费获取短信验证码

Golang 语言高效使用字符串的方法

01介绍

在 Golang 语言中,string 类型的值是只读的,不可以被修改。如果需要修改,通常的做法是对原字符串进行截取和拼接操作,从而生成一个新字符串,但是会涉及内存分配和数据拷贝,从而有性能开销。本文我们介绍在 Golang 语言中怎么高效使用字符串。

02字符串的数据结构

在 Golang 语言中,字符串的值存储在一块连续的内存空间,我们可以把存储数据的内存空间看作一个字节数组,字符串在 runtime 中的数据结构是一个结构体 stringStruct,该结构体包含两个字段,分别是指针类型的 str 和整型的 len。字段 str 是指向字节数组头部的指针值,字段 len 的值是字符串的长度(字节个数)。


type stringStruct struct {
 str unsafe.Pointer
 len int
}

我们通过示例代码,比较一下字符串和字符串指针的性能差距。我们定义两个函数,分别用 string 和 *string 作为函数的参数。


var strs string = `Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.`

func str (str string) {
 _ = str + "golang"
}

func ptr (str *string) {
 _ = *str + "golang"
}

func BenchmarkString (b *testing.B) {
 for i := 0; i < b.N; i++ {
 str(strs)
 }
}

func BenchmarkStringPtr (b *testing.B) {
 for i := 0; i < b.N; i++ {
 ptr(&strs)
 }
}

output:


go test -bench . -benchmem string_test.go
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkString-16    21987604    46.05 ns/op   128 B/op    1 allocs/op
BenchmarkStringPtr-16   24459241    46.23 ns/op   128 B/op    1 allocs/op
PASS
ok  command-line-arguments 2.590s

阅读上面这段代码,我们可以发现使用字符串作为参数,和使用字符串指针作为参数,它们的性能基本相同。

虽然字符串的值并不是具体的数据,而是一个指向存储字符串数据的内存地址的指针和一个字符串的长度,但是字符串仍然是值类型。

03字符串是只读的,不可修改

在 Golang 语言中,字符串是只读的,它不可以被修改。


func main () {
 str := "golang"
 fmt.Println(str) // golang
 byteSlice := []byte(str)
 byteSlice[0] = 'a'
 fmt.Println(string(byteSlice)) // alang
 fmt.Println(str) // golang
}

阅读上面这段代码,我们将字符串类型的变量 str 转换为字节切片类型,并赋值给变量 byteSlice,使用索引下标修改 byteSlice 的值,打印结果仍未发生改变。

因为字符串转换为字节切片,Golang 编译器会为字节切片类型的变量重新分配内存来存储数据,而不是和字符串类型的变量共用同一块内存空间。

可能会有读者想到用指针修改字符串类型的变量存储在内存中的数据。


func main () {
 var str string = "golang"
 fmt.Println(str)
 ptr := (*uintptr)(unsafe.Pointer(&str))
 var arr *[6]byte = (*[6]byte)(unsafe.Pointer(*ptr))
 var len *int = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&str)) + unsafe.Sizeof((*uintptr)(nil))))
 for i := 0; i < (*len); i++ {
  fmt.Printf("%p => %c\n", &((*arr)[i]), (*arr)[i])
  ptr2 := &((*arr)[i])
  val := (*ptr2)
  (*ptr2) = val + 1
 }
 fmt.Println(str)
}

output:


go run main.go
golang
0x10c96d2 => g
unexpected fault address 0x10c96d2
fatal error: fault
[signal SIGBUS: bus error code=0x2 addr=0x10c96d2 pc=0x10a4c56]

阅读上面这段代码,我们可以发现在代码中尝试通过指针修改 string 类型的 str 变量的存储在内存中的数据,结果引发了 signal SIGBUS 运行时错误,从而证明 string 类型的变量是只读的。

我们已经知道字符串在 runtime 中的结构体包含两个字段,指向存储数据的内存地址的指针和字符串的长度,因为字符串是只读的,字符串被赋值后,它的数据和长度都不会被修改,所以读取字符串的长度,实际上就是读取字段 len 的值,复杂度是 O(1)。

在字符串比较时,因为字符串是只读的,不可修改的,所以只要两个比较的字符串的长度 len 的值不同,就可以判断这两个字符串不相同,不用再去比较两个字符串存储的具体数据。

如果 len 的值相同,再去判断两个字符串的指针是否指向同一块内存,如果 len 的值相同,并且指针指向同一块内存,则可以判断两个字符串相同。但是如果 len 的值相同,而指针不是指向同一块内存,那么还需要继续去比较两个字符串的指针指向的字符串数据是否相同。

04字符串拼接

在 Golang 语言中,关于字符串拼接有多种方式,分别是:

  • 使用操作符 +/+=
  • 使用 fmt.Sprintf
  • 使用 bytes.Buffer
  • 使用 strings.Join
  • 使用 strings.Builder

其中使用操作符是最易用的,但是它不是最高效的,一般使用场景是用于已知需要拼接的字符串的长度。

使用 fmt.Sprintf 拼接字符串,性能是最差的,但是它可以格式化,所以一般使用场景是需要格式化拼接字符串。

使用 bytes.Buffer 和使用 strings.Join 的性能比较接近,性能最高的字符串拼接方式是使用 strings.Builder 。

我准备对 strings.Builder 的字符串拼接方式多费些笔墨。

Golang 语言标准库 strings 中的 Builder 类型,用于在 Write 方法中有效拼接字符串,它减少了数据拷贝和内存分配。


type Builder struct {
 addr *Builder // of receiver, to detect copies by value
 buf []byte
}

Builder 结构体中包含两个字段,分别是 addr 和 buf,字段 addr 是指针类型,字段 buf 是字节切片类型,但是它的值仍然不允许被修改,但是字节切片中的值可以被拼接或者被重置。

Builder 提供了一系列 Write* 拼接方法,这些方法可以用于把新数据拼接到已存在的数据的末尾,同时如果字节切片的容量不够用,可以自动扩容。需要注意的是,只要触发扩容,就会涉及内存分配和数据拷贝。自动扩容规则和切片的扩容规则相同。

除了自动扩容,还可以手动扩容,Builder 提供的 Grow 方法,可以根据 int 类型的传参,扩充字节数量。因为扩容操作,会涉及内存分配和数据拷贝,所以调用 Grow 方法手动扩容时,Golang 也做了优化,如果当前字节切片的容量剩余字节数小于或等于传参的值, Grow 方法将不会执行扩容操作。手动扩容规则是原字节切片容量的 2 倍加上传参的值。

Builder 类型还提供了一个重置方法 Reset,它可以将 Builder 类型的变量重置为零值。被重置后,原字节切片将会被垃圾回收。

在了解完上述 Builder 的介绍后,相信读者已对 Builder 有了初步认识。下面我们通过代码看一下预分配字节数量和未分配字节数量的区别:


var lan []string = []string{
 "golang",
 "php",
 "javascript",
}

func stringBuilder (lan []string) string {
 var str strings.Builder
 for _, val := range lan {
 str.WriteString(val)
 }
 return str.String()
}

func stringBuilderGrow (lan []string) string {
 var str strings.Builder
 str.Grow(16)
 for _, val := range lan {
 str.WriteString(val)
 }
 return str.String()
}

func BenchmarkBuilder (b *testing.B) {
 for i := 0; i < b.N; i++ {
 stringBuilder(lan)
 }
}

func BenchmarkBuilderGrow (b *testing.B) {
 for i := 0; i < b.N; i++ {
 stringBuilderGrow(lan)
 }
}

output:


go test -bench . -benchmem builder_test.go
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
BenchmarkBuilder-16    13761441    81.85 ns/op   56 B/op    3 allocs/op
BenchmarkBuilderGrow-16   20487056    56.20 ns/op   48 B/op    2 allocs/op
PASS
ok  command-line-arguments 2.888s

阅读上面这段代码,可以发现调用 Grow 方法,预分配字节数量比未预分配字节数量的字符串拼接效率高。我们在可以预估字节数量的前提下,尽量使用 Grow 方法预先分配字节数量。

注意:第一,Builder 类型的变量在被调用之后,不可以再被复制,否则会引发 panic。第二,因为 Builder 类型的值不是完全不可修改的,所以使用者需要注意并发安全的问题。

05字符串和字节切片互相转换

因为切片类型除了只能和 nil 做比较之外,切片类型之间是无法做比较操作的。如果我们需要对切片类型做比较操作,通常的做法是先将切片类型转换为字符串类型。但是因为 string 类型是只读的,不可修改的,所以转换操作会涉及内存分配和数据拷贝。

为了提升转换的性能,唯一的方法就是减少或者避免内存分配的开销。在 Golang 语言中,运行时对二者的互相转换也做了优化,感兴趣的读者可以阅读 runtime 中的相关源码:


/usr/local/go/class="lazy" data-src/runtime/string.go

但是,我们还可以继续优化,实现零拷贝的转换操作,从而避免内存分配的开销,提升转换效率。

先阅读 reflect 中 StringHeader 和 SliceHeader 的数据结构:


// /usr/local/go/class="lazy" data-src/reflect/value.go

type StringHeader struct {
 Data uintptr // 指向存储数据的字节数组
 Len int // 长度
}

type SliceHeader struct {
 Data uintptr // 指向存储数据的字节数组
 Len int // 长度
 Cap int // 容量
}

阅读上面这段代码,我们可以发现 StringHeader 和 SliceHeader 的字段只缺少一个表示容量的字段 Cap,二者都有指向存储数据的字节数组的指针和长度。我们只需要通过使用 unsafe.Pointer 获取内存地址,就可以实现在原内存空间修改数据,避免了内存分配和数据拷贝的开销。

因为 StringHeader 比 SliceHeader 缺少一个表示容量的字段 Cap,所以通过 unsafe.Pointer 将 *SliceHeader 转换为 *StringHeader 没有问题,但是反之就不行了。我们需要补上一个 Cap 字段,并且将字段 Len 的值作为字段 Cap 的默认值。


func main () {
 str := "golang"
 fmt.Printf("str val:%s type:%T\n", str, str)
 strPtr := (*reflect.SliceHeader)(unsafe.Pointer(&str))
 // strPtr[0] = 'a'
 strPtr.Cap = strPtr.Len
 fmt.Println(strPtr.Data)
 str2 := *(*[]byte)(unsafe.Pointer(strPtr))
 fmt.Printf("str2 val:%s type:%T\n", str2, str2)
 fmt.Println((*reflect.SliceHeader)(unsafe.Pointer(&str2)).Data)
}

output:


go run main.go
golang
str val:golang type:string
17602449
str2 val:golang type:[]uint8
17602449

阅读上面这段代码,我们可以发现通过使用 unsafe.Pointer 把字符串转换为字节切片,可以做到零拷贝,str 和 str2 共用同一块内存,无需新分配一块内存。但是需要注意的是,转换后的字节切片仍然不能修改,因为在 Golang 语言中字符串是只读的,通过索引下标修改会引发 panic。

06总结

本文我们介绍了怎么高效使用 Golang 语言中的字符串,先是介绍了字符串在 runtime 中的数据结构,然后介绍了字符串拼接的几种方式,字符串与字节切片零拷贝互相转换,还通过示例代码证明了字符串在 Golang 语言中是只读的。更多关于字符串的操作,读者可以阅读标准库 strings 和 strconv 了解更多内容。

到此这篇关于Golang 语言高效使用字符串的方法的文章就介绍到这了,更多相关Golang 语言使用字符串内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

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

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

Golang 语言高效使用字符串的方法

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

下载Word文档

猜你喜欢

Golang语言怎么高效拼接字符串

这篇文章主要介绍了Golang语言怎么高效拼接字符串,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。01、介绍在编程语言中,字符串是一种重要的数据结构。在 Golang 语言中
2023-06-25

Go语言中高效的字符串拼接方法分享

Go语言是一门开源编程语言,由Google开发,具有高性能和简洁的特点。在Go语言中,字符串拼接是一项常见的操作。本文将分享一些高效的字符串拼接方法,帮助Go语言开发者提高代码的性能和效率。一、使用+号进行字符串拼接最简单的方法是使用+
Go语言中高效的字符串拼接方法分享
2024-03-12

go语言删除字符串字符的方法介绍

今天小编给大家分享的是go语言删除字符串字符的方法介绍,相信很多人都不太了解,为了让大家更加了解,所以给大家总结了以下内容,一起往下看吧。一定会有所收获的哦。 删除字符串的方法:1、用TrimSpace()来去除字符串空格;2、用Tri
2023-07-04

c语言查找字符串指定字符的方法

小编给大家分享一下c语言查找字符串指定字符的方法,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!C语言是什么C语言是一门面向过程的、抽象化的通用程序设计语言,广泛应用于底层开发,使用C语言可以以简易的方式编译、处理低级存储器
2023-06-14

Golang高效处理空格字符的方法

Golang高效处理空格字符的方法在Golang编程中,处理字符串中的空格字符是一个常见的任务。空格字符可以是空格、制表符、换行符等,在文本处理和字符串操作中经常会遇到。本文将介绍一些高效处理空格字符的方法,并附上相应的代码示例。方法一
Golang高效处理空格字符的方法
2024-03-12

Golang字符转整型的高效方法分享

go 中将字符高效转换为整型的指南:使用 strconv.parseint() 函数,提供要转换的字符串和基数。使用 fmt.scanf() 函数,指定格式化字符串以读取指定格式的数据。实战案例:使用 strconv.parseint()
Golang字符转整型的高效方法分享
2024-04-03

Linux strcat函数:字符串拼接的高效方法

strcat() 是一个 C 语言库函数,用于将两个字符串连接(拼接)在一起以下是 strcat() 函数的基本用法:#includechar dest[100] = "Hello, ";const char sr
Linux strcat函数:字符串拼接的高效方法
2024-09-14

Go语言字符串截取方法详解

Go语言字符串截取方法详解在Go语言中,字符串是不可变的字节序列,因此在进行字符串截取时需要使用一些方法来实现。字符串截取是获取字符串中的特定部分的一种常见操作,可以根据需求截取字符串的前几个字符、后几个字符或者从特定位置截取一定长度的字
Go语言字符串截取方法详解
2024-03-13

c语言处理字符串的方法有哪些

C语言处理字符串的方法有以下几种:1. 字符串赋值:使用strcpy函数将一个字符串赋值给另一个字符串。2. 字符串连接:使用strcat函数将两个字符串连接起来。3. 字符串比较:使用strcmp函数比较两个字符串是否相等。4. 字符串长
2023-08-24

c语言用scanf输入字符串的方法是什么

在C语言中,可以使用`scanf`函数来输入字符串。具体步骤如下:1. 声明一个字符数组来存储输入的字符串,例如 `char str[100];`。这里假设最大长度为100个字符。2. 使用`scanf`函数来读取输入的字符串。`scanf
2023-09-12

go语言字符串拼接的方法是什么

在Go语言中,可以使用`+`运算符或`fmt.Sprintf()`函数来进行字符串拼接。1. 使用`+`运算符```gostr1 := "Hello"str2 := "World"result := str1 + " " + str2```
2023-09-27

c语言字符串加密的方法是什么

一种常见的C语言字符串加密方法是使用简单的替换算法,也称为凯撒密码。该方法通过将字符串中的每个字符按照固定的偏移量进行替换,从而实现加密和解密。具体步骤如下:定义一个偏移量(比如3),用于对字符串中的字符进行替换。遍历字符串中的每个字符
c语言字符串加密的方法是什么
2024-03-02

c语言字符串输出的方法是什么

在C语言中,可以使用printf函数来输出字符串。下面是一个简单的例子:#include int main() {char str[] = "Hello, World!";printf("%s\n", str); //
c语言字符串输出的方法是什么
2024-03-11

编程热搜

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

目录