共计 3434 个字符,预计需要花费 9 分钟才能阅读完成。
一、String 构造定义
// src/runtime/string.go:stringStruct
type stringStruct struct {
str unsafe.Pointer
len int
}
String 类型在 Go 语言内存模型中其实是一个“描述符”,用一个 2 字节的数据结构示意,它自身并不真正存储字符串数据,而仅是由一个指向底层存储的指针和字符串的长度字段组成的。
str:指向字符串底层存储首地址的指针,1 字节。
len:字符串的长度,1 字节。
因而,咱们即使间接将 String 类型变量作为函数参数,其传递的开销也是恒定的,不会随着字符串大小的变动而变动。
二、String 个性
1. String 类型的值在它的生命周期内不可扭转
type stringStruct struct {
str unsafe.Pointer
len int
}
func main() {
var s string = "hello"
s[0] = 'a' // 谬误:无奈给 s[0]赋值,因为字符串内容是不可扭转的
fmt.Printf("%#v\n", (*stringStruct)(unsafe.Pointer(&s)))
// 输入:&main.stringStruct{str:(unsafe.Pointer)(0x2b599c), len:5}
s = "world" // 批改字符串,字符串底层构造体中 str 指针曾经发生变化
fmt.Printf("%#v\n", (*stringStruct)(unsafe.Pointer(&s)))
// 输入:&main.stringStruct{str:(unsafe.Pointer)(0x2b5a00), len:5}
fmt.Printf("%#v\n", (*[5]byte)((*stringStruct)(unsafe.Pointer(&s)).str))
// 输入:&[5]uint8{0x77, 0x6f, 0x72, 0x6c, 0x64} 别离对应 w o r l d 的 ASCII 码
}
因为 runtime.stringStruct 构造是非导出的,不能间接应用,所以手动定义了一个 stringStruct 构造体。
String 类型的数据不可扭转的个性,进步了字符串的 并发安全性 和存储利用率。
- 字符串能够被多个协程共享,开发者不必再放心字符串的并发平安问题。
- 针对同一个字符串值,无论它在程序的几个地位被应用,编译器只须要为它调配一块存储,大大提高了存储利用率。
2. 没有结尾’\0’,存储了字符串长度
Go 字符串中没有结尾’\0’,并且存储了字符串长度,获取字符串长度的工夫复杂度是常数,无论字符串中字符个数有多少,咱们都能够疾速失去字符串的长度值。
3. String 能够是空的""
,但不能是 nil
var s string = ""
s = nil // 谬误
4. 对非 ASCII 字符提供原生反对,打消了源码在不同环境下显示乱码的可能
Go 语言源文件默认采纳的是 Unicode 字符集,Unicode 字符集是目前市面上最风行的字符集,它囊括了简直所有支流非 ASCII 字符(包含中文字符)。
Go 字符串中的每个字符都是一个 Unicode 字符,并且这些 Unicode 字符是以 UTF-8 编码格局存储在内存当中的。
5. 原生反对“所见即所得”的原始字符串,大大降低结构多行字符串时的心智累赘。
var s string = ` ,_---~~~~~----._
_,,_,*^____ _____*g*\"*,--,
/ __/ /' ^. / \ ^@q f
[@f | @)) | | @)) l 0 _/
\/ \~____ / __ \_____/ \
| _l__l_ I
} [______] I
] | | | |
] ~ ~ |
| |
| |`
fmt.Println(s)
三、String 惯例操作
1. 下标操作
在字符串的实现中,真正存储数据的是底层的数组。字符串的下标操作实质上等价于底层数组的下标操作。咱们在后面的代码中理论碰到过针对字符串的下标操作,模式是这样的:
var s = "乘风破浪"
fmt.Printf("0x%x\n", s[0]) // 0xe4:字符“乘”utf- 8 编码的第一个字节
咱们能够看到,通过下标操作,咱们获取的是字符串中特定下标上的字节,而不是字符。
2. 字符迭代
Go 有两种迭代模式:惯例 for 迭代 与 for range 迭代 。
通过这两种模式的迭代对字符串进行操作失去的后果是不同的。
通过惯例 for 迭代对字符串进行的操作是一种字节视角的迭代,每轮迭代失去的的后果都是组成字符串内容的一个字节,以及该字节所在的下标值,这也等价于对字符串底层数组的迭代:
var s = "乘风破浪"
for i := 0; i < len(s); i++ {fmt.Printf("index: %d, value: 0x%x\n", i, s[i])
}
输入:
index: 0, value: 0xe4
index: 1, value: 0xb9
index: 2, value: 0x98 // "\xe4\xb9\x98" 乘
index: 3, value: 0xe9
index: 4, value: 0xa3
index: 5, value: 0x8e // "\xe9\xa3\x8e" 风
index: 6, value: 0xe7
index: 7, value: 0xa0
index: 8, value: 0xb4 // "\xe7\xa0\xb4" 破
index: 9, value: 0xe6
index: 10, value: 0xb5
index: 11, value: 0xaa // "\xe6\xb5\xaa" 浪
通过 for range 迭代,咱们每轮迭代失去的是字符串中 Unicode 字符的码点值,以及该字符在字符串中的偏移值:
var s = "乘风破浪"
for i, v := range s {fmt.Printf("index: %d, value: 0x%x\n", i, v)
}
输入:
index: 0, value: 0x4e58
index: 3, value: 0x98ce
index: 6, value: 0x7834
index: 9, value: 0x6d6a
3. 字符串连贯
尽管通过 +/+= 进行字符串连贯的开发体验是最好的,但连贯性能就未必是最快的了。
Go 还提供了 strings.Builder、strings.Join、fmt.Sprintf 等函数来进行字符串连贯操作。
4. 字符串比拟
Go 字符串类型反对各种比拟关系操作符,包含 ==
、!=
、>=
、<=
、>
和 <
。在字符串的比拟上,Go 采纳字典序的比拟策略,别离从每个字符串的起始处,开始一一字节地对两个字符串类型变量进行比拟。
当两个字符串之间呈现了第一个不雷同的元素,比拟就完结了,这两个元素的比拟后果就会做为串最终的比拟后果。如果呈现两个字符串长度不同的状况,长度比拟小的字符串会用空元素补齐,空元素比其余非空元素都小。
如果两个字符串的长度不雷同,那么咱们不须要比拟具体字符串数据,也能够判定两个字符串是不同的。然而如果两个字符串长度雷同,就要进一步判断,数据指针是否指向同一块底层存储数据。如果还雷同,那么咱们能够说两个字符串是等价的,如果不同,那就还须要进一步去比对理论的数据内容。
func main() {
// ==
s1 := "乘风破浪"
s2 := "乘风" + "破浪"
fmt.Println(s1 == s2) // true
// !=
s1 = "Go"
s2 = "PHP"
fmt.Println(s1 != s2) // true
// < and <=
s1 = "12345"
s2 = "23456"
fmt.Println(s1 < s2) // true
fmt.Println(s1 <= s2) // true
// > and >=
s1 = "12345"
s2 = "123"
fmt.Println(s1 > s2) // true
fmt.Println(s1 >= s2) // true
}
第五个操作:字符串转换。
Go 反对字符串与字节切片、字符串与 rune 切片的双向转换,并且这种转换无需调用任何函数,只需应用显式类型转换就能够了
var s = "乘风破浪"
// string -> []rune
rs := []rune(s)
fmt.Printf("%x\n", rs) // [4e58 98ce 7834 6d6a]
// string -> []byte
bs := []byte(s)
fmt.Printf("%x\n", bs) // e4b998e9a38ee7a0b4e6b5aa
// []rune -> string
s1 := string(rs)
fmt.Println(s1) // 乘风破浪
// []byte -> string
s2 := string(bs)
fmt.Println(s2) // 乘风破浪