string简介
字符串(string)是 Go 语言提供的一种基础数据类型,在我们编程过程中,字符串可以说是我们使用的最多的一个数据结构了,凡是涉及到文本处理的地方,我们都会用到字符串。
在 go 语言中,字符串实际上是一个只读的字节切片。
string底层结构
// src/runtime/string.go
type stringStruct struct {
str unsafe.Pointer
len int
}
- str :指向字符串的首地址
- len :表示字符串的长度(
len
存储实际的字节数,而非字符数。所以对于非单字节编码的字符)
定义一个字符串
data:= "Hello"
实际结构如下:
字符串声明
golang以字面量来声明字符串有两种方式,双引号和反引号:
str1 := "Hello World"
str2 := `Hello
Golang`
使用双引号声明的字符串和其他语言中的字符串没有太多的区别,但是这种使用双引号的字符串只能用于单行字符串的初始化,当字符串里使用到一些特殊字符,比如双引号,换行符等等需要用进行转义。但是,反引号声明的字符串没有这些限制,字符内容即为字符串里的原始内容,所以一般用反引号来声明的比较复杂的字符串,比如json串。
基本操作
package main
import "fmt"
func main() {
// 使用字符串字面量初始化
var a = "a,星"
fmt.Println(a) //a,星
// 可以使用下标访问,但不可修改
fmt.Printf("a[0] is %d\n", a[0]) //a[0] is 97
fmt.Printf("a[0:2] is %s\n", a[0:2]) //a[0:2] is a,
//a[0] = 'a' 编译报错,Cannot assign to a[0]
//
// 字符串拼接
var b = a + "狗"
fmt.Printf("b is %s\n", b) //b is a,星狗
// 使用内置 len() 函数获取其长度
fmt.Printf("a's length is: %d\n", len(a)) //a's length is: 5
// 使用 for;len 遍历
for i := 0; i < len(a); i++ {
fmt.Println(i, a[i])
}
//0 97
//1 44
//2 230
//3 152
//4 159
// 使用 for;range 遍历
for i, v := range a {
fmt.Println(i, v)
}
//0 97
//1 44
//2 26143
}
为什么只读
-
安全性:字符串的不可变性有助于防止数据竞争和意外修改。在多线程或并发环境中,如果字符串是可变的,那么一个goroutine可能在另一个goroutine不知情的情况下修改了字符串的内容,从而导致难以调试的错误。
-
简化内存管理:由于字符串是不可变的,一旦字符串被创建,其内容就不会改变。这意味着Go运行时可以安全地共享字符串实例,而不需要担心一个实例的内容会被另一个操作修改。这种共享减少了内存分配和复制的需要,提高了程序的效率。
-
一致性:保持与其他现代编程语言(如 Java、Python、C#)的设计一致性,使开发者在不同语言之间切换时更容易理解和使用字符串。
对string使用len()
len("hello") = 5,而len("你好") = 6
在 Go 语言中,len
函数返回字符串的字节长度,而不是字符长度。这是因为 Go 语言中的字符串是以 UTF-8 编码存储的。
-
对于字符串
"hello"
,每个字符都用一个字节表示,因为它们是 ASCII 字符。 因此,len("hello")
返回 5。 -
对于字符串
"你好"
,每个汉字用三个字节表示(UTF-8 编码下)。所以"你好"
中有两个汉字,每个汉字占三个字节,总共是 6 个字节。因此,len("你好")
返回 6。
这是因为 UTF-8 是一种可变长度的字符编码,对于不同的字符,使用的字节数是不同的。具体来说:
- ASCII 字符(U+0000 至 U+007F)用 1 个字节表示。
- 其他的字符会使用 2 到 4 个字节表示,具体取决于字符的 Unicode 代码点。
因此,在使用 len
函数时,需要注意它返回的是字节数,而不是字符数。如果需要获取字符串中的字符数,可以将字符串转换为 rune 切片:
package main
import (
"fmt"
)
func main() {
fmt.Println(len("hello")) // 输出 5
fmt.Println(len("你好")) // 输出 6
fmt.Println(len([]rune("你好"))) // 输出 2
}
在上面的代码中,[]rune("你好")
将字符串转换为 rune 切片,每个 rune 表示一个 Unicode 字符,因此可以正确地计算出字符的数量。
string和[]byte之间的转化
前面说了,string是只读的,不可以被改变,但是可以将字符串转换为字节切片,然后通过下标修改字节切片,再转为字符串。
package main
import "fmt"
func main() {
var ss string
ss = "Hello"
strByte := []byte(ss)
strByte[1] = 65
fmt.Println(string(strByte)) //hAllo
}
虽然这种方法看似可行,修改了字符串Hello,但其实最终得到的只是ss字符串的一个拷贝,源字符串并没有改变。要注意的是用这种方法去修改中文结果会是一串乱码,因为中文并不是占一个字节。
[]byte转化为string的原理
string与[]byte的转化其实会发生一次内存拷贝,并申请一块新的数组(切片的底层是数组)内存空间。
[]byte转化为string是否一定会发生内存拷贝
很多场景中会用到[]byte转化为string,但是并不是每一次转化都会发生内存拷贝,转化为的字符串被用于临时场景就不会发生内存拷贝。
- 字符串比较:string(ss)=="hello"
- 字符串拼接:"hello" + string(ss) + "world"
- 用作查找:key,val := map[string(ss)]
这几种情况下,[]byte转化成的字符串并不会被后面的程序用到,只是在当下场景被临时用到,所有不会发生内存拷贝,而是直接返回一个string,这个string的指针指向切片的内存
字符串拼接
go语言中字符串是不可改变的,所以在对字符串进行拼接的时候会有内存的拷贝,存在性能损耗。常见的字符串拼接方式:
方法 | 说明 |
+ | +拼接2个字符串时,会生成一个新的字符串,开辟一段新的内存空间,新空间的大小是原来两个字符串的大小之和,所以没拼接一次买就要开辟一段空间,性能很差 |
Sprintf | Sprintf 会从临时对象池中获取一个对象,然后格式化操作,最后转化为string,释放对象,实现很复杂,性能也很差 |
strings.Bulider | 底层存储使用[] byte,转化为字符串时可复用,每次分配内存的时候,支持预分配内存并且自动扩容,所以总体来说,开辟内存的次数就少,性能最好 |
bytes.Buffer | 底层存储使用[] byte,转化为字符串时不可复用,底层实现和strings.Builder差不多,性能比strings.Builder略差一点,区别是bytes.Buffer转化为字符串时重新申请了一块空间,存放生成的字符串变量,而strings.Builder直接将底层的[]byte转换成了字符串类型返回了回来,性能仅次于strings.Builder |
append | 直接使用[]byte扩容机制,可复用,支持预分配内存和自动扩容,性能只比+和Sprintf好,但是如果能提前分配好内存的话,性能将会仅次于strings.Bulider |
string.Join | strings.join的性能约等于strings.builder,在已知字符串slice的时候可以使用,未知时不建议使用,构造切片也是会有性能损耗的 |
空string不为nil
最后一点注意var s string
定义了一个字符串的空值,字符串的空值是空字符串,即""
。字符串不可能为nil
。