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 语言中,关于字符串拼接有多种方式,分别是:
- 使用操作符 +/+=
?j_
kAM!M!r'^cb>73"Gr
__1j_
j4(4)4(йAq4(йM!A4(l4(
14(йAф4(muA4(йAq4(йAйM!Aф4)4(4(4)4)4)靽4(4)靽4(4(4(b+vG>>G:kRA*+__*"&k"n.J0RB3v_crZ"7v_crΣ?jb6B;j_*"&R3nr_b>k/W>D4(LZ"G;cV#j_#b_rjV6zB;_.j7Z?__*"&n.n6ckb;_rb>nk_jN73>bJ0nk4("cV#_jZWjZ"nkn_碾Bs&7jZ"[#vjnZroB;kkR2 |