Skip to content

字符编码问题

1.什么是字符集(Character Set)

  • 形象比喻: 图书馆的收录名单
  • 核心作用: 解决有没有/是不是字符的问题 如果字符集里没有这个字符, 你就无法在计算机里表示它, 也就是会乱码
  • 解释: 字符集就是一个关于字符的集合(名单), 它只负责规定:
    1. 我的系统里有哪些字符
    2. 每个字分配一个唯一的编号(码点)是多少
    3. 它不负责告诉计算机这个编号在内存/硬盘上怎么存储(是存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(码点), 编码表就是一张映射表(字典), 它明确规定:

    1. 字符集里的第 N 号字符, 对应的二进制数值是多少
    2. 人们常把 编码表编码规则 混为一谈, 统称为 编码格式(如 UTF-8)
    3. 但在严谨概念里, 它侧重于数值的对应关系

常见编码表有哪些?

注: 通常编码表是依附于字符集存在的, 只有确定了有哪些字符才能给这些字符分配码点

字符集映射规则对照表

编码/码表映射规则特点
ASCII字符: A
十六进制(码点): 41
十进制: 65
二进制: 01000001
仅支持英文及标点符号, 且规定一个字符占一个字节
GBK字符:
十六进制(码点): 0xD6D0
十进制: 54992
二进制: 11010110 11010000
支持中文/英文及其标点符号, 且规定一个字符占两个字节
Unicode字符:
十六进制(码点): U+4E2D
十进制: 20013
支持所有字符(中文/英文/俄文/拉丁文等等)
注:这里只规定了的号码是 20013但没规定 20013 怎么写成字节流

3.什么是编码规则(Encoding Scheme / Rule)?

  • 形象比喻: 图书的打包入库规则

  • 核心作用: 解决 字符怎么存到硬盘 的问题

    1. 乱码通常就是因为 存的时候用的规则和读的时候用的规则不一致
    2. ASSCII 标准去找 这个字符, 找不到就显示 ? 或其他奇怪的字符
  • 解释:

    1. 知道了字符的数字编号(比如 = 20013), 现在要把它存入硬盘(变成字节 Byte)
    2. 编码规则决定了: 这个数字要切分成几个字节? 每个字节长什么样?
    3. 因为数字有大有小(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 转为十进制数字映射关系: UnicodeU+4E2D 这个码点对应的数字是 20013
3编码规则决定怎么把这个数字切成字节存入硬盘UTF-8编码规则存储: 20013 -> 11100100 10111000 10101101 -> E4 B8 AD 3个字节

想想如果用其他编码规则存储 会如何?

编码规则编码规则中如何拆分字节查找的编码表编码表的码点及对应数字存储的二进制
UTF-8中文字符:3字节unicodeU+4E2D -> 2001320013 -> 11100100 10111000 10101101
UTF-16中文字符:2字节unicodeU+4E2D -> 2001320013 -> 01001110 00101101
UTF-32所有字符:4字节unicodeU+4E2D -> 2001320013 -> ``
GBK中文字符:2字节gbk

想想储存时为什么要拆分字节?

  • 因为1个字节8位, 8位最多表示 0-255 这个范围
  • 码表对应的数字可能大于255, 那就导致一个字节存不下, 所以要拆开多个字节来存
  • 如何拆分字节, 就是这些编码规则所需要考虑的事情

为什么程序员会遇到乱码

  1. 你的同事用 UTF-8 规则把 存成了 E4 B8 AD (3个字节)
  2. 你拿到文件, 却误以为它是 GBK 规则编码的
  3. 你的编辑器按 GBK 规则去读:
    • 它读取前两个字节 E4 B8, 在 GBK 表里一查, 发现对应汉字
    • 它读取剩下的 AD 和下一个字的字节, 拼在一起, 查出来另一个怪字 (举例)
  4. 结果: 你看到了 涓€ 这种乱码

结论

  • 字符集错了: 字根本显示不出来(变问号 ?)
  • 编码规则错了: 字节被错误切割和映射, 变成乱码 涓€
  • 解决之道: 统一使用 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

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))
}

Released under the MIT License.