群里分享有大佬分享了这样一道题, 如下, 问最后输出几?
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
举个例子, -1
的补码:
- ABS(-1) = 1, 二进制为:
0000 0001
(已int8类型为例) - 对第一步取反: ^
0000 0001
=1111 1110
- 对第二步加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编码的字节数.
rune
为int32
类型, 所以-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进阶培训#