浅谈golang字符编码

1、 Golang 字符编码

Golang 的代码是由 Unicode 字符组成的,并由 Unicode 编码规范中的 UTF-8 编码格式进行编码并存储。

Unicode 是编码字符集,囊括了当今世界使用的全部语言和符号的字符。有三种编码形式:UTF-8UTF-16UTF-32。(UTF: Unicode Transformation Format,统一码转换格式)

在这几种编码格式的名称中,- 右边的整数的含义是,以多少个比特作为一个编码单元。以 UTF-8 为例,它会以 8 个比特也就是一个字节,作为一个编码单元。并且,它与标准的 ASCII 编码是完全兼容的。也就是说,在 [0x00, 0x7F]的范围内,这两种编码表示的字符都是相同的,这也是 UTF-8 编码格式的一个巨大优势(这里不探讨 UTF-16UTF-32)。

UTF-8 是一种可变长的编码方案。换句话说,它会用一个或多个字节来表示某个字符,最多使用四个字节。比如,对于一个英文字符,它仅用一个字节就可以表示,而对于一个中文字符,它需要使用三个字节才能够表示。不论怎样,一个受支持的字符总是可以由 UTF-8 编码为一个字节序列。以下会简称后者为 UTF-8 编码值。

在这里插入图片描述
从上图可知 UTF-8 的编码方式:

  • 什么时候读1个字节的字符?
    • 字节的第一位为0,后面7位为符号的unicode码。所以这样看,英语字母的utf-8ascii一致。
  • 什么时候读多个字节的字符?
    • 对于有n个字节的字符,(n>1)…. 其中第一个字节的高n位就为1,换句话说:
      • 第一个字节读到0,那就是读1个字节
      • 第一个字节读到n1,就要读n个字节
0xxxxxxx # 读1个字节
110xxxxx 10xxxxxx # 读2个字节
1110xxxx 10xxxxxx 10xxxxxx #读3个字节
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx #读4个字节Unicode符号范围      |        UTF-8编码方式
(十六进制)           |        (二进制)
------------------ -+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

Unicode 是如何填充UTF-8各个字节的呢?

比如 这个汉字,对应的 unicode编码为 U+7801

  • 对应的十六进制处于 0000 0800-0000 FFFF 中,也就是 3 个字节,相应的二进制为 1110xxxx 10xxxxxx 10xxxxxx
  • unicode编码为 U+7801 对应的二进制为 111100000000001,为了和接下来填充字节方便,这里做个格式优化 111 100000 000001
  • 从后向前填充,高位不够的补0
  • 000001 填充第三个字节(从左往右数)10000001
  • 100000 填充第二个字节 10100000
  • 111 填充第一个字节,高位不够的就补0,为 11100111
  • 最终结果为 11100111 10100000 10000001(对应的十六进制分别对应 e7 a0 81
func TestInt(t *testing.T) {s1 := "码"for i := 0; i < len(s1); i++ {fmt.Printf("%x ", s1[i])}
}

打印的结果为 e7 a0 81,和上面演算的一致。

2、string 数据结构

先来看看 Golangstring 的数据结构

type StringHeader struct {Data uintptrLen  int
}

其中包含指向字节数组的指针 Data 和数组的大小 Len,后者 Len 方便在 len() 时可以 O(1) 时间给出大小,就是常见的以空间换时间。字符串由字符组成,字符的底层由字节组成,而一个字符串在底层的表示是一个字节序列,这个字节序列就存储在 Data 里,不过是只读的。

import ("fmt""testing"
)func TestStr(t *testing.T) {str := "Hello World"fmt.Println(str)
}

把上面代码 go tool compile -S str_test.go > str_test.S 生成汇编代码,然后找到

go.string."Hello World" SRODATA dupok size=110x0000 48 65 6c 6c 6f 20 57 6f 72 6c 64                 Hello World

能够看到 Hello World 旁有一个 SRODATA 的标记,在 Golang 中编译器会将只读数据标记成 SRODATA

再来看看 slice 的数据结构

type SliceHeader struct {Data uintptrLen  intCap  int
}

相比 string 多了个 Cap,因此在 Golang 中,字符串实际上是只读的字节切片。

那么对于只读的 string,若是想要改值应该怎么弄呢?

func TestModifyString(t *testing.T) {str := "golang编程"l := []byte(str)l[0] = 'G'fmt.Println(string(l)) // Golang编程
}

转成相应的字节数组,然后以索引的形式更新值。

3、string 编码方式

前面说过,字符串由字符组成,字符的底层由字节组成,而一个字符串在底层的表示是一个字节序列。在 Golang 中,字符可以被分成两种类型处理:对占 1 个字节的英文类字符,可以使用 byte(或者 unit8);对占 1 ~ 4 个字节的其他字符,可以使用 rune(或者int32),如中文、特殊符号等。

// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

可以看到 byterune 其实分别就是 uint8int32 的别名,byte1 个字节, rune4个字节。

func TestStrLen(t *testing.T) {str1 := "go"str2 := "go编程"fmt.Printf("%v len is %d\n", str1, len(str1))fmt.Printf("%v len is %d\n", str2, len(str2))}

运行后,发现 str1 长度为 2 这个没问题,但 str2 的长度不是 4 而是 8,这是什么原因呢?

先不着急找答案,看看下面的代码

func printBytes(s string) {fmt.Printf("Bytes: ")for i := 0; i < len(s); i++ {fmt.Printf("%x ", s[i]) // 按十六进制输出}fmt.Printf("\n")
}func printChars(s string) {fmt.Printf("Charaters: ")for i := 0; i < len(s); i++ {fmt.Printf("%c ", s[i]) // 将数字转换成它对应的 Unicode 字符}fmt.Printf("\n")
}func TestInt(t *testing.T) {s1 := "go编程"fmt.Printf("s1: %s, bytes len(s1)=%d\n", s1, len(s1))fmt.Printf("s1: %s, rune  len(s1)=%d\n", s1, len([]rune(s1)))printBytes(s1)printChars(s1)
}

运行后打印如下

s1: go编程, bytes len(s1)=8
s1: go编程, rune  len(s1)=4
Bytes: 67 6f e7 bc 96 e7 a8 8b 
Charaters: g o ç ¼ – ç ¨

仔细看,发现 rune 类型的输出了 4,另外 printChars 输出乱码了。

先来看看 rune 类型,是 int32 的别名,也就是说,一个 rune 类型的值会由 4 个字节宽度的空间来存储。它的存储空间总是能够存下一个 UTF-8 编码值。一个 rune 类型的值在底层其实就是一个 UTF-8 编码值。前者是(便于我们人类理解的)外部展现,后者是(便于计算机系统理解的)内在表达。

Golang 中常用 rune 类型来处理中文。printChars 之所以输出乱码,是因为在第一节中提到的在 UTF-8 中汉字是以三个字节存储的,len() 是按单字节来计算长度,因此对于三个字节的中文来说输出三分之铁定乱码。那么如何输出才不乱码呢?

func TestRune(t *testing.T) {str := "golang编程"l := []rune(str)for i := 0; i < len(l); i++ {fmt.Printf("%c ", l[i])}
}

打印输出 g o l a n g 编 程

当然了,还可以使用 for range 来打印字符串里的中文。

func TestRange(t *testing.T) {str := "golang编程"for i, s := range str {fmt.Printf("%d: %c\n", i, s)}
}

打印输出

0: g
1: o
2: l
3: a
4: n
5: g
6: 编
9: 程

那为什么会这样呢?原因就在 Golang 中,会把 for range 结构转换成如下所示的形式

	// Transform string range statements like "for v1, v2 = range a" intoha := afor hv1 := 0; hv1 < len(ha); {hv1t := hv1hv2 := rune(ha[hv1])if hv2 < utf8.RuneSelf {hv1++} else {hv2, hv1 = decoderune(ha, hv1)}v1, v2 = hv1t, hv2// original body}

for range 循环在迭代字符串时会逐个处理字符串中的 Unicode 码点(rune),而不是字节。由于 Golang 的原生字符串类型是以 UTF-8 编码的,UTF-8 是一种能够表示 Unicode 码点的变长编码方式,for range 循环能够正确处理这种编码。

通俗点就是 for range 会先把被遍历的字符串值拆成一个字节序列,然后再试图找出这个字节序列中包含的每一个 UTF-8 编码值,或者说每一个 Unicode字符。

func TestRange(t *testing.T) {str := "golang编程"for i, s := range str {fmt.Printf("%d: %c [% x]\n", i, s, []byte(string(s)))}
}

打印输出

0: g [67]
1: o [6f]
2: l [6c]
3: a [61]
4: n [6e]
5: g [67]
6:[e7 bc 96]
9:[e7 a8 8b]

由此可以看出,字符串中相邻 Unicode 字符的索引值不一定是连续的。 这取决于前一个 Unicode 字符是否为单字节字符(byte)。Golang 中的一个 string 类型值会由若干个 Unicode 字符组成,每个 Unicode 字符都可以由一个 rune 类型的值来承载。这些字符在底层都会被转换为 UTF-8 编码值,而这些 UTF-8 编码值又会以字节序列的形式表达和存储。因此,一个string 类型的值在底层就是一个能够表达若干个 UTF-8 编码值的字节序列。

ok,到这里了,发现两种不同的 for 循环在输出字符串的字符时会有所不同,这里做个归类

  • for-standalone 会遍历字符串的每一个字节(Byte类型),在遇到字符串中有汉字时会乱码
  • for-range 会遍历字符串的每一个 Unicode 字符(Rune 类型) ,在遇到字符串中有汉字时不会乱码

最后说说 stringbyterune 三者之间的关系。

  • string 在底层的表示是由单个字节组成的只读的字节序列,Golang 的字符串是以 UTF-8 编码存储的,这意味着它们可以包含任意的 Unicode 字符。Golang 把字符分 byterune 两种类型处理。
  • byte 是类型 unit8 的别名,用于存放占 1 个字节的 ASCII 字符,如英文字符,返回的是字符原始字节。由于 Golang 的字符串是以 UTF-8 编码的,一个 byte 可能表示一个字符的一部分(对于多字节字符如中文字符),也可能表示一个完整的字符(对于 ASCII 字符)。
  • rune 是类型 int32 的别名,用于存放多字节字符,如占 3 字节的中文字符,返回的是字符 Unicode 码点值(或者说它代表一个 Unicode 码点)。在处理字符串时,rune 用于表示字符串中的一个完整的 Unicode 字符,无论这个字符是由多少个字节组成的。rune 类型的变量可以存储任何 Unicode 字符,包括那些由多个字节表示的字符。

等等,等等,到这里,不妨再多看看。那么如果计算一个字符串的长度呢,用自带的 len() 函数对于单字节的字符串来说是准确的,若是带有中文字符这种多字节的字符串就不准确了,这时除了自己造轮子外,其实可以用 Golang 内置的 utf8.RuneCountInString 来统计。

func TestCountStr(t *testing.T) {str := "golang编程"fmt.Println(utf8.RuneCountInString(str)) // 8
}

有兴趣的读者可以看看其内部实现。

// RuneCountInString is like RuneCount but its input is a string.
func RuneCountInString(s string) (n int) {ns := len(s)for i := 0; i < ns; n++ {c := s[i]if c < RuneSelf {// ASCII fast pathi++continue}x := first[c]if x == xx {i++ // invalid.continue}size := int(x & 7)if i+size > ns {i++ // Short or invalid.continue}accept := acceptRanges[x>>4]if c := s[i+1]; c < accept.lo || accept.hi < c {size = 1} else if size == 2 {} else if c := s[i+2]; c < locb || hicb < c {size = 1} else if size == 3 {} else if c := s[i+3]; c < locb || hicb < c {size = 1}i += size}return n
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/30525.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024年项目进度控制软件大比拼:找出适合您团队的最佳工具

本文整理了9大热门项目进度控制软件&#xff1a;PingCode、Worktile、Monday.com、Asana、Trello、Jira、ClickUp、Wrike、Zoho Projects。并且进行详细介绍对比。 在项目管理工具的选择上&#xff0c;不同规模的团队有着各自的需求和偏好。例如&#xff0c;小型团队倾向于选择…

新手搭建Magic-API

项目场景&#xff1a; 我本是一个前端和GIS开发工程师&#xff0c;但新单位并没有配置完整的开发团队&#xff0c;确切说目前只有我一个人做开发&#xff0c;那么肯定避免不了要研究下后端。最近有一个小程序要开发&#xff0c;管理平台我直接用的fastAdminthinkphp写完了页面…

终极版本的Typora上传到博客园和csdn

激活插件 下载网址是这个&#xff1a; https://codeload.github.com/obgnail/typora_plugin/zip/refs/tags/1.9.4 解压之后这样的&#xff1a; 解压之后将plugin&#xff0c;复制到自己的安装目录下的resources 点击安装即可&#xff1a; 更改配置文件 "dependencies&q…

XL5300 dTOF测距模块 加镜头后可达7.6米测距距离 ±4%测距精度

XL5300 直接飞行时间&#xff08;dToF&#xff09;传感器是一个整体方案dTOF 模组&#xff0c;应用设计简单。片内集成了单光子雪崩二极管&#xff08;SPAD&#xff09;接收阵列以及VCSEL激光发射器。利用自主研发的 SPAD 和独特的ToF 采集与处理技术&#xff0c;XL5300模块可实…

软件产品进行确认测试有什么好处?第三方软件测试机构分享

软件确认测试是一项旨在验证软件是否符合预期需求和规格的测试活动。通过确认测试&#xff0c;您可以确保软件的功能、性能和用户界面的符合程度&#xff0c;从而降低软件发布后出现问题的风险。 一、软件产品进行确认测试的好处   1、减少软件发布后修复问题的成本。通过及…

python 版本管理工具 pyenv-win 安装

一、下载 pyenv pyenv-win 使用 powershell 下载 Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1" -OutFile "./install-pyenv-win.ps1"; &"./ins…

Vue59-全局事件总线:任意组件间通信

一、原理图 只是总结出的经验&#xff0c;不是新的API&#xff01; 二、x的要求&#xff1a; 1、保证x被所有组件看见&#xff1b; 2、x可以调用的到$on&#xff0c;才能绑定事件&#xff0c;还能调用到&#xff1a;$of&#xff0c; $emit&#xff1b; 三、x的创建&#xff…

机器学习课程复习——奇异值分解

1. 三种奇异值分解 奇异值分解(Singular Value Decomposition, SVD)包含了: 完全奇异值分解(Complete Singular Value Decomposition, CSVD)紧奇异值分解(Tight Singular Value Decomposition, TSVD)截断奇异值分解(Truncated Singular Value Decomposition, TSVD)no…

助力低空经济-eVTOL/无人机ADS-B航管应答机选型指南

一、低空经济概述 “低空经济”在今年全国两会首次写入政府工作报告。近日&#xff0c;工业和信息化部、科学技术部、财政部、中国民用航空局印发《通用航空装备创新应用实施方案&#xff08;2024—2030年&#xff09;》&#xff0c;提出到2030年&#xff0c;推动低空经济形成…

主窗体设计

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 Python、QT与PyCharm配置完成后&#xff0c;接下来需要对快手爬票的主窗体进行设计&#xff0c;首先需要创建主窗体外层为&#xff08;红色框内&…

相交链表(Leetcode)

题目分析&#xff1a; . - 力扣&#xff08;LeetCode&#xff09; 相交链表&#xff1a;首先我想到的第一个思路是&#xff1a;如图可知&#xff0c;A和B链表存在长度差&#xff0c;从左边一起遍历链表不好找交点&#xff0c;那我们就从后面开始找&#xff0c;但是这是单链表&…

一个新的剪辑拼接图片和视频类APP在测试阶段需要测试内容,以iPhone APP为例:

1.UI参照原型图和设计稿 如有改动&#xff0c;需及时沟通 2.iPad转屏、不同iPhone和iPad机型测试 3.黑夜白天模式 2.各功能模块流程需要测试跑通 3.订阅支付模块 a. UI设计是否和设计稿一致 b.涉及订阅的位置都要测试 c.免费试用是否显示&#xff1b;试用结束后&#xff0c…

HDFS笔记

第1章 HDFS概述 1.1 HDFS产出背景及定义 1&#xff09;HDFS产生背景 随着数据量越来越大&#xff0c;在一个操作系统存不下所有的数据&#xff0c;那么就分配到更多的操作系统管理的磁盘中&#xff0c;但是不方便管理和维护&#xff0c;迫切需要一种系统来管理多台机器上的文…

typeScript debug 调试

以leetcode 20为例 0.首先编写代码 function isValid(s: string): boolean {let stack: string[] []for (let index 0; index < s.length; index) {let x: string s[index]debuggerswitch (x) {case (:stack.push())breakcase [:stack.push(])breakcase {:stack.push(})…

快速压缩前端项目

背景 作为前端开发工程师难免会遇到需要把项目压缩成压缩文件来传送的情况&#xff0c;这时候需要压缩软件进行压缩文件处理 问题 项目中的依赖包文件非常庞大&#xff0c;严重影响压缩速度&#xff0c;即使想先删除再压缩&#xff0c;删除文件也不会很快完成 解决 首先要安…

EXCELITAS电源维修TLX302高压电源维修

埃赛力达电源维修 EXCELITAS电源维修 海曼电源维修 高压电源维修 EXCELITAS高压电源维修故障包括&#xff1a;无输出&#xff0c;高压达不到&#xff0c;电流达不到标准&#xff0c;高压打火,高压线接头处太靠近铁壳部分。无光,风扇不转。保险丝断&#xff0c;可以强制发光,不…

Java——构造器(构造方法)和 this

一、什么是构造器 构造器&#xff08;Constructor&#xff09;是Java类的一种特殊方法&#xff0c;用于初始化对象的状态。构造器在创建对象时被调用&#xff0c;可以对对象的成员变量进行初始化。 我之前的文章《Java——类和对象-CSDN博客》中也提到了构造器。 二、构造器…

文件二维码怎么快速生成?在线文件生码的使用技巧

文件现在经常会做成二维码的方式来展示内容&#xff0c;通过这种方式能够更加简单快捷的将文件分享给其他人查看或者下载&#xff0c;而且文件生成活码可以长期使用&#xff0c;随时替换当前二维码中的内容&#xff0c;那么可以长期使用的文件二维码该如何制作呢&#xff1f; …

Android开发Activity生命周期详解

本文详解Android开发Activity生命周期。 目录 一、Activity 二、Activity生命周期 三、生命周期特性 四、常见情况生命周期的执行顺序 一、Activity Activity是用户交互的第一接口&#xff0c;它提供了一个用户完成指令的窗口。当开发者创建Activity之后&#xff0c;通过…

Springboot 集成 Shardingsphere-JDBC

Springboot 集成 Shardingsphere-JDBC Shardingsphere系列目录&#xff1a;背景前提新增依赖分表策略简单分库分表策略垂直分库广播表水平分库(单表)水平分库(多表)水平分表 HINT配置逻辑代码 自定义分库分表&#xff08;精准定位范围查询&#xff09;配置代码精准定位数据库精…