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