00.前言
如果有任何问题 请指出。
01.string的编码方式
string是utf8编码,所以不能直接通过下标获取中文等字符
例如
a := "aaa心"
fmt.Println(a[3])// 结果错误 å
因为通过下标获取的是一个byte(uint8) 而我们的中文字符占3个byte(int32) 所以我们可以通过强制转换为[]rune类型再通过下标获取,rune类型的值与Unicode码点一一对应,在Go语言中,rune就是int32的别名。
1)通过[]rune获取中文字符
a := "aaa心"
fmt.Printf("%s\n", string([]rune(s2)[3]))// 心 结果正确
2)Unicode与utf8的关系
unicode是字符集 uft8是编码方式
字符集: 为每一个字符分配一个唯一的 ID(学名为码位 / 码点 / Code Point);
编码规则: 将码位转换为字节序列的规则(编码/解码 可以理解为 加密/解密 的过程)
- unicode是静态的,而且是固定长度的,一一对应;unicode-8是一套规则,字长是可变的,即对应的字符结果是可变的。
- unicode字符集采用两个字节来存储字符。如果直接用unicode字符集那么像字母等这些只需要占一个字节的字符就会浪费空间,所以我们采用utf8变长编码解决这个问题,但是如果在内存中存储,使用utf-8却由于它的长度不固定,带来了很大的不便,使得在内存处理字符变得复杂。应对这个问题的解决策略是:在内存中存储字符时还是使用unicode编码,因为unicode编码的长度固定,处理起来很方便。而在文件的存储中,则使用utf-8编码,可以压缩内存,节省空间。这里一般有个自动转换的机制,即从文件中读取utf-8编码到内存时,会自动转换为unicode编码,而从内存中将字符保存到文件时,则自动转换为utf-8编码。
3)其它获取中文字符的方法
- 通过for range遍历
var s2 = "aaa心"
for k, v := range s2 { // 用range遍历 获取的是unicode字符(rune)
fmt.Println(k, v)
} // a a a 心
- 通过切片获取
var s2 = "aaa心"
fmt.Println(s2[3:]) // 心
//本质上就是获取 心这个字符 开始的这个字节到最后一个字节 组成的字符
02.string的不可变性
1)何为不可变
字符串不可变指的是 这个字符串是只读的不可写,不能通过下标改变这个字符串。
例子
a := "fsdfdsfsd"
a[1] = 'x' // 错误
我们不能通过下标改变这个字符串
疑问
a := "fsdfdsfsd"
fmt.Printf("%d\n", &a) // 824633786944
a = "xxxxxxx"
fmt.Printf("%d\n", &a) // 824633786944
我们发现给a重新赋新的字符串 但是他的地址没有变化 好像不满足我们的不可变性 这个问题我们后文的源码分析会解释。
2)如何实现通过下标改变字符串
通过上文我们知道不能通过 a[1]=‘x’ 这种方式改变字符串,不过我们知道string是utf8编码 本质上[]byte数组,虽然string不能通过下标赋值,但是[]byte数组可以,所以我们可以通过把string类型强转为[]byte类型 改变后再 转回string
例子
a := "fsdfdsfsd"
bytes := []byte(a)
bytes[0] = 'h'
a = string(bytes)
fmt.Printf("%s\n", a) // hsdfdsfsd
我们可以看到确实改变了字符串结果。
但是根据我们之前的了解 转换为[]byte只能获取到一个byte 理论上bytes[0]=‘中’ 是错的
验证
a := "fsdfdsfsd"
bytes := []byte(a)
bytes[0] = '中' // 程序爆红 不能编译
a = string(bytes)
fmt.Printf("%s\n", a) // hsdfdsfsd
那我们如何把字符串的第一个下标改成中文字符呢,其实与[]byte数组同理 我们改成[]rune数组就可以了
例子
a := "fsdfdsfsd"
runes := []rune(s)
runes[0] = '中'
a = string(bytes)
fmt.Printf("%s\n", a) // 中sdfdsfsd
可以看出成功改成了中文字符
03.string源码分析
1)stringStruct结构体
string的数据结构就是stringStruct这个结构体
type stringStruct struct {
str unsafe.Pointer
len int
}
我们可以看到它由两个字段构成
- str unsafe.Pointer: 指向底层数组的指针
- len:长度
2)gostringnocopy函数
这个函数的作用是在字符串构建过程中把stringStruct转换成string
func gostringnocopy(str *byte) string { // 跟据字符串地址构建string
ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)} // 先构造stringStruct
s := *(*string)(unsafe.Pointer(&ss)) // 再将stringStruct转换成string
return s
}
3)解释一下不可变那里提出的疑问
我们可以看到 gostringnocopy函数 在构建的时候 先把byte指针赋值给了 str,之后我们又把构建好的ss转换成string指针 然后再解引用 返回string类型。到这里我们明白了什么?
- 1:返回的string类型 它的地址不是字符串所在的地址,stringStruct中str的地址才是字符串所在地址
- 2:到这里我们就可以理解了为什么 a:=“sss” a=“xxx”,这种改变a的地址没有变化,因为这里a的地址并不是字符串地址 只是stringStruct转换为string后的地址 而它自始至终没有变化,所以地址不变。
4)不可变性如何实现
首先我们需要了解几个概念
- 左值:左值指的是可以被赋值的量
- 右值:只能读的量
- 字面量:字面量可以理解为不可变的量 以固定形式存放的量 或者说字面量表示的含义就是它本身 在式子中作为右值,例如 "aaa"就是string字面量,0就是int字面量
例子
a:="aaaa" //a是左值
"aaa" = "BBB" // "aaa"是右值 也是字面量
前文我们知道了string里记录的是字符串的首地址,我们进行改变也就是对这个地址指向的值进行操作,这个可能实现吗?答案是不可能的。
这里我们把问题简化为了对于指针指向的值,我们可能改变这个值本身吗?
例子
int main() {
char *str = "aaaaa";
*str = 'b'; // 使用指针修改字符串字面量的值
printf("%s\n", str);
return 0;
}
以上代码运行时会报错
分析
我们首先我们让str指针指向这个字符串的首地址,之后我们再把这个首地址指向的值变成’b’,这里我们分析一下str取到的是什么,它取到的实际是’a’这个字面量,也就是str = ‘b’ 等价与 ‘a’='b’显然不对。不过我猜会有人说 *str 不是等价于 str[0]吗 为什么str[0]可以赋值
例子
int main() {
char str[] = "aaaaa";
str[0] = 'b';
printf("%s\n", str); // baaaa
printf("%s\n", str[1]); // a,自动解引用
return 0;
}
代码正确。
究其原因是数组可以作为左值,str[0] 本质上指针 之所以数组可以通过str[1]下标这种方式获取到值 是因为数组是一个语法糖,他把 指针指向这个字符串 与 解引用这两个步骤 合并了,所以数组在作为右值时 会自动解引用 这也就是 为什么我们说 str[1] 等价于 *(str+1),但是在作为左值的时候,他俩是不等价的 此时数组 是指针并不会解引用,所以str[1]='x’这种方式 其实是指针指向的位置变了 从原来指向’a’所在位置变成了指向’x’所在位置。