讲清楚Go字符串和utf8编码

unicode 字符集

unicode 字符集给世界上每个字符做了编码,每个字符对应一个码点(code pooint)。unicode字符集将所有的字符按平面划分,分为1个基础平面+16个补充平面,参考:unicode字符集。

码点范围:

  • 基础平面:U+0000 - U+FFFF
  • 补充平面:U+10000 - U+10FFFF

注意:这里码点用16进制写法,每个16进制位等于4个二进制位,2个16进制位就等于1字节。

utf8

unicode码点不好被计算机直接存储,假如每个字符都按最长码点(U+10FFFF),用3字节存储,对基础平面的字符是极大的浪费,而我们日常用的最多的就是基础平面的字符。

出于这个原因,utf8被发明出来。utf8是一种编码方式,它规定每个unicode码点如何被编码。简单来说,它的规则很简单:

  1. 根据码点范围,决定需要的字节数
  2. 前缀+码点二进制

下面的表格具体展示了码点范围和对应的编码规则

分类 范围 Byte1 Byte2 Byte3 Byte4
基本平面 U+0000, U+007F 0xxxxxxx
U+0080, U+07FF 110xxxxx 10xxxxxx
U+0800, U+FFFF 1110xxxx 10xxxxxx 10xxxxxx
补充平面 U+10000, U+10FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

1个基本平面的字符被细分成了三个范围,16个补充片面算一个范围,具体来说:

  • U+0000,U+007F: 这之间的码点,用1个字节编码,前缀是0,剩下的7位就是码点的二进制位
  • U+0080,U+07FF:这之间的码点,用2个字节表示,第一个字节前缀是110,第二个字节前缀是10,一二字节剩余的位置就是码点二进制位原样写进来
  • U+0800, U+FFFF,U+10000,U+10FFFF: 以此类推

举个例子:

中文"世"的码点是:4E16

码点写成二进制:0100,1110,0001,0110

码点落在第3区间,utf8编码为:11100100 ,10111000 ,10010110

utf8编码用16进制表示:E4B896


中文"界"的码点是:754C

码点写成二进制:0111,0101,0100,1100

码点落在第3区间,utf8编码为:11100111 ,10010101 ,10001100

utf8编码用16进制表示:E7958C

Go 字符串

Go 中的字符串就是byte slice,Go 对于string literal采用utf8编码,每个byte存的就是utf8编码的结果。

go 复制代码
word := "世界"
fmt.Printf("% x\n", word)  // output: e4 b8 96 e7 95 8c
fmt.Printf("% x\n", []byte(word)) // same output: e4 b8 96 e7 95 8c

要强调的是,string本身就是一个byte slice,没有任何特殊。只是 Go 在处理 string literal 时,默认将 utf8 编码存到这个byte slice中。除此以外,我们可以随意往里面插入字节,甚至是无法用utf8解析的字节。

go 复制代码
utf8Bytes := []byte{0xe4, 0xb8, 0x96, 0xe7, 0x95, 0x8c}
fmt.Printf("%s\n", string(utf8Bytes)) // output: 世界

utf8Bytes = append(utf8Bytes, 0xff) // 插入一个不合法的utf8字节
fmt.Printf("%s\n", string(utf8Bytes)) // output: 世界�

在打印时,对于无法用 utf8 解析的字符,默认输出�,这叫replacement character。当渲染失败时,默认展示这个字符,它的码点是U+FFFD。

Go 字符

Go 中单个字符直接存储unicode码点,不经过utf8编码。一个很直观的例子

go 复制代码
s := "世"
c := '世'
fmt.Printf("%x\n", s) // output: E4B896
fmt.Printf("%x\n", c) // output: 4E16

双引号"世"被当做字符串,存储 utf8 编码,单引号'世'被当做单个字符,直接存储 unicode 码点。

Go 给 unicode 码点取了新名字: rune。从定义来看:type rune = int32,rune和int32完全等价。这也说明码点在Go中永远都是用4字节存储。

我们可以把 string 的每个utf8字符解码成码点(rune)输出。

go 复制代码
world := "世界"
fmt.Printf("%x\n", []rune(world)) // output: [4e16 754c]
相关推荐
学长毕设42 分钟前
基于springboot的食品仓库管理系统
java·spring boot·后端
滴滴答答哒1 小时前
ASP.NET Core + FaceRecognitionDotNet 人脸识别实战:DI 单例、特征向量持久化与 1:N 比对
后端·asp.net
captain3764 小时前
MyBatis操作数据库(入门2)
java·数据库·spring boot·后端·spring·mybatis
IT_陈寒5 小时前
Java序列化踩坑记:对象循环引用让我加班两小时
前端·人工智能·后端
泡海椒5 小时前
05-雷达图多维画像-信用评级多维画像
后端
王中阳Go5 小时前
5.27 分钟干完后端和全部测试:一场三小时笔试的五个失分点,四个不在代码上
大数据·数据库·人工智能·后端·大语言模型
大哥43095 小时前
Modbus 的"字序":一个配错就变成天文数字,或者干脆变成 0
后端
岩钧5 小时前
New API 源码解析:一个生产级 AI 网关的架构与实现
后端
学到老活到老5 小时前
第 24 集:配置文件写了 80,Spring Boot 为什么读到 90?
java·后端·spring
用户360055579005 小时前
Kestrel_VLLM - 25 · 全注意力层设备化、整层对拍与访存粒度
后端