Go语言中的字符串

golang string

Posted by alovn on January 22, 2021

字符集

一个bit可以是0也可以是1,8个bit组成一个byte。全为0时代表数字0,全为1时代表数字255:

1
2
00000000 //0
11111111 //255

一个byte可以表示256个数字。两个byte可以表示65536个数字, 整数可以这样存,那字符呢?

对收录的字符进行一一编号, 然后得到一个字符编号对照表,这就是字符集。一个字符按映射关系对应到一个数字。比如字母A:

字符编号二进制
A650100 0001

ASCII字符集只收录了128个字符,其扩展字符集也只有256个。这…没有汉字怎么能行呢?于是出现了GB2312。没有繁体字也不行呀,因此又出现了BIG5?但是依然有许多字符没有被收录, 与其不断推出收录更多字符的字符集,不如本着全球统一标准的目的,制作一个通用字符集,于是诞生了通用的Unicode字符集,它于1990年开始研发并于1994年发布,实现了跨语言跨平台的文本转换与处理。

字符集类型出生日期包含
ASCII1967英文字母、阿拉伯数字、常用字符、控制字符
GB23121980简体中文、拉丁字母、日文假名…
BIG51984繁体字…
GB13000.11993中日韩…
GBK1995汉字扩展规范,不支持韩语…
GB180302000兼容GBK,支持更多

编码

字符集促成了字符与二进制的合作,但是有了字符集就万事大吉了吗?

定长编码

二进制如何划分字符边界,不管变化多大多小,统一按最长的来,位数不够高位补零,这就是定长编码。字符边界问题是解决了,但是这着实有些浪费内存呀。而且字符集收录的越多,编号跨度越大,定长编码造成的浪费越明显。还得再想办法。

变长编码

既然定长编码不行,那就用变长编码,小编号少占字节,大编号多占字节。但是怎么划分字符边界呢?来看一种解决方案:

1
2
3
如果编号属于区间[0,127],就占用一字节,且最高位固定标识为0。
如果属于区间[128,2047],就占用两字节,且有固定标识位110和10。
三个以及更多字节的编码也都遵循这样的规则。
编号编号(十进制)模板
[U+0000, U+007F][0,127]0???????
[U+0080, U+07FF][128,2047]110????? 10??????
[U+0800, U+FFFF][2048,65535]1110???? 10?????? 10??????
[U+10000, U+10FFFF][65536,1114111]11110??? 10?????? 10?????? 10??????

来试试看吧

解码示例1
1
01100101

这个字节最高位是0,就表示这个字符只占一个字节,除去标识位,剩下的7位就是该字符的二进制编号:

1
1100101

转换成十进制为101, 对应字符 e。

解码示例2

再看这个编码 11100100, 它以1110开头, 就表示这个字符占用3个字节, 它要和后面两个以10开头的字节共同组成一个字符:

1
11100100 10111000 10010110

除去这些标识位把剩下的这三部分组合起来,就得到该字符的二进制编号:

1
01001110 00010110

转化成十进制是19990, 对应汉字『世』。

以上的示例是解码过程,我们再来编码试试。

编码示例

我们用世界的『界』字为例,在unicode字符集中编号为30028, 符合[2048,65535]这个区间,所以要占用3个字节, 使用 |1110???? 10?????? 10?????? 这个模板。把30028转成二进制是:

1
01110101 01001100

再对应填到模板中:

1
11100111 10010101 10001100

OK!编码完成!以上用的其实就是utf-8编码,也是go语言默认的编码方式。现在字符串你知道该怎么存了吧,要字符集配合编码才行。

字符集与编码

现在你明白字符集与编码直接的关系了吗?

Unicode字符集提供了字符到数字编号的映射。

UTF-8则是一种存储编码的算法方式,除了UTF-8,其它的存储方式还有UTF-16、UTF-32等。

比如英文字符A,在Unicode中的对应的值为65,使用UTF-8、UTF-16、UTF-32不同格式存储时是完全不同的。

1
2
3
UTF-8以字节为单位对Unicode进行编码。
UTF-16编码以16位无符号整数为单位。
UTF-32编码以32位无符号整数为单位。

Golang中的字符串

接下来我们看看字符串类型的变量在Golang中是什么结构, 首先得需要一个起始地址吧,这样才能找到字符串内容,但是找得到开头猜不到结尾,内存那么大, 天知道它该在哪里结束呀。

C语言说:『你在字符串内存结尾处,放一个特定字符标识不就好了。』

C语言用的是编号为0的字符, 这也就限定了内存中不能再出现这个标识符,否则将发生不可预估的后果。所以Go语言并没有采用这个方法。而是在起始地址的后面多存了一个长度,这个长度并不是字符个数,而是字节个数。比如以下这个字符串:

1
var str string = "hello世界"

编码后, 有11个字节:

1
01101000 01100101 01101100 01101100 01101111 11100100 10111000 10010110 11100111 10010101 10001100

str变量会记录一个字符串的起始地址 data,还有一个字节数 len = 11。现在既找得到开头,又找得到结尾,还不限制字符串内容。

字符串结构在golang源码中的定义是这样的:

1
2
3
4
type stringStruct struct {
    str unsafe.Pointer
    len int
}

rune类型

上面说到定义字符串 var str string = “hello世界”,字节数是11,用len(str) 得到的就是字节数而不是字符数量,那怎么获取到字符数量呢?这需要用到utf8包下的RuneCountInString函数:

1
utf8.RuneCountInString(str) //7

也可以将字符串转换为[]rune后再调用len

1
len([]rune(str)) //7

rune 也是Go中的内置类型,它是int32的别名,在各方面都等同于int32。按惯例,它用于区分字符值和整数值。字符串可以直接转换为[]rune,也就是字符串中的字符编码。

1
2
3
string 类型的底层是一个 byte 数组。
byte是一个字节代表的数据(一个字符可能有多个字节,如unicode编码下的中文字符)。
rune表示一个unicode字符。

源码

OK! 关于golang实现字符串源码实现可以查看这里 https://github.com/golang/go/blob/master/src/runtime/string.go#L228