字符编码问题
1.什么是字符集(Character Set)
- 形象比喻:
图书馆的收录名单 - 核心作用:
解决有没有/是不是字符的问题如果字符集里没有这个字符, 你就无法在计算机里表示它, 也就是会乱码 - 解释: 字符集就是一个关于字符的集合(名单), 它只负责规定:
我的系统里有哪些字符- 每个字分配一个唯一的编号(码点)是多少
- 它不负责告诉计算机这个编号在内存/硬盘上怎么存储(是存1个字节还是4个字节)
常见字符集有哪些?
| 字符集 | 特点 | 局限 |
|---|---|---|
| ASCII | 美国人的小名单, 只有 128 个字符(英文字母、数字、基本标点) | 没有中文、法文、俄文放入中文会报错或变成问号 |
| ISO-8859 系列(如 Latin-1) | 欧洲人的扩展名单, 在 ASCII 基础上增加了西欧语言字符(如 é, ü, ñ), 共 256 个字符 | 依然没有中文、日文、韩文等字符 |
| GB2312 / GBK / GB18030 | 中国人的名单: GB2312(6000 多常用汉字) GBK:(2 万多汉字, 兼容 GB2312) | 主要面向中文场景, 缺少emoji和箭头等特殊字符 |
| Unicode(万国码) | 全人类的终极名单,收录世界所有语言文字、符号、Emoji, 现代编程基石, 给每个字符全球唯一 ID | 无明显局限, 需配合 UTF-8/16/32 等编码使用 |
2.什么是编码表(Encoding Table / Mapping)?
形象比喻:
图书管中图书的身份证号对照簿(每本书都有唯一的ID)核心作用:
建立字符 与 数字 的桥梁解决字符对应哪个数字的问题解释:有了字符集(名单)和 ID(码点), 编码表就是一张映射表(字典), 它明确规定:
- 字符集里的第 N 号字符, 对应的二进制数值是多少
- 人们常把
编码表和编码规则混为一谈, 统称为编码格式(如 UTF-8) - 但在严谨概念里, 它侧重于数值的对应关系
常见编码表有哪些?
注: 通常编码表是依附于字符集存在的, 只有确定了有哪些字符才能给这些字符分配码点
字符集映射规则对照表
| 编码/码表 | 映射规则 | 特点 |
|---|---|---|
| ASCII | 字符: A 十六进制(码点): 41 十进制: 65 二进制: 01000001 | 仅支持英文及标点符号, 且规定一个字符占一个字节 |
| GBK | 字符: 中 十六进制(码点): 0xD6D0 十进制: 54992 二进制: 11010110 11010000 | 支持中文/英文及其标点符号, 且规定一个字符占两个字节 |
| Unicode | 字符: 中 十六进制(码点): U+4E2D 十进制: 20013 | 支持所有字符(中文/英文/俄文/拉丁文等等) 注:这里只规定了 中的号码是 20013但没规定 20013 怎么写成字节流 |
3.什么是编码规则(Encoding Scheme / Rule)?
形象比喻: 图书的
打包入库规则核心作用: 解决
字符怎么存到硬盘的问题- 乱码通常就是因为
存的时候用的规则和读的时候用的规则不一致 - 在
ASSCII标准去找中这个字符, 找不到就显示?或其他奇怪的字符
- 乱码通常就是因为
解释:
- 知道了字符的数字编号(比如
中= 20013), 现在要把它存入硬盘(变成字节 Byte) - 编码规则决定了: 这个数字要切分成几个字节? 每个字节长什么样?
- 因为数字有大有小(ASCII 只要 1 个字节, 汉字可能要 2-4 个字节), 规则决定了存储的格式
- 知道了字符的数字编号(比如
常见编码规则有哪些?
UTF 的全称为: Unicode Transformation Format
| 编码 | 规则 | 优点 | 特点 |
|---|---|---|---|
| UTF-8 | 变长存储 英文字母: 1个字节 常用汉字: 3个字节 生僻字/Emoji: 4个字节 | 省空间, 兼容性好, 无字节序问题 | 与utf32比, 处理速度慢一些 |
| UTF-16 | 变长存储(2或4字节) 大部分常用字(包括汉字): 2个字节 生僻字/Emoji: 4个字节(代理对) | 处理汉字比UTF-8快, 节省空间(相比UTF-32) | 存在字节序问题(大端BOM vs 小端BOM) |
| UTF-32 | 定长存储 所有字符: 4个字节 | 处理速度最快,逻辑最简单 | 速度非常快, 极度浪费空间 |
| GBK编码 | 针对GBK字符集 英文: 1字节 汉字: 2字节 | 适配中文场景 | 不是Unicode的实现, 与Unicode编码混用易乱码 |
三者的关系
假设我们要在电脑上保存汉字 中:
| 步骤 | 概念 | 动作描述 | 具体数据示例 |
|---|---|---|---|
| 1 | 字符集 | 查名单, 确认中 这个字符存在, 并分配全球唯一 ID | 字符集: Unicode 字符集 中 这个字符的码点是 U+4E2D |
| 2 | 编码表 | 查字典, 将十六进制 ID 转为十进制数字 | 映射关系: Unicode 中 U+4E2D 这个码点对应的数字是 20013 |
| 3 | 编码规则 | 决定怎么把这个数字切成字节存入硬盘 | UTF-8编码规则存储: 20013 -> 11100100 10111000 10101101 -> E4 B8 AD 3个字节 |
想想如果用其他编码规则存储
中会如何?
| 编码规则 | 编码规则中如何拆分字节 | 查找的编码表 | 编码表的码点及对应数字 | 存储的二进制 |
|---|---|---|---|---|
| UTF-8 | 中文字符:3字节 | unicode | U+4E2D -> 20013 | 20013 -> 11100100 10111000 10101101 |
| UTF-16 | 中文字符:2字节 | unicode | U+4E2D -> 20013 | 20013 -> 01001110 00101101 |
| UTF-32 | 所有字符:4字节 | unicode | U+4E2D -> 20013 | 20013 -> `` |
| GBK | 中文字符:2字节 | gbk |
想想储存时为什么要拆分字节?
- 因为1个字节8位, 8位最多表示
0-255这个范围 码表对应的数字可能大于255, 那就导致一个字节存不下, 所以要拆开多个字节来存- 如何拆分字节, 就是这些编码规则所需要考虑的事情
为什么程序员会遇到乱码
- 你的同事用
UTF-8规则把中存成了E4 B8 AD(3个字节) - 你拿到文件, 却误以为它是
GBK规则编码的 - 你的编辑器按 GBK 规则去读:
- 它读取前两个字节
E4 B8, 在 GBK 表里一查, 发现对应汉字涓 - 它读取剩下的
AD和下一个字的字节, 拼在一起, 查出来另一个怪字€(举例)
- 它读取前两个字节
- 结果: 你看到了
涓€这种乱码
结论
- 字符集错了: 字根本显示不出来(变问号
?) - 编码规则错了: 字节被错误切割和映射, 变成乱码
涓€ - 解决之道: 统一使用
Unicode 字符集 + UTF-8 编码规则这是最合理的开发选择
unicode/utf8 方法
- valid: 判断
byte[]是否完全由有效的 UTF-8 编码符文组成 - validRune: 判断
rune是否可以合法编码为 UTF-8 - ValidString: 判断
字符串是否是完全由有效的 UTF-8 编码符文组成
go
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
// valid
valid := []byte("Hello, 世界")
invalid := []byte{0xff, 0xfe, 0xfd}
fmt.Println(utf8.Valid(valid)) // true
fmt.Println(utf8.Valid(invalid)) // false
// validRune
valid2 := 'a'
invalid2 := rune(0xfffffff)
fmt.Println(utf8.ValidRune(valid2)) // true
fmt.Println(utf8.ValidRune(invalid2)) // false
// validString
valid3 := "Hello, 世界"
invalid3 := string([]byte{0xff, 0xfe, 0xfd})
fmt.Println(utf8.ValidString(valid3)) // true
fmt.Println(utf8.ValidString(invalid3)) // false
}数据序列化
十六进制 hex
go
package main
import (
"encoding/hex"
"fmt"
)
func main() {
// string -> hex string
str := "hello"
hexStr := hex.EncodeToString([]byte(str))
fmt.Printf("hexStr: %s\n", hexStr)
// hex string -> string
decoded, err := hex.DecodeString(hexStr)
if err != nil {
fmt.Println(err)
}
fmt.Printf("decode: %s\n", decoded)
}base64
go
package main
import (
"encoding/base64"
"fmt"
)
func main() {
// string -> base64
base64str := base64.StdEncoding.EncodeToString([]byte("1"))
fmt.Println("base64str: ", base64str)
// base64 -> string
decode, err := base64.StdEncoding.DecodeString(base64str)
if err != nil {
fmt.Println("base64 decode err: ", err)
}
fmt.Println("origin str: ", string(decode))
}json/xml/toml/yaml/jsonc/json5
- 标准库:encoding/json 老版本用的, 速度比较慢
- 标准库:encoding/json/v2 提高 Date/Datetime 等时间日期处理速度
- 标准库:encoding/xml
- 开源库:toml
- 开源库:go-toml
- 开源库:go-yaml
- 开源库:sonic 字节开源的更快的JSON处理包
- 开源库:go-json 更快的json处理包,兼容标准库 encoding/json
- 开源库:jsonc
- 开源库:json5
go
package main
import (
"encoding/json"
"fmt"
)
func main() {
// 1. json 字符串转 go 语言 map
jsonStr := `{
"id": 1001,
"name": "张三",
"skills": ["Go", "Python"],
"email": "zhangsan@example.com",
"avatar": "https://avatars.githubusercontent.com/u/123456?v=4"
}`
dataMap := map[string]any{}
err := json.Unmarshal([]byte(jsonStr), &dataMap)
if err != nil {
fmt.Println("JSON 字符串解析出现错误: ", err)
}
fmt.Println("dataMap:", dataMap)
// 2. go 语言 map 转 json 字符串
dataMap["email"] = "zhangsan@test.com" // update value
jsonBytes, err := json.Marshal(dataMap)
if err != nil {
fmt.Println("Map 转 JSON 失败", err)
}
fmt.Println("jsonStr:", string(jsonBytes))
}go
package main
import (
"encoding/json"
"fmt"
)
type User struct {
ID int `json:"id"` // 映射 json 的 "id" 字段
Name string `json:"name"` // 映射 json 的 "name" 字段
Skills []string `json:"skills"` // 映射 json 的 "skills" 字段(数组)
Email string `json:"email"` // 映射 json 的 "Email" 字段
Avatar string `json:"avatar,omitempty"` // omitempty: 如果为空则不生成该字段,反序列化时无影响
}
func main() {
// 1. json 字符串转 go 语言 struct
jsonBytes := `{
"id": 1001,
"name": "张三",
"skills": ["Go", "Python"],
"email": "zhangsan@example.com",
"avatar": "https://avatars.githubusercontent.com/u/123456?v=4"
}`
var user User
err := json.Unmarshal([]byte(jsonBytes), &user)
if err != nil {
fmt.Println("json 转 struct 出现错误:", err)
}
fmt.Println("user:", user)
// update values
user.Skills = append(user.Skills, "JavaScript")
// 2. go 语言 struct 转 json 字符串
jsonStr, err := json.Marshal(user)
if err != nil {
fmt.Println("struct 转 json 出现错误:", err)
}
fmt.Println("jsonStr:", string(jsonStr))
}