老邬 發表於 2022-3-15 10:01:00

详解 Go 中的 rune 类型

<p><span id="profileBt" class="rich_media_meta rich_media_meta_nickname">Go语言中文网&nbsp;<span id="publish_time" class="rich_media_meta rich_media_meta_text">2022-03-15 08:52</span></span></p>
<p>&nbsp;</p>
<p>刚接触 Go 语言时,就听说有一个叫&nbsp;<code>rune</code>&nbsp;的数据类型,即使查阅过一些资料,对它的理解依旧比较模糊,加之对陌生事物的天然排斥,在之后很长一段时间的编程工作中,我都没有让它出现在我的代码里。</p>
<p>逃避虽然有用,但是似乎有些可耻,想要成为一名成熟、优秀的 Go 语言开发工程师,必须要有直面陌生事物并且成功运用的勇气和能力,带着这样的觉悟,让我们一起走近&nbsp;<code>rune</code>,直视它!</p>
<h2>了解一下,<code>rune</code>类型究竟是什么?</h2>
<p><code>rune</code>&nbsp;类型是 Go 语言的一种特殊数字类型。在&nbsp;<code>builtin/builtin.go</code>&nbsp;文件中,它的定义:<code>type rune = int32</code>;官方对它的解释是:<code>rune</code>&nbsp;是类型&nbsp;<code>int32</code>&nbsp;的别名,在所有方面都等价于它,用来区分字符值跟整数值。使用单引号定义 ,返回采用 UTF-8 编码的 Unicode 码点。Go 语言通过&nbsp;<code>rune</code>&nbsp;处理中文,支持国际化多语言。</p>
<p>众所周知,Go 语言有两种类型声明方式:一种叫类型定义声明,另一种叫类型别名声明。其中,别名的使用在大型项目重构中作用最为明显,它能解决代码升级或迁移过程中可能存在的类型兼容性问题。而<code>rune</code>&nbsp;跟&nbsp;<code>byte</code>&nbsp;是 Go 语言中仅有的两个类型别名,专门用来处理字符。当然,我们也可以通过&nbsp;<code>type</code>&nbsp;关键字加等号的方式声明更多的类型别名。</p>
<h2>学习一下,<code>rune</code>类型怎么用?</h2>
<p>我们知道,字符串由字符组成,字符的底层由字节组成,而一个字符串在底层的表示是一个字节序列。在 Go 语言中,字符可以被分成两种类型处理:对占 1 个字节的英文类字符,可以使用&nbsp;<code>byte</code>(或者&nbsp;<code>unit8</code>&nbsp;);对占 1 ~ 4 个字节的其他字符,可以使用&nbsp;<code>rune</code>(或者&nbsp;<code>int32</code>&nbsp;),如中文、特殊符号等。</p>
<p>下面,我们通过示例应用来具体感受一下。</p>
<ul class="list-paddingleft-1">
<li>统计带中文字符串长度</li>
</ul>
<pre><code>//&nbsp;使用内置函数&nbsp;len()&nbsp;统计字符串长度<br>fmt.Println(len("Go语言编程"))&nbsp;&nbsp;//&nbsp;输出:14 &nbsp;<br></code></pre>
<p>前面说到,字符串在底层的表示是一个字节序列。其中,英文字符占用 1 字节,中文字符占用 3 字节,所以得到的长度 14 显然是底层占用字节长度,而不是字符串长度,这时,便需要用到&nbsp;<code>rune</code>&nbsp;类型。</p>
<pre><code>//&nbsp;转换成&nbsp;rune&nbsp;数组后统计字符串长度<br>fmt.Println(len([]rune("Go语言编程")))&nbsp;&nbsp;//&nbsp;输出:6<br></code></pre>
<p>这回对了。很容易,我们解锁了&nbsp;<code>rune</code>&nbsp;类型的第一个功能,即统计字符串长度。</p>
<ul class="list-paddingleft-1">
<li>截取带中文字符串</li>
</ul>
<p>如果想要截取字符串中 ”Go语言“ 这一段,考虑到底层是一个字节序列,或者说是一个数组,通常情况下,我们会这样:</p>
<pre><code>s&nbsp;:=&nbsp;"Go语言编程"<br>//&nbsp;8=2*1+2*3<br>fmt.Println(s)&nbsp;&nbsp;//&nbsp;输出:Go语言<br></code></pre>
<p>结果符合预期。但是,按照字节的方式进行截取,必须预先计算出需要截取字符串的字节数,如果字节数计算错误,就会显示乱码,比如这样:</p>
<pre><code>s&nbsp;:=&nbsp;"Go语言编程"<br>fmt.Println(s)&nbsp;//&nbsp;输出:Go语�<br></code></pre>
<p>此外,如果截取的字符串较长,那通过字节的方式进行截取显然不是一个高效准确的办法。那有没有不用计算字节数,简单又不会出现乱码的方法呢?不妨试试这样:</p>
<pre><code>s&nbsp;:=&nbsp;"Go语言编程"<br>//&nbsp;转成&nbsp;rune&nbsp;数组,需要几个字符,取几个字符<br>fmt.Println(string([]rune(s)[:4]))&nbsp;//&nbsp;输出:Go语言&nbsp;&nbsp;&nbsp;&nbsp;<br></code></pre>
<p>到这里,我们解锁了&nbsp;<code>rune</code>&nbsp;类型的第二个功能,即截取字符串。</p>
<h2>思考一下,为什么&nbsp;<code>rune</code>&nbsp;类型可以做到?</h2>
<p>通过上面的示例,我们发现似乎在处理带中文的字符串时,都需要用到&nbsp;<code>rune</code>&nbsp;类型,这究竟是为什么呢?除了使用&nbsp;<code>rune</code>&nbsp;类型,还有其他方法吗?</p>
<p>在深入思考之前,我们需要首先弄清楚&nbsp;<code>string</code>&nbsp;、<code>byte</code>、<code>rune</code>&nbsp;三者间的关系。</p>
<p>字符串在底层的表示是由单个字节组成的一个不可修改的字节序列,字节使用&nbsp;UTF-8&nbsp;编码标识&nbsp;Unicode&nbsp;文本。Unicode 文本意味着&nbsp;<code>.go</code>&nbsp;文件内可以包含世界上的任意语言或字符,该文件在任意系统上打开都不会乱码。UTF-8 是 Unicode 的一种实现方式,是一种针对 Unicode 可变长度的字符编码,它定义了字符串具体以何种方式存储在内存中。UFT-8 使用 1 ~ 4 为每个字符编码。</p>
<p>Go 语言把字符分&nbsp;<code>byte</code>&nbsp;和&nbsp;<code>rune</code>&nbsp;两种类型处理。<code>byte</code>&nbsp;是类型&nbsp;<code>unit8</code>&nbsp;的别名,用于存放占 1 字节的 ASCII 字符,如英文字符,返回的是字符原始字节。<code>rune</code>&nbsp;是类型&nbsp;<code>int32</code>&nbsp;的别名,用于存放多字节字符,如占 3 字节的中文字符,返回的是字符 Unicode 码点值。如下图所示:</p>
<pre><code>s&nbsp;:=&nbsp;"Go语言编程"<br>//&nbsp;byte<br>fmt.Println([]byte(s))&nbsp;//&nbsp;输出:<br>//&nbsp;rune<br>fmt.Println([]rune(s))&nbsp;//&nbsp;输出:<br></code></pre>
<p>它们的对应关系如下图:<img alt="图片" class="rich_pages wxw-img lazyload" data-ratio="0.4216710182767624" data-src="https://mmbiz.qpic.cn/mmbiz_png/rJDC5vuwJCNoUJI5HH4uiaAYjK4vIgnu7W7h9Y0LezHISIzDTxDCE6FVVxzUb6gefUYo2X5mraBO4p9uiaK0QRuA/640?wx_fmt=png&amp;wxfrom=5&amp;wx_lazy=1&amp;wx_co=1" data-type="png" data-w="1532" data-fail="0">了解了这些,我们再回过来看看,刚才的问题是不是清楚明白很多?接下来,让我们再来看看源码中是如何处理的,以&nbsp;utf8.RuneCountInString()&nbsp;函数为例。&nbsp; &nbsp;</p>
<p>&nbsp;</p>
<p>示例:</p>
<pre><code>//&nbsp;统计字符串长度<br>fmt.Println(utf8.RuneCountInString("Go语言编程"))&nbsp;//&nbsp;输出:6<br></code></pre>
<p>源码:</p>
<pre><code>//&nbsp;RuneCountInString&nbsp;is&nbsp;like&nbsp;RuneCount&nbsp;but&nbsp;its&nbsp;input&nbsp;is&nbsp;a&nbsp;string.<br>func&nbsp;RuneCountInString(s&nbsp;string)&nbsp;(n&nbsp;int)&nbsp;{<br>&nbsp;//&nbsp;调用&nbsp;len()&nbsp;函数得到字节数<br>&nbsp;ns&nbsp;:=&nbsp;len(s)<br>&nbsp;for&nbsp;i&nbsp;:=&nbsp;0;&nbsp;i&nbsp;&lt;&nbsp;ns;&nbsp;n++&nbsp;{<br>&nbsp;&nbsp;c&nbsp;:=&nbsp;s<br>&nbsp;&nbsp;//&nbsp;如码点值小于&nbsp;128,则为占&nbsp;1&nbsp;字节的&nbsp;ASCII&nbsp;字符(或者说英文字符),长度&nbsp;+&nbsp;1<br>&nbsp;&nbsp;if&nbsp;c&nbsp;&lt;&nbsp;RuneSelf&nbsp;{&nbsp;//&nbsp;RuneSelf&nbsp;=&nbsp;128<br>&nbsp;&nbsp;&nbsp;//&nbsp;ASCII&nbsp;fast&nbsp;path<br>&nbsp;&nbsp;&nbsp;i++<br>&nbsp;&nbsp;&nbsp;continue<br>&nbsp;&nbsp;}<br>&nbsp;&nbsp;//&nbsp;查询首字节信息表,得到中文占&nbsp;3&nbsp;字节,所以这里的&nbsp;x&nbsp;=&nbsp;3<br>&nbsp;&nbsp;x&nbsp;:=&nbsp;first<br>&nbsp;&nbsp;//&nbsp;判断&nbsp;x&nbsp;=&nbsp;3,xx&nbsp;=&nbsp;241(0xF1)<br>&nbsp;&nbsp;if&nbsp;x&nbsp;==&nbsp;xx&nbsp;{<br>&nbsp;&nbsp;&nbsp;i++&nbsp;//&nbsp;invalid.<br>&nbsp;&nbsp;&nbsp;continue<br>&nbsp;&nbsp;}<br>&nbsp;&nbsp;//&nbsp;提取有效的&nbsp;UTF-8&nbsp;字节长度编码信息,size&nbsp;=&nbsp;3<br>&nbsp;&nbsp;size&nbsp;:=&nbsp;int(x&nbsp;&amp;&nbsp;7)<br>&nbsp;&nbsp;if&nbsp;i+size&nbsp;&gt;&nbsp;ns&nbsp;{<br>&nbsp;&nbsp;&nbsp;i++&nbsp;//&nbsp;Short&nbsp;or&nbsp;invalid.<br>&nbsp;&nbsp;&nbsp;continue<br>&nbsp;&nbsp;}<br>&nbsp;&nbsp;//&nbsp;提取有效字节范围<br>&nbsp;&nbsp;accept&nbsp;:=&nbsp;acceptRanges<br>&nbsp;&nbsp;//&nbsp;accept.lo,accept.hi,表示&nbsp;UTF-8&nbsp;中第二字节的有效范围<br>&nbsp;&nbsp;//&nbsp;locb&nbsp;=&nbsp;0b10000000,表示&nbsp;UTF-8&nbsp;编码非首字节的数值下限<br>&nbsp;&nbsp;//&nbsp;hicb&nbsp;=&nbsp;0b10111111,表示&nbsp;UTF-8&nbsp;编码非首字节的数值上限<br>&nbsp;&nbsp;if&nbsp;c&nbsp;:=&nbsp;s;&nbsp;c&nbsp;&lt;&nbsp;accept.lo&nbsp;||&nbsp;accept.hi&nbsp;&lt;&nbsp;c&nbsp;{<br>&nbsp;&nbsp;&nbsp;size&nbsp;=&nbsp;1<br>&nbsp;&nbsp;}&nbsp;else&nbsp;if&nbsp;size&nbsp;==&nbsp;2&nbsp;{<br>&nbsp;&nbsp;}&nbsp;else&nbsp;if&nbsp;c&nbsp;:=&nbsp;s;&nbsp;c&nbsp;&lt;&nbsp;locb&nbsp;||&nbsp;hicb&nbsp;&lt;&nbsp;c&nbsp;{<br>&nbsp;&nbsp;&nbsp;size&nbsp;=&nbsp;1<br>&nbsp;&nbsp;}&nbsp;else&nbsp;if&nbsp;size&nbsp;==&nbsp;3&nbsp;{<br>&nbsp;&nbsp;}&nbsp;else&nbsp;if&nbsp;c&nbsp;:=&nbsp;s;&nbsp;c&nbsp;&lt;&nbsp;locb&nbsp;||&nbsp;hicb&nbsp;&lt;&nbsp;c&nbsp;{<br>&nbsp;&nbsp;&nbsp;size&nbsp;=&nbsp;1<br>&nbsp;&nbsp;}<br>&nbsp;&nbsp;i&nbsp;+=&nbsp;size<br>&nbsp;}<br>&nbsp;return&nbsp;n<br>}<br></code></pre>
<p>调用该函数时,传入一个原始的字符串,代码会根据每个字符的码点大小判断是否为 ASCII 字符,如果是,则算做 1 位;如果不是,则查询首字节表,明确字符占用的字节数,验证有效性后再进行计数。</p>
<h2>小小总结</h2>
<p>在我看来,<code>rune</code>&nbsp;类型只是一种名称叫法,表示用来处理长度大于 1 字节( 8 位)、不超过 4 字节( 32 位)的字符类型。但万变不离其宗,我们使用函数时,无论传入参数的是原始字符串还是&nbsp;<code>rune</code>,最终都是对字节进行处理。看似陌生的事物,沉下心了解到其本质以后,才发现原来并不陌生,缺少的只是正视它的勇气!</p>
<p></p>
<p>UTF-8:<em>https://zh.wikipedia.org/wiki/UTF-8</em></p>
<p></p>
<p>Unicode:<em>https://zh.wikipedia.org/wiki/Unicode</em></p>
<p></p>
<p><code>utf8.RuneCountInString()</code>:<em>https://golang.org/src/unicode/utf8/utf8.go</em></p>
<p>&nbsp;</p>
<hr>
<p><strong>推荐阅读</strong></p>
<ul class="list-paddingleft-1">
<li>
<p>Rob Pike 带你重新认识字符串、字节、rune和字符</p>

</li>

</ul>
<p>&nbsp;</p>
<p>
<strong>福利</strong><br>我为大家整理了一份从入门到进阶的Go学习资料礼包,包含学习建议:入门看什么,进阶看什么。关注公众号 「polarisxu」,回复&nbsp;<strong>ebook</strong>&nbsp;获取;还可以回复「<strong>进群</strong>」,和数万 Gopher 交流学习</p><br><br>
来源:https://www.cnblogs.com/cheyunhua/p/16007219.html
頁: [1]
查看完整版本: 详解 Go 中的 rune 类型