20-Unicode

Golang字符编码基础

Golang的标识符可以包含“任何Unicode编码可以表示的字母字符”。

虽然可以直接把一个整数值转换为一个string类型的值,但是被转换的整数值应该可以代表一个有效的Unicode代码点,否则转换的结果将会是,即,一个仅由高亮的问好组成的字符串值。

当一个string类型的值被转换为[]rune类型值的时候,其中的字符串会被拆分为一个个Unicode字符。

Golang的代码正式由Unicode字符组成的,所以的源代码都必须按照Unicode编码规范中的UTF-8编码格式进行编码。也就是说,Golang的源码文件必须使用UTF-8编码格式进行存储,如果源码文件中出现了非UTF-8编码的字符,那么在构建、安装和运行的时候,go命令会报错“illegal UTF-8 encoding”。

ASCII编码

ASCII由ANSI制定的单字节字符编码方案,可以用于基于文本的数据交换。ASCII编码方案使用单个字节(byte)的二进制数来编码一个字符。

  • 标准的ASCII编码用一个字节的最高比特(bit)位作为奇偶校验位,
  • 而扩展的ASCII编码则将刺猬用于表示字符。

ASCII编码支持的可打印字符和控制字符的集合叫做ASCII编码集

Unicode编码规范是另一种更通用、针对数码字符和文本的字符编码标准,它为世界上现存的所有自然语言中的每个字符,都设定了一个唯一的二进制编码。它定义了不同的自然语言的文本数据在国际间交换的统一方式,并为全球化软件创建了一个重要的基础。

Unicode编码规范以ASCII编码集为出发点,并突破了ASCII只能对拉丁字母进行编码的限制。它不但提供了可以对世界上超过百万的字符进行编码的能力,还支持所有已知的转义序列和控制代码。

在计算机内部,抽象的字符会被编码为整数,这些整数的范围被成为代码空间,在代码空间之内,每一个特定的整数都被成为一个代码点。一个受支持的抽象字符会被映射并分配给某个特定的代码点,反过来,一个代码点总是可以被看成一个被编码的字符。

Unicode编码

Unicode编码规范通常使用十六进制标表示法来表示Unicode代码点的整数值,并使用“U+”作为前缀。

如,英文字母字符“a”的Unicode代码点是“U+0061”。

在Unicode编码规范中,一个字符能且只能有与它对应的那个代码点表示。Unicode编码规范提供了三种不同的编码格式:

  • UTF-8
  • UTF-16
  • UTF-32

在这三种编码格式的名称中,“-”右边的整数的含义是,以多少个比特位作为一个编码单元

例如,UTF-8就是以8个比特位,也就是一个字节作为一个编码单元,并且,它与标砖的ASCII编码是完全兼容的,也就是说在[0x00,0x7F]的范围内,这两种编码表示的字符都是相同的,这是UTF-8的巨大优势

UTF是UCS Transformation Format是缩写,UCS是Universal Charater Set或者Unicode Character Set的缩写,所以UTF可以翻译为UNicode转换格式,它代表的是字符与字节序列之间的转换方式

UTF-8是一种可变宽的编码方案,它会用一个或者多个字节的二进制数字来表示某个字符,最多使用四个字节。比如:

  • 对于一个英文字符,它仅需要一个字节的二进制数就可以表示
  • 对于一个中文字符,它需要要三个字节的二进制数才能够表示

不论怎么,一个受支持的字符总是可以由UTF-8编码为一个字节序列,简称UTF-8编码值。

string类型的值在底层的表达

在底层,string类型的值是由一系列相对应的Unicode代码点的UTF-8编码值来表达的。在Golang中一个string类型的值:

  • 既可以被拆分为一个包含多个字符的序列:以rune为元素的切片可以表示
  • 也可以被拆分为一个包含多个字节的序列:以byte为元素的切片可以表示

rune是Golang特有的一个基本数据类型,它的一个值表示一个字符,即:一个Unicode字符。比如'G'、'o'、'爱'、'好'、'者'代表的就是一个Unicode字符。

因为UTF-8编码格式会把一个Unicode字符编码为一个长度为1~4个字节序列,所以一个rune类型的值可以由一个或多个字节来表示。

type rune = int32
// rune类型实际上是int32类型的一个别名类型
// 一个rune类型的值会由四个字节宽度的空间来存储
// 它的存储空间总是能够存下一个UTF-8编码值

一个rune类型的值,在底层就是一个UTF-8编码值。前者是便于人类理解的外部展现,后者是便于计算机系统理解的内在表达。

str := "Go爱好者 "
fmt.Printf("The string: %q\n", str)

// 字符串值“Go 爱好者”被转换为[]rune类型的值,
// 其中每一个字符(不论中英文)都会独立成为一个rune类型的元素值
fmt.Printf("  => runes(char): %q\n", []rune(str))   //   => runes(char): ['G' 'o' '爱' '好' '者']

// 每个rune类型的值在底层都是由一个UTF-8编码值来表达的,
// 所以可以使用下面这种方式展示字符序列
fmt.Printf("  => runes(hex): %x\n", []rune(str))    //   => runes(hex): [47 6f 7231 597d 8005]

// 将每个字符的UTF-8编码值都拆成响应的字节序列
// 每三个字节对应一个中文字符
fmt.Printf("  => bytes(hex): [% x]\n", []byte(str))     //   => bytes(hex): [47 6f e7 88 b1 e5 a5 bd e8 80 85]

对于一个多字节的UTF-8编码值来说,可以把它当成一个整体转换为单一的整数,也可以先把它拆成字节序列,在把每个字节分别转换为一个整数,从而得到多个整数。这两种表示法展现出来的内容往往会不一样,比如:

  • 对于中文字符‘爱’它的UTF-8编码值
  • 可以展现为单一的整数:7231
  • 也可以展现为三个整数:e7 88 b1

images

一个string类型的值会由若干个Unicode字符组成,每个Unicode字符都可以由一个rune类型的值来承载。这些字符在底层都会被转换为UTF-8编码值,而这些UTF-8编码值有会以字节序列的形式表达和存储。因此一个string类型的值在底层就是一个能够表达若干个UTF-8编码值的字节序列。

range子句遍历字符串

带有range子句的for语句会先把遍历的字符串值拆成一个字节序列,然后再试图找出这个字节序列中包含的一个UTF-8编码值,或者说每一个Unicode字符。

这样的for语句可以为两个迭代变量赋值:

  • 第一个变量的值:就是当前字节序列中的某个UTF-8编码值的第一个字节所对应的那个索引值
  • 第二个变量的值:这个UTF-8编码值代表的那个Unicode字符,其类型是rune
str := "Go 爱好者 "
for i, c := range str {
 fmt.Printf("%d: %q [% x]\n", i, c, []byte(string(c)))
}

// output
// 输出for-range语句的两个迭代变量和第二个值的字节序列形式
0: 'G' [47]
1: 'o' [6f]
2: '爱' [e7 88 b1]
5: '好' [e5 a5 bd]
8: '者' [e8 80 85]

// for-range语句可以逐一地迭代出字符串值里的每个Unicode字符,
// 但是相邻的Unicode字符的索引值并不一定是连续的,
// 这取决与前一个Unicode字符是否为单字节字符
上次修改: 25 November 2019