Golang 的字符编码与 regexp
前言
最近在使用 Golang 的 regexp 对网络流量做正则匹配时,发现有些情况无法正确进行匹配,找到资料发现 regexp 内部以 UTF-8 编码的方式来处理正则表达式,而网络流量是字节序列,由其中的非 UTF-8 字符造成的问题。
我们这里从 Golang 的字符编码和 regexp 处理机制开始学习和分析问题,并寻找一个有效且比较通用的解决方法,本文对此进行记录。
本文代码测试环境 go version go1.14.2 darwin/amd64
regexp匹配字节序列
我们将匹配网络流量所遇到的问题,进行抽象和最小化复现,如下:

我们可以看到 \xff 没有按照预期被匹配到,那么问题出在哪里呢?
UTF-8编码
翻阅 Golang 的资料,我们知道 Golang 的源码采用 UTF-8 编码, regexp 库的正则表达式也是采用 UTF-8 进行解析编译(而且 Golang 的作者也是 UTF-8 的作者),那我们先来看看 UTF-8 编码规范。
1.ASCII
在计算机的世界,字符最终都由二进制来存储,标准 ASCII 编码使用一个字节(低7位),所以只能表示 127 个字符,而不同国家有不同的字符,所以建立了自己的编码规范,当不同国家相互通信的时候,由于编码规范不同,就会造成乱码问题。

2.Unicode
为了解决乱码问题,提出了 Unicode 字符集,为所有字符分配一个独一无二的编码,随着 Unicode 的发展,不断添加新的字符,目前最新的 Unicode 采用 UCS-4(Unicode-32) 标准,也就是使用 4 字节(32位) 来进行编码,理论上可以涵盖所有字符。
但是 Unicode 只是字符集,没有考虑计算机中的使用和存储问题,比如:
与已存在的 ASCII 编码不兼容,ASCII(A)=65 / UCS-2(A)=0065
由于 Unicode 编码高字节可能为 0,C 语言字符串串函数将出现 00 截断问题
从全世界来看原来 ASCII 的字符串使用得最多,而换成 Unicode 过后,这些 ASCII 字符的存储都将额外占用字节(存储0x00)
3.UTF-8
后来提出了 UTF-8 编码方案,UTF-8 是在互联网上使用最广的一种 Unicode 的实现方式;UTF-8 是一种变长的编码方式,编码规则如下:
对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 的码点,兼容 ASCII
对于需要 n 字节来表示的符号(n > 1),第一个字节的前 n 位都设为 1,第 n+1 位设置为 0;后面字节的前两位一律设为 10,剩下的的二进制位则用于存储这个符号的 Unicode 码点(从低位开始)。
编码规则如下:

编码中文 你 如下:

1.根据 UTF-8 编码规则,当需要编码的符号超过 1 个字节时,其第一个字节前面的 1 的个数表示该字符占用了几个字节。
2.UTF-8 是自同步码(Self-synchronizing_code),在 UTF-8 编码规则中,任意字符的第一个字节必然以 0 / 110 / 1110 / 11110 开头,UTF-8 选择 10 作为后续字节的前缀码,以此进行区分。自同步码可以便于程序寻找字符边界,快速跳过字符,当遇到错误字符时,可以跳过该字符完成后续字符的解析,这样不会造成乱码扩散的问题(GB2312存在该问题)
byte/rune/string
在 Golang 中源码使用 UTF-8 编码,我们编写的代码/字符会按照 UTF-8 进行编码,而和字符相关的有三种类型 byte/rune/string。
byte 是最简单的字节类型(uint8),string 是固定长度的字节序列,其定义和初始化在 https://github.com/golang/go/blob/master/src/runtime/string.go,可以看到 string 底层就是使用 []byte 实现的:

rune 类型则是 Golang 中用来处理 UTF-8 编码的类型,实际类型为 int32,存储的值是字符的 Unicode 码点,所以 rune 类型可以便于我们更直观的遍历字符(对比遍历字节)如下:

类型转换
byte(uint8) 和 rune(int32) 可以直接通过位扩展或者舍弃高位来进行转换。
string 转换比较复杂,我们一步一步来看:
string 和 byte 类型相互转换时,底层都是 byte 可以直接相互转换,但是当单字节 byte 转 string 类型时,会调用底层函数 intstring() (https://github.com/golang/go/blob/master/src/runtime/string.go#L244),然后调用 encoderune() 函数,对该字节进行 UTF-8 编码,测试如下:

string 和 rune 类型相互转换时,对于 UTF-8 字符的相互转换,底层数据发生变化 UTF-8编码 <=> Unicode编码;而对于非 UTF-8 字符,将以底层单字节进行处理:
string => rune 时,会调用 stringtoslicerune() (https://github.com/golang/go/blob/master/src/runtime/string.go#L178),最终跟进到 Golang 编译器的 for-range 实现(https://github.com/golang/go/blob/master/src/cmd/compile/internal/walk/range.go#L220),转换时调用 decoderune() 对字符进行 UTF-8 解码,解码失败时(非 UTF-8 字符)将返回 RuneError = \uFFFD;
rune => string 时,和 byte 单字节转换一样,会调用 intstring() 函数,对值进行 UTF-8 编码。
测试如下:

regexp处理表达式
在 regexp 中所有的字符都必须为 UTF-8 编码,在正则表达式编译前会对字符进行检查,非 UTF-8 字符将直接提示错误;当然他也支持转义字符,比如:\t \a 或者 16进制,在代码中我们一般需要使用反引号包裹正则表达式(原始字符串),转义字符由 regexp 在内部进行解析处理,如下:

当然为了让 regexp 编译包含非 UTF-8 编码字符的表达式,必须用反引号包裹才行
我们在使用 regexp 时,其内部首先会对正则表达式进行编译,然后再进行匹配。
1.编译
编译主要是构建自动机表达式,其底层最终使用 rune 类型存储字符(https://github.com/golang/go/blob/master/src/regexp/syntax/prog.go#L112),所以 \xff 通过转义后最终存储为 0x00ff (rune)

除此之外,在编译阶段 regexp 还会提前生成正则表达式中的前缀字符串,在执行自动机匹配前,先用匹配前缀字符串,以提高匹配效率。需要注意的是,生成前缀字符串时其底层将调用 strings.Builder 的 WriteRune() 函数(https://github.com/golang/go/blob/master/src/regexp/syntax/prog.go#L147),内部将调用 utf8.EncodeRune() 强制转换表达式的字符为 UTF-8 编码(如:\xff => \xc3\xbf)。
2.匹配
当匹配时,首先使用前缀字符串匹配,这里使用常规的字符串匹配。UTF-8 可以正常进行匹配,但当我们的字符串中包含非 UTF-8 字符就会出现问题,原因正则表达式中的前缀字符串已经被强制 UTF-8 编码了,示例如下:

当执行自动机匹配时,将最终调用 tryBacktrace() 函数进行逐字节回溯匹配(https://github.com/golang/go/blob/master/src/regexp/backtrack.go#L140),使用 step() 函数遍历字符串(https://github.com/golang/go/blob/master/src/regexp/regexp.go#L383),该函数有 string/byte/rune 三种实现,其中 string/byte 将调用 utf8.DecodeRune*() 强制为 rune 类型,所以三种实现最终都返回 rune 类型,然后和自动机表达式存储的 rune 值进行比较,完成匹配。而这里当非 UTF-8 字符通过 utf8.DecodeRune*() 函数时,将返回 RuneError=0xfffd,示例如下:

比较复杂,不过简而言之就是 regexp 内部会对表达式进行 UTF-8 编码,会对字符串进行 UTF-8 解码。
了解 regexp 底层匹配运行原理过后,我们甚至可以构造出更奇怪的匹配:

解决方法
在了解以上知识点过后,就很容易解决问题了:表达式可以使用任意字符,待匹配字符串在匹配前手动转换为合法的 UTF-8 字符串。
因为当 regexp 使用前缀字符串匹配时,会自动转换表达式字符为 UTF-8 编码,和我们的字符串一致;当 regexp 使用自动机匹配时,底层使用 rune 进行比较,我们传入的 UTF-8 字符串将被正确通过 UTF-8 解码,可以正确进行匹配。
实现测试如下:

总结
关于开头提出的 regexp 匹配的问题到这里就解决了,在不断深入语言实现细节的过程中发现:Golang 本身在尽可能的保持 UTF-8 编码的一致性,但在编程中字节序列是不可避免的,Golang 中使用 string/byte 类型来进行处理,在 regexp 底层实现同样使用了 UTF-8 编码,所以问题就出现了,字节序列数据和编码后的数据不一致。
个人感觉 regexp 用于匹配字节流并不是一个预期的使用场景,像是 Golang 官方在 UTF-8 方面的一个取舍。
当然这个过程中,我们翻阅了很多 Golang 底层的知识,如字符集、源码等,让我们了解了一些 Golang 的实现细节;在实际常见下我们不是一定要使用标准库 regexp,还可以使用其他的正则表达式库来绕过这个问题。

喜欢的朋友记得点赞、收藏、关注哦!!!
相关文章:

Golang 的字符编码与 regexp
前言 最近在使用 Golang 的 regexp 对网络流量做正则匹配时,发现有些情况无法正确进行匹配,找到资料发现 regexp 内部以 UTF-8 编码的方式来处理正则表达式,而网络流量是字节序列,由其中的非 UTF-8 字符造成的问题。 我们这里从 G…...

利用ollama 与deepseek r1大模型搭建本地知识库
1.安装运行ollama ollama下载 https://ollama.com/download/windows 验证ollama是否安装成功 ollama --version 访问ollama本地地址: http://localhost:11434/ 出现如下界面 ollama运行模型 ollama run llama3.2 ollama常用操作命令 启动 Ollama 服务…...

Java短信验证功能简单使用
注册登录阿里云官网:https://www.aliyun.com/ 搜索短信服务 自己一步步申请就可以了 开发文档: https://next.api.aliyun.com/api-tools/sdk/Dysmsapi?version2017-05-25&languagejava-tea&tabprimer-doc 1.引入依赖 <dependency>…...

CAS单点登录(第7版)21.可接受的使用政策
如有疑问,请看视频:CAS单点登录(第7版) 可接受的使用政策 概述 可接受的使用政策 CAS 也称为使用条款或 EULA,它允许用户在继续应用程序之前接受使用策略。此功能的生产级部署需要修改流,以便通过外部存…...

53倍性能提升!TiDB 全局索引如何优化分区表查询?
作者: Defined2014 原文来源: https://tidb.net/blog/7077577f 什么是 TiDB 全局索引 在 TiDB 中,全局索引是一种定义在分区表上的索引类型,它允许索引分区与表分区之间建立一对多的映射关系,即一个索引分区可以对…...

Pythong 解决Pycharm 运行太慢
Pythong 解决Pycharm 运行太慢 官方给Pycharm自身占用的最大内存设低估了限制,我的Pycharm刚开始默认是256mb。 首先找到自己的Pycharm安装目录 根据合适自己的改 保存,重启Pycharm...

库里存储的数据有大量回车时,该如何进行存取
如图,打印模板存了很多坐标性的字段数据: 大量带换行的文本数据存到库里之后取出,前端需要做非空、合法校验, 然后在循环中,使用eval 函数接收每一句字符串,去执行这句 JavaScript 代码。 let arrStr tem…...

【devops】Github Actions Secrets | 如何在Github中设置CI的Secret供CI的yaml使用
一、Github Actions 1、ci.yml name: CIon: [ push ]jobs:build:runs-on: ubuntu-lateststeps:- name: Checkout codeuses: actions/checkoutv3- name: Set up Gouses: actions/setup-gov4with:go-version: 1.23.0- name: Cache Go modulesuses: actions/cachev3with:path: |…...

体验 DeepSeek-R1:解密 1.5B、7B、8B 版本的强大性能与应用
文章目录 🍋引言🍋DeepSeek 模型简介🍋版本更新:1.5B、7B、8B 的区别与特点🍋模型评估🍋体验 DeepSeek 的过程🍋总结 🍋引言 随着大规模语言模型的持续发展,许多模型在性…...
一文说清楚什么是Token以及项目中使用Token延伸的问题
首先可以参考我的往期文章,我这里说清楚了Cookie,Seesion,Token以及JWT是什么 其实Token你就可以理解成这是一个认证令牌就好了 详细分清Session,Cookie和Token之间的区别,以及JWT是什么东西_还分不清 cookie、sessi…...
大模型-Tool call、检索增强
大模型 Tool call 心知天气:https://www.seniverse.com/ 例子:调用天气接口 API from openai import OpenAI import requests import json """ ##### 天气接口 API 密钥获取:https://www.free-api.com/doc/558 ##### &quo…...
【算法】【区间和】acwing算法基础 802. 区间和 【有点复杂,但思路简单】
题目 假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。 现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。 接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] …...

Ubuntu22.04通过Docker部署Jeecgboot
程序发布环境包括docker、mysql、redis、maven、nodejs、npm等。 一、安装docker 1、用如下命令卸载旧Docker: for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done 2、安装APT环境依赖包…...

HTML4
HTML 初体验 1.鼠标右键 > 新建 > 文本文档 > 输入以下内容,并保存 2.修改后缀为 .html ,然后双击打开即可 这里的后缀名,使用 .htm 也可以,但推荐使用更标准的 .html <marquee>尚硅谷,让天下没有难…...

STM32F10X 启动文件完整分析
最近在准备面试相关 顺便复盘总结一下之前的内容 启动文件在基于ARM的芯片是很重要的组成部分,它主要负责完成芯片上电启动时的一系列初始化工作和各种异常及中断的入口地址。 也是理解bootloader自举的关键点,所以需要理解一下 1. 向量表定义 启动文件…...
typescript快速入门之安装与运行
安装 安装ts环境,最好全局安装,这样就不需要开一个项目又安装 npm i -g typescript初始化 可以运行初始化配置文件,也可以手动生成;不生成的话会运行默认配置 使用默认配置 把ts文件转成js文件使用的是es3语言,语…...
React源码解读
配置React源码本地调试环境 本次环境构建采用了node版本为16、react-scripts 版本号为 3.4.4,源码下载地址 react源码调试: react源码调试环境 使用 create-react-app 脚手架创建项目 npx create-react-app react-test 进入刚刚下载的目录,弹射 crea…...

【DeepSeek-R1】 API申请(火山方舟联网版)
DeepSeek-R1 API申请(火山方舟联网版) 1、新建联网版应用2、开通信息增强服务3、开启联网内容插件4、创建接入点5、获取模型名称6、获取API Key 如果第一次注册账号,请先按照文章《【Deepseek-R1】 API申请(火山方舟)》…...

负载均衡集群——LVS-DR配置
一、简介 1.1 什么是集群? 两台及以上的计算机完成一个任务的模式称为集群。 常见的集群类型包括: LB(负载均衡)集群:按照不同的算法将前端的访问转发给后端计算点,使节点负载相对平衡。提高并发能力 缺…...

数据结构篇
链表 用数组模拟链表,看该链表结构,有几个域则用几个数组分别存储 单链表是只知道下一个元素位置,双链表还知道上一个链表位置 单链表 双向链表 左移右移 栈 模拟栈 判断括号序列 队列 模拟队列 递归 集合和哈希 集合就是哈希表 哈希表的实现…...

剑指offer20_链表中环的入口节点
链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...

RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...

优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

企业如何增强终端安全?
在数字化转型加速的今天,企业的业务运行越来越依赖于终端设备。从员工的笔记本电脑、智能手机,到工厂里的物联网设备、智能传感器,这些终端构成了企业与外部世界连接的 “神经末梢”。然而,随着远程办公的常态化和设备接入的爆炸式…...

Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习)
Aspose.PDF 限制绕过方案:Java 字节码技术实战分享(仅供学习) 一、Aspose.PDF 简介二、说明(⚠️仅供学习与研究使用)三、技术流程总览四、准备工作1. 下载 Jar 包2. Maven 项目依赖配置 五、字节码修改实现代码&#…...

打手机检测算法AI智能分析网关V4守护公共/工业/医疗等多场景安全应用
一、方案背景 在现代生产与生活场景中,如工厂高危作业区、医院手术室、公共场景等,人员违规打手机的行为潜藏着巨大风险。传统依靠人工巡查的监管方式,存在效率低、覆盖面不足、判断主观性强等问题,难以满足对人员打手机行为精…...
在 Spring Boot 项目里,MYSQL中json类型字段使用
前言: 因为程序特殊需求导致,需要mysql数据库存储json类型数据,因此记录一下使用流程 1.java实体中新增字段 private List<User> users 2.增加mybatis-plus注解 TableField(typeHandler FastjsonTypeHandler.class) private Lis…...
如何配置一个sql server使得其它用户可以通过excel odbc获取数据
要让其他用户通过 Excel 使用 ODBC 连接到 SQL Server 获取数据,你需要完成以下配置步骤: ✅ 一、在 SQL Server 端配置(服务器设置) 1. 启用 TCP/IP 协议 打开 “SQL Server 配置管理器”。导航到:SQL Server 网络配…...

对象回调初步研究
_OBJECT_TYPE结构分析 在介绍什么是对象回调前,首先要熟悉下结构 以我们上篇线程回调介绍过的导出的PsProcessType 结构为例,用_OBJECT_TYPE这个结构来解析它,0x80处就是今天要介绍的回调链表,但是先不着急,先把目光…...
C#最佳实践:为何优先使用as或is而非强制转换
C#最佳实践:为何优先使用as或is而非强制转换 在 C# 的编程世界里,类型转换是我们经常会遇到的操作。就像在现实生活中,我们可能需要把不同形状的物品重新整理归类一样,在代码里,我们也常常需要将一个数据类型转换为另…...