Morse's Site
1428 字
7 分钟
Golang习题总结系列 | 1. `len(string(rune(-1)))`是多少?
2021-06-17

群里分享有大佬分享了这样一道题, 如下, 问最后输出几?

package main import ( "fmt" ) func main() { var a rune = -1 fmt.Println(len(string(a))) }

正确的答案是: 3. 为啥呢?

习题相关知识#

这道题需要两个知识点: UTF-8编码与补码

  • 什么是UTF-8, Unicode编码?

计算机存储的数据最终都是0/1. 那么当我们打开一个文本文件后, 如果不知道这个文件的对应的编码格式, 那么我们无法对这段0/1的数据做反编码, 也就无法阅读里面的内容了. 而UTF-8与Unicode就是所谓文本编码格式.

Unicode是一种编码规定, 只规定了每个字符的唯一编码是什么, 如汉字”我”, 对应Unicode编码为\u6211. 但是没有规定在计算机中如何存储.

UTF-8是Unicode编码方案的一种实现方式(其他实现方式, 如: UTF-16, UTF-32). 它俩的关系就像JVM的规范与JVM的实现(JVM的实现有: Sun JVM, Oracle JRockit, Open JDK等).

UTF-8编码是一种变长(长度会有变化)的编码方式, 最少占用1个字节, 最多占用4个字节.

那么一个Unicode编码如何转为UTF-8编码呢? 下面一张表是它们的对应关系.

对应关系

举个例子, 还是已”我”这个汉字举例. “我”的Unicode编码为\u6211, 即: 6211(不要看’\u’, 它只是在说这里是一个unicode码)

因为 6211 => 07FF && 6211 <= FFFF, 所以匹配上面的第三条. 所以需要三个字节.

长度确定了, 如何转换呢?

6211 是16进制, 对应的二进制为: 0110 0010 0001 0001, 因占用三个字节, 那么就按对应关系的第三条1110xxxx 10xxxxxx 10xxxxxx按顺序填充里面的x. 即: 11100110 10001000 10010001

  • 补码

补码是计算机对负数进行编码的一种方式, 目前绝大多数操作系统都是用补码的方式来表示一个负数.

那么一个十进制的数字, 怎么推断它的补码呢?

首先: 正数的补码就是它本身的二进制. 只有负数才需要推算补码.

计算补码非常简单, 一共三步:

  1. 先对负数取绝对值, 然后转二进制.
  2. 对第一步的二进制结果取反.
  3. 对第二步的结果加1

举个例子, -1的补码:

  1. ABS(-1) = 1, 二进制为: 0000 0001 (已int8类型为例)
  2. 对第一步取反: ^0000 0001 = 1111 1110
  3. 对第二步加1: 1111 1110 + 1 = 1111 1111

所以对int8类型而言, -1的二进制数为: 1111 1111

解题#

回到开头的题目

package main import ( "fmt" ) func main() { var a rune = -1 fmt.Println(len(string(a))) }

在Golang中String是用UTF-8编码, string内部的slice长度对应UTF-8编码的字节数.

runeint32类型, 所以-1的二进制为1111 1111 1111 1111 1111 1111 1111 1111, 十六进制为0xffffffff, 没有落到上面任何一条规则. 所以我们只能通过源码分析了.

首先, 反编译我们的题目代码, 看看string(a)到底做了什么?

编译

重点如截图: main.go第九行中调用了runtime.intstring()方法.

接下来我们看下这个方法做了什么?

// v就是我们的问题中的-1 func intstring(buf *[4]byte, v int64) (s string) { ... // 重点看这里就可以了, 其余代码与v没有什么关系 n := encoderune(b, rune(v)) return s[:n] } // r 就是我们的-1 func encoderune(p []byte, r rune) int { // 这里用到了我们上面说的补码的知识, 所以转uint32后等于 1 << 32 - 1 = 4294967295 // Negative values are erroneous. Making it unsigned addresses the problem. switch i := uint32(r); { case i <= rune1Max: // rune1Max = 1<<7 - 1 即: 127 p[0] = byte(r) return 1 case i <= rune2Max: // rune2Max = 1<<11 - 1 即: 2047 _ = p[1] // eliminate bounds checks p[0] = t2 | byte(r>>6) p[1] = tx | byte(r)&maskx return 2 case i > maxRune, surrogateMin <= i && i <= surrogateMax: // maxRune = '\U0010FFFF' 即: 1114111 // surrogateMin = 0xD800 即: 56064 // surrogateMax = 0xDFFF 即: 57343 r = runeError fallthrough case i <= rune3Max: // rune3Max = 1<<16 - 1 即: 65535 _ = p[2] // eliminate bounds checks p[0] = t3 | byte(r>>12) p[1] = tx | byte(r>>6)&maskx p[2] = tx | byte(r)&maskx return 3 default: _ = p[3] // eliminate bounds checks p[0] = t4 | byte(r>>18) p[1] = tx | byte(r>>12)&maskx p[2] = tx | byte(r>>6)&maskx p[3] = tx | byte(r)&maskx return 4 } }

因为 uint32(-1) = 4294967295, 满足分支: case i > maxRune, surrogateMin <= i && i <= surrogateMax:, 然后fallthrough 进入分支: case i <= rune3Max: 最后返回 3.

而调用函数encoderune的地方:

n := encoderune(b, rune(v)) // 这里返回3 return s[:n] // s[0:3] 所以stirng的长度是3

所以是答案最终是三个字节.

总结#

学习了补码, UTF-8的知识, 最终我们还是要看Golang的源码才能最终分析出来答案, 其实最初的没有看代码, 脑里想到是4. 最终运行与结果不一致我才翻代码了解了原因.

那么这道题只问了长度, 那么转换后的字符串打印是什么呢, 即fmt.Println(string(a))输出什么呢?

答案是: 无意义字符. 即乱码.

那么正确的数字转字符串应该怎么做呢?

s := strconv.Itoa(-1)

Go提供的SDK的Itoa()核心思想就是获取入参数字中的每一位, 转为byte后, 在添加到byte[]中, 最后换为字符串.

不过获取每一位的数字并将数字转为byte的过程分了好多种情况, 比如有的通过查表实现的, 比如当入参数字如果是2的幂, 又会选别的算法. 总之就是根据入参数字的不同, 通过不同的算法优化转换效率.

#01 Blog/2 Golang进阶培训#

Golang习题总结系列 | 1. `len(string(rune(-1)))`是多少?
https://fuwari.vercel.app/posts/golang/len-string-rune/
作者
Morse Hsiao
发布于
2021-06-17
许可协议
CC BY-NC-SA 4.0