1 go语言的字符集和编码方案
Go 语言源码默认使用unicode字符集,并采用UTF8编码方案。 Go还提供了rune原生类型表示unicode字符。
在大多数场景,并不需要深入了解字符集和字符编码方案。
但是在涉及不同字符集的转换或同一字符集不同编码方案之间的转换时,
了解字符原理和字符编码方案就显得非常必要了。 这里介绍 Go的Unicode字符表示以及如何使用Go进行字符编码方案转换。
string类型在golang中以utf-8的编码形式存在,而string的底层存储结构,划分到字节即byte,划分到字符即rune。本文将会介绍字符编码的一些基础概念,详细讲述三者之间的关系,并提供部分字符串相关的操作实践。
byte是uint8类型的别名,通常用于表示一个字节(8bit)。
rune是int32类型的别名,通常用于表示一个字符(32bit)。
string是8bit字节的集合,通常是表示UTF-8编码的字符串。
从官方概念来看,string表示的是byte的集合,即八位的一个字节的集合,通常情况下使用UTF-8的编码方式,但不绝对。
2 rune字符
而rune表示用四个字节组成的一个字符,rune值为字符的Unicode编码。
string类型在golang中以utf-8的编码形式存在,而string的底层存储结构,划分到字节即byte,划分到字符即rune。
unicode 为 ASCII 的扩展集,是字符与二进制的一一对应
unicode/utf8
encode rune 支持将 rune 函数接受一个rune值,通过UTF-8的编码规则。
将其转化为[]byte并写入p,同时返回写入的字节数。
对于字符串来说,如果使用Unicode进行存储,则每个字符使用的存储长度是不固定的,而且是无法进行精确分割的。
如中文字符“南”使用的Unicode编码为0x5357,对于该编码可以整体理解为一个字符“南”,也可以理解为0x53(S)和0x57(W)。
因而单纯使用Unicode是无法进行字符串编码的,因为计算机无法去识别要在几个字节处做分割,哪几个字节要组成一个字符。
所以需要一种Unicode之上,存在部分冗余位的编码方式,以准确表示单个字符,并在多个字符进行组合的时候,能够正确进行分割,即UTF-8。
UTF-8是针对Unicode的一种可变长度字符编码,它可以用来表示Unicode标准中的任何字符。
因而UTF-8是Unicode字符编码的一种实现方式,Unicode强调单个字符的一一对应关系,UTF-8是Unicode的组合实现方式,此外还有UTF-16,UTF-32等类似编码,普适性较UTF-8稍弱。
不同国家,地区,甚至行业使用的字符和字符总数将有所不同。
当时美国人只用到128个字符,预留128个备用。 要表示256个字符,使用8个比特就可以了。
这就是ASCII码 美国标准信息交换代码,American Standard Code for Information Interchange
这8比特恰好等于计算机的基本存储数据单元 --- 字节的比特数相同,这样一个字节就恰好表示ASCII字符集的一个字符。
计算机字符集的每隔字符都有两个属性: 码点 code point 和表示这个码点的内存编码 位模式,表示这个字符码点的二进制比特串。
所谓码点 --- 这里使用了Unicode字符集的码点概念 --- 是指将字符集全部字符排成一队,字符在队伍中的唯一序号值。
以ASCII字符集的字符为例:
码点 字符 含义 内存编码表示,位模式,二进制
0 不可见 空字符 0000 0000
...
65 A 字符A 01000001
...
127 不可见 DEL 1111 1111
我们看到ASCII字符集的每个字符码点和内存编码表示一致。例如ASCII字符A在ASCII码表的码点序号 为65,内存编码值也为 0100 0001
对于象形文字,使用ASCII字符集就不够了,因此亚洲国家字符串为 GB18030 BIG5 SHIFT_JIS 等。
字符集多了,容易造成混乱,乱码问题就出现了,比如 地球 二字,在网站存储和传输时使用 GB18030字符集编码,位模式 0xB5D8,0xC7F2
而在使用BIG5的浏览器中加载 0xB5D8,0xC7F2 分别表示 崋,⑩
乱码主要原因是字符集不兼容,一个内存编码表示在不同字符集中表示不同字符。
比如从 0 到 127 的代码是 ASCII,它们在两个代码页中是相同的。
128 及以上的一些代码是不同的。
如果不知道正在使用的代码页,就无法理解 128 及以上的代码。 亚洲语言有超过 1000+ 个字母,无法放入单个字节。因此,DBCS 使用了双字节字符集。在这种情况下,一些字母使用 1 个字节,而另一些字母使用 2 个字节。为了简化所有这些复杂性,我们需要一个包含所有书写系统的字符集。Unicode联盟就这样诞生了。
在 Unicode 中,字母映射到称为 Code Point 的东西。例如,A 映射到 U+0041(数字为十六进制),即十进制 65。事实上,前 128 个 ASCII 字符在 Unicode 中保留了相同的码位。一些最大的优点是
3 编码转换方案
日常编码中,我们经常涉及在不同字符集的字符编码方案间进行转换,以满足字符在不同字符编码环境的解析,处理,展示,存储的需求。
这里以UTF8编码为例,使用Go实现这两个字符编码环境下字符编码的转换。
GB18030 全称是信息技术中文编码字符集。
是国家标准所规定的变长多字节字符集,是计算计系统必须遵守的基础标准之一。
该字符集采用变长多字节编码,每个字符可以由1,2,4字节编码表示,因此编码空间庞大,可以定义160多万个字符。
Go语言默认源码文件字符采用UTF8编码方案的Unicode字符。
在Go中,每一个rune对应一个Unicode字符码点,而Unicode字符在内存编码表示则放在 []byte 类型中。
从rune类型转换为 []byte 类型称为 编码。 encode,而反过来称之为 decode 解码。
unicode码点 码点在内存编码表示 位模式
rune ------编码-----> []byte
<----解码-----
一般概念中,字符串包含字节,码点表示单个Unicode值表示的个体。 具有16进制值。
在go语言中,码点就是rune,其类型为int32的别名。
Go 源代码始终为utf8
字符串可以包含任意字节
字符集文字不包括字节级转义符时,字符串始终包含有效utf8序列
代码Unicode码点的字节序被称为rune
在go语言不会保证字符串的字符被规范化。
4 在go中转换编码的例子
每个rune对应一个unicode 码点。 我们通过标准库提供的unicode/utf8 对rune进行解码操作,如下:
func encodeRune() {
var r rune = 0x4E2D //在unicode编码的码点0x4E2D 表示 中
buf := make([]byte, 3)
n := utf8.EncodeRune(buf, r)
fmt.Printf("the byte slice after encoding rune 0x4E2D is:\n")
fmt.Printf("[")
for i := 0; i < n; i++{
fmt.Printf("0x%X", buf[i])
}
fmt.Printf("]\n")
fmt.Printf("the unicode charactor is %s\n", string(buf))
}
//[]byte -> rune
func decodeRune() {
var buf = []byte{0xE4, 0xB8, 0xAD}
r, _ := utf8.DecodeRune(buf)
fmt.Printf("the rune after decoding [0xE4,0xB8, 0xAD] is 0x%X \n", r )
}
fund main() {
encodeRune()
decodeRune()
}
运行示例:
go run .\main.go
the byte slice after encoding rune 0x4E2D is:
[0xE40xB80xAD]
the unicode charactor is 中
the rune after decoding [0xE4,0xB8, 0xAD] is 0x4E2D
我们再通过打印字符字面量底层的内存空间内容验证示例输出结果的正确性:
func BaseCache() {
var s = "中"
fmt.Printf("Unicode 字符:%s => 其 UTF-8 内存编码表示为:", s)
for _, v := range []byte(s) {
fmt.Printf("0x%X ", v)
}
fmt.Printf("\n")
}
输出结果:
nicode 字符:中 => 其 UTF-8 内存编码表示为:0xE4 0xB8 0xAD
可以发现,unicode字符的中 底层内存空间内容与其UTF8编码后的切片内容是一样的。
5 更多一点的
将UTF8编码环境的中文转换为 GB18030编码环境的编码表示 位模式,
并验证转换后的结果在GB18030能否被正确解析和呈现。
将 中 字符从UTF8表示,转换为GB18030编码
unicode码点 utf8 内存编码表示
U+4E2D 中 =====> E4 B8 AD
| |
| |
GB18030码点表 GB18030内存表示
D6D0 中 =====> D6 D0 中
Go标准库没有直接提供简体中文编码与UTF8编码之间的转换实现,
但是Go标准库依赖的 golang.org/x/text模块提供了相关转换实现。
golang.org/x/text 同样是Go核心团队维护的工具包。
我们可以将该模块的包作为标准库,只是Go1兼容性不保证这些包对外提供的API稳定性。
代码如下:
package main
...
真正执行UTF8到GB18030编码形式转换的是 simplifiedchinese.GB18030.NewEncode方法,它读取以UTF8编码表示形式存在的字节流[]byte
并将其转换为以GB18030编码表示的字节流返回。
如果Macos输出为乱码,需要在系统环境变量全部设置为 GB18030
locale
LANG= "zh_CH.GB18030"
LC_COLLATE="zh_CH.GB18030"
...
这样可以支持中文的内存编码输出到系统控制台。
使用Go标准库及其依赖库 golang.org/x/text 的包,
我们不仅可以实现Go默认字符编码UTF-8与其他字符集编码的相互转换,
还可以实现任意字符集编码之间的相互转换。
也可以将GB18030编码数据转换为 UTF-16和UTF-32。
其转换逻辑如下
中
数据源 0xD6 0xD0 0x4E 0x2D 转换结果 中
gb18030
| |
| |
reader chain bytes Reader ---> transform.Reader ----> transform.Reader
| rune -> []byte
[]byte -> rune
Go 以 utf8编码表示码点
我们使用golang.org/x/text 的 transform.Reader 链实现任意字符编码间的转换。
从上图可以看出,我们使用了一个惯用的Reader链结构完成了数据从gb18030编码到UTF-16和UTF-32的转换。
第一个 transform.Reader 在GB18030编码的源数据 []byte 转换为 rune,即unicode码点,
并以Go默认的UTF-8编码格式保存在内存中。
第二个 transform.Reader 在UTF16.Encoder帮助下,将rune再编码转换为最终数据。
编码规则
• ASCII字符(不包含扩展128+)0000 0000-0000 007F (0~7bit) • 0xxxxxxx • 0000 0080-0000 07FF (8~11bit) • 110xxxxx 10xxxxxx • 0000 0800-0000 FFFF (12~16bit) • 1110xxxx 10xxxxxx 10xxxxxx • 0001 0000-0010 FFFF (17~21bit) • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
5 小结
rune
代表Unicode字符,等价于int32
。
字符串底层是字节byte
(uint8
别名)集合,以UTF-8存储。
Unicode码点是字符的唯一标识,
UTF-8是一种变长编码,确保不同字符集兼容。
ASCII是最早的7位字符集,最多表示128个内容。
Unicode扩展了ASCII。