字符集和编码方案在go的形式

举报
码乐 发表于 2024/01/05 08:45:22 2024/01/05
【摘要】 1 字符集和编码方案Go 语言源码默认使用unicode字符集,并采用UTF8编码方案。 Go还提供了rune原生类型表示unicode字符。在大多数语言的场景,并不需要深入了解字符集和字符编码方案。但是,在涉及不同字符集的转换或同一字符集不同编码方案之间的转换时,了解字符原理和字符编码方案就显得非常必要了。 这里介绍 Go的Unicode字符表示以及如何使用Go进行字符编码方案转换。st...

1 字符集和编码方案

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的编码方式,但不绝对。

而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 分别表示 崋,⑩

乱码主要原因是字符集不兼容,一个内存编码表示在不同字符集中表示不同字符。

1.1 编码方案的转换

日常编码中,我们经常涉及在不同字符集的字符编码方案间进行转换,以满足字符在不同字符编码环境的解析,处理,展示,存储的需求。

这里以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语言不会保证字符串的字符被规范化。

每个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编码后的切片内容是一样的。

将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
    

1.2 字符编码的统一:unicode 和 utf8

统一码又被称之为万国码 unicode,在1994年发布。此时人类终于有了收纳全部人造字符的统一字符集。

Unicode是Universal Multiple-Octet Coded Character Set 的缩写。 中文意思是 通用多字节编码字符集。

它是由一个名为Unicode学会,Unicode Consortium的机构制定的字符集系统。

Unicode字符集致力于为全世界线程每个语言的字符分配统一且唯一的字符编号,以满足跨语言,跨平台进行文本数据交换,处理,存储和显示。

	码点/序号          字符

	U+0000          .....
	...
	U+4E2D           中
	...
	U+10FFFF         ...

序号是全世界所有语言文字的符号分配的唯一编号。序号的范围从 0x000000 到0x10FFFF,全部可以容纳110多万个字符。

这个序号也被称之为Unicode码点。第二列字符就称为Unicode字符。

考虑到目前使用最多也就是最基础的ASCII字符集码点兼容性。前128个码点对应ASCII字符集。

现在Unicode字符集的码点表有了,只需要知道每个码点在计算机的内存编码表示,位模式。

我们知道ASCII字符集的内存编码表示位模式使用的是与其字符码点相同的数值,那么Unicode采用的是上面内存编码表示呢?

答案是并不唯一,常用的有三类:

	* UTF-16
	该方案使用2或4表示每个Unicode字符码点。它的优点是编码解码简单,因为全部字符都用偶数字节表示;
            
	其不足也很明显,比如存在字节序问题,不兼容ASCII字符内存表示以及空间效率不佳。

	* UTF-32
	该方案固定使用4字节表示每个Unicode字符码点。它的优点也是编解码简单,因为全部字符都用4字节表示。
            
            不足也和UTF-16一样明显,同样存在字节序的问题。 不兼容ASCII字符内存表示,且空间效率是这三个方案最差的。

	*UTF-8
	以其他的不同,UTF8使用变长字节对Unicode字符码点进行编码。

编码采用字节数量与Unicode字符在码点表的序号有关。

表示序号(码点)小的字符使用的字节少,表示序号(码点)大的字符使用的字节多。

UTF8编码使用的字节从1到4不等。前128个与ASCII字符重合码点(U+0000 ~U+007F)使用1字节表示;

带变音符号的拉丁文,希腊文,西里尔阿拉伯文使用2字节表示,函数使用3字节表示,其他极少使用的语言使用4字节表示。

这样的编码方案是兼容ASCII字符内存表示的,采用UTF-8方案在内存表示Unicode字符时,已有的ASCII字符可以直接被作为Unicode字符进行存储和传输,无需改变。

此外,UTF8的编码单元为1字节,也就是一次编解码1字节,所以在处理UTF8方案表示的Unicode字符就不需要像UTF16或UTF32那样考虑字节序问题。

这三个方案中,UTF8方案空间效率最高。

我们分别使用这三个编码方式对 Unicode的字符 A 编码,LE表示小端字节库 Little Endian,BE 表示大端字节序 Big Endian

	Unicode  A
	Unicode 码点序号  0x000041
	UTF-8编码: 0x41
	UTF-16BE编码: 0xFEFF0041
	UTF-16LE编码: 0xFFFE4100
	UTF-32BE编码: 0x0000FEFF00000041
	UTF-32LE编码: 0xFFFE000041000000

由于UTF-16和UTF-32编码方案存在字节序问题,因此上面针对每个方案各自给出两个结果。

以UTF-16小端字节序结果0xFFFE4100为例,这个编码结果由4字节组成,前两个字节为0xFEFE,这个特定位模式时字节序标记(Byte Order Mark,BOM)。

Unicode规范对字节序标记约定如下:

	FF FE             UTF-16  小端字节序
	FE FF             UTF-16  大端字节序
	FF FE 00 00       UTF-32 小端字节序
	00 00 FE FF       UTF-32 大端字节序
	EF BB BF          UTF-8

如果没有提供字节序标记,则默认采用大端字节序解码。

另外我们注意到,Unicode 规范为UTF-8也准备了一个字节序标记 EF BB BF

但是由于UTF-8 没有字节序问题,因此这个BOM只是用于表明该数据流采用的为UTF-8编码方案,算是一个编码方案类型标记。

UTF-8编码方案由于优点众多,经过多年发展,已成为Unicode 字符在计算机中内存编码表示 位模式方案的事实标准。 Go运用顺应了这个趋势。源码文件的字符编码也是使用UTF8编码。

2 小结

Unicode是一种在计算机上使用的字符编码。

它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。

本质上Unicode表示了一种字符与二进制编码的一一对应关系,所以是一种单字符的编码。

  1. 对于ASCII(不包含扩展128+)字符,UTF-8编码、Unicode编码、ASCII码均相同(即单字节以0开头)

  2. 对于非ASCII(不包含扩展128+)字符,若字符有n个字节(编码后)。

则首字节的开头为n个1和1个0,其余字节均以10开头。除去这些开头固定位,其余位组合表示Unicode字符。

转换(2+字节UTF-8)

UTF-8 to Unicode

    将UTF-8 按字节进行分割,以编码规则去掉每个字节头部的占位01,剩下位进行组合即Unicode字符

Unicode to UTF-8

    从低位开始每次取6位,前加10组成尾部一个字节。
    直到不足六位,加上对应的n个1和1个0,首字节的大端不足位补0,
    如补充字节后位数不够则再增加一字节,规则同上。

(按规则预估字节数,优先写好每个字节的填充位,从末端补充即可)

Go默认字符集Unicode以及采用编码方案UTF8,深入理解字符,字符集的属性,码点和内存编码表示以及它们之间的关系,

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。