字符集
一个bit可以是0也可以是1,8个bit组成一个byte。全为0时代表数字0,全为1时代表数字255:
1
2
00000000 //0
11111111 //255
一个byte可以表示256个数字。两个byte可以表示65536个数字, 整数可以这样存,那字符呢?
对收录的字符进行一一编号, 然后得到一个字符编号对照表,这就是字符集。一个字符按映射关系对应到一个数字。比如字母A:
字符 | 编号 | 二进制 |
---|---|---|
A | 65 | 0100 0001 |
ASCII字符集只收录了128个字符,其扩展字符集也只有256个。这…没有汉字怎么能行呢?于是出现了GB2312。没有繁体字也不行呀,因此又出现了BIG5?但是依然有许多字符没有被收录, 与其不断推出收录更多字符的字符集,不如本着全球统一标准的目的,制作一个通用字符集,于是诞生了通用的Unicode字符集,它于1990年开始研发并于1994年发布,实现了跨语言跨平台的文本转换与处理。
字符集类型 | 出生日期 | 包含 |
---|---|---|
ASCII | 1967 | 英文字母、阿拉伯数字、常用字符、控制字符 |
GB2312 | 1980 | 简体中文、拉丁字母、日文假名… |
BIG5 | 1984 | 繁体字… |
GB13000.1 | 1993 | 中日韩… |
GBK | 1995 | 汉字扩展规范,不支持韩语… |
GB18030 | 2000 | 兼容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