字符串

1.如何存储字符

计算机只能识别二进制码,对于十进制数字可以转成二进制数字存储。保存字符串则需要使用到 字符集,字符集是所有字符编号组成的总和。

1.1 字符集

字符集全称中文位数字符数缺陷
ASCIIAmerican Standard Code for Information Interchange美国信息交换标准代码7 bit128 个字符没有涵盖西文字母之外的字符
ASCIIAmerican Standard Code for Information Interchange美国信息交换标准代码8 bit256 个字符没有涵盖西文字母之外的字符
GBK
UnicodeUniversal Multiple-ctet Coded Character Set统一字符编码标准60000

字符统一成 Unicode 编码,乱码问题从此消失了。 但是 Unicode 只是一个字符集。字符集只是给所有的字符一个唯一编号,但是却没有规定如何存储`。 Unicode 最常用的是用两个字节表示一个字符,如果你写的文本基本上全部是英文的话,用 Unicode 编码比 ASCII 编码需要多一倍的存储空间,在存储和传输上就十分不划算。所以又引入了 字符编码。

1.2 字符编码

Unicode 和 UTF-8 之间有啥关系?

UTF(Unicode Transformation Format)可译成 Unicode 格式转换,即怎样把 Unicode 对应的数字转换成程序数据。

Unicode 和 Ascii 是字符集。UTF-8、UTF-16、UTF-32 等是编码规则。 可以通过编码规则将字符集的字符表示成程序中的数据。

例如:“汉字” 对应不同编码的程序数据是:
char      data_utf8[]  =  {0xE6,0xB1,0x89,0xE5,0xAD,0x97};    //UTF-8编码
char16_t data_utf16[]  =  {0x6C49,0x5B57};                    //UTF-16编码
char32_t data_utf32[]  =  {0x00006C49,0x00005B57};            //UTF-32编码

UTF-8

UTF-8 是一套以 8 位为一个编码单位的 可变长编码 。比如: 一个编号为 65 的字符,只需要一个字节就可以存下,但是编号 40657 的字符需要两个字节的空间才可以装下,而更靠后的字符可能会需要三个甚至四个字节的空间。

Unicode编码           UTF-8字节流
U+ 0000 ~ U+ 007F:    0XXXXXXX
U+ 0080 ~ U+ 07FF:    110XXXXX 10XXXXXX
U+ 0800 ~ U+ FFFF:    1110XXXX 10XXXXXX 10XXXXXX
U+10000 ~ U+1FFFF:    11110XXX 10XXXXXX 10XXXXXX 10XXXXXX

将 Unicode 按照 UTF-8 编码为字节序列

汉字:                      知
Unicode码:                 30693
Unicode码的十六进制表示为:   0x77E5

根据utf-8的上表中的编码规则,「知」字的码位U+77E5属于第三行的范围:所以需要3个字节(byte)

       7    7    E    5
    0111 0111 1110 0101    77E5对应的二进制的
--------------------------
1110XXXX 10XXXXXX 10XXXXXX 模版(由77E5范围,取上表第三行)
    0111   011111   100101 根据模板格式调整77E5二进制的结构
11100111 10011111 10100101 代入模版
   E   7    9   F    A   5 转化回16进制

这就是将 U+77E5 按照 UTF-8 编码为字节序列 E79FA5 的过程。反之亦然。

2.源码

runtime/string.go
type stringStruct struct {
	str unsafe.Pointer // 指向一个 [len]byte 的数组
	len int // 长度
}

3.常用函数

4.面试题

4.1 string 为什么是不可变的?

  • 性能考量
    • 内存分配优化:字符串是不可变的,意味着一旦创建,其内容就不会改变。因此,字符串可以被多个变量共享,无需每次修改时都复制整个字符串内容,节省了内存和 CPU 资源。
    • 并发安全:由于字符串内容不可变,所以在多线程(goroutine)环境下不需要任何同步锁就能安全访问,降低了并发编程时的复杂度和潜在风险。
  • 简化内存管理
    • 不可变字符串允许编译器和运行时系统做出更优的内存布局决策。例如,编译器可以静态地知道字符串的生命周期,有助于提升垃圾回收(GC)的效率。
    • 由于字符串是只读的,Go 的运行时系统不需要担心字符串内容在使用过程中被意外篡改。

4.2 string 的内存分配?

在 Go 语言中,字符串(string)的内存分配是在堆上进行的,而不是栈上。Go 语言中的字符串实际上是包含了指向底层字节数组(字节切片[]byte)的一个 不可变引用。这个字节数组存储了字符串的实际内容,它位于 内存中。

字符串变量本身(即引用)在函数 上分配,它保存的是指向堆上字节数组的指针以及字符串的长度。当创建或修改字符串时,Go 会分配新的内存来存放字符串内容,并更新字符串变量指向新分配的内存地址。

例如,当声明一个字符串变量时:

s := "Hello, world!"

s 变量就是在栈上分配的,它存储的是一个指向堆上包含 “Hello, world!” 字符串内容的字节数组的指针以及字符串的长度。而字符串的实际内容则存储在堆内存中。由于 Go 语言的字符串是不可变的,所以即使有多个变量引用同一个字符串内容,也会共享同一份堆内存空间,从而节省内存。

最后更新于