// \xe4\xb8\xad utf8: e4b8ad, U+4E2D, 中
// \xe5\x9b\xbd utf8: e59bbd, U+56FD, 国
// \xe4\xba\xba utf8: e4baba, U+4EBA, 人
// \xe2\x8c\x98 utf8: e28c98, U+2318, ⌘
const sample = "\xe4\xb8\xad\xe5\x9b\xbd\xe4\xba\xba\xbd\x20\x3d\xe2\x8c\x98"
fmt.Printf("len: %d \n", len(sample))
fmt.Println(sample)
for i := 0; i < len(sample); i++ {
fmt.Printf("%U index: %d\n", sample[i], i)
}
for i, v := range sample {
fmt.Printf("%#U index: %d\n", v, i)
}
fmt.Println()
疑问
本文将围绕上述代码进行讨论,如果你能准确的答出上述代码结果,则可直接看文末总结了;看完本文我们可以解答如下知识点:
rune
类型是什么,和string
有什么关联关系?string
通过下标index
访问和for range
访问的区别?s[i]
是指向字符串s
的第n
个字符?
字符串是什么
在go语言中,字符串是一个只读的字节数组([]byte)
,字符串里面可以包含任意的字节,它不需要包含unicode、utf8或者其他预定义的格式;
如下是一个包含\x
转义字符的字符串字面常量,稍微作一点简单说明,前三个字节utf8
格式编码为为0xe4b8ad
, unicode码是U+4E2D
,中文字符是‘中’;其他字符可以将行首的代码实例运行起来观察;
const sample = "\xe4\xb8\xad\xe5\x9b\xbd\xe4\xba\xba\xbd\x20\x3d\xe2\x8c\x98"
字符串打印输出技巧
由于在上述字符串常量中包含无效ASCII
和无效的UTF-8
,因此直接打印字符串的话,会包含一些迷惑的输出:
fmt.Println(sample)
// 输出
// 中国人� =⌘
对于字符串而言,最常见的方式则为通过下标索引的方式打印,每个独立的索引处都是一个byte
,这里注意,输出的字节和字符串字面常量中定义的转义字节一一对应:
for i := 0; i < len(sample); i++ {
fmt.Printf("%x ", sample[i])
}
// 输出
// e4 b8 ad e5 9b bd e4 ba ba bd 20 3d e2 8c 98
同样,对于字符串和字节切片来说,可以通过%x
的方式便捷的打印字符串为十六进制的byte
序列,
fmt.Printf("%x\n", sample)
// 输出
// e4b8ade59bbde4bababd203de28c98
一个不错的技巧是使用该格式的“空格”标志,在 % 和 x 之间放置一个空格。 将此处使用的格式字符串与上面的格式字符串进行比较:
fmt.Printf("% x\n", sample)
// 输出
// e4 b8 ad e5 9b bd e4 ba ba bd 20 3d e2 8c 98
更多的,%q 的打印方式将字符串以unicode
方式输出,其他无效的uft8格式将进行转义字符串字节序列的方式打印:
fmt.Printf("%q\n", sample)
// 输出
// "中国人\xbd =⌘"
同样,在%
和q
之间放置 +
号,将字符串除了ASCII外的其他byte序列以utf8的方式输出,具体的结果为unicode字符集;结果中可以看到除了倒数第二三两个字符是
和=
,其他的字符都是以unicode字符集打印;
fmt.Printf("%+q\n", sample)
// 输出
// "\u4e2d\u56fd\u4eba\xbd =\u2318"
值得指出的是,所有这些方法对字节切片的行为与对字符串的行为完全相同;可以自行将sample
变量强转位[]byte
类型进行上述打印测试;
字符串字面量和utf8
的关系
从上面的验证来看,字符串通过下标索引的是byte,而不是对应的字符,这就证明了字符串本质是只读的字节数组;
如下是一个简单的程序,它以三种不同的方式打印带有单个字符的字符串常量,一种作为纯字符串,一种作为仅打印 ASCII 字符的字符串,一种作为十六进制的单个字节。 为了避免混淆,我们创建了一个“原始字符串”,用反引号括起来,因此它只能包含文字文本不包含转义字符。 (用双引号括起来的常规字符串可以包含如上所示的转义序列。)
func main() {
const placeOfInterest = `⌘`
fmt.Printf("plain string: ")
fmt.Printf("%s", placeOfInterest)
fmt.Printf("\n")
fmt.Printf("quoted string: ")
fmt.Printf("%+q", placeOfInterest)
fmt.Printf("\n")
fmt.Printf("hex bytes: ")
for i := 0; i < len(placeOfInterest); i++ {
fmt.Printf("%x ", placeOfInterest[i])
}
fmt.Printf("\n")
// 输出
// plain string: ⌘
// quoted string: "\u2318"
// hex bytes: e2 8c 98
}
这结果表明,Unicode 字符值 U+2318,即符号⌘,由字节 e2 8c 98 表示,并且这些字节是十六进制值 0x2318 的 UTF-8 编码。
简而言之,Go 源代码是以 UTF-8 编码的 Unicode 字符,因此字符串字面量的源代码是 UTF-8 文本;如果字符串字面量不包含转义的字符,则字符串一定是有效的utf8格式,
码点(unicode
字符集)、字符和rune
的关系
上面一直在讲述string的本质问题,还未描述byte和character的区别;像小写字母a
是一个字符,unicode为U+00E0,⌘
也是一个字符,unicode为U+2318;因此这里我理解字符即为unicode字符集种的字符;
Unicode 标准使用术语“代码点”来指代由单个值表示的项目。 码点 U+2318,十六进制值为 2318,代表符号⌘。代码点读起来有点令人费解,在go语言中,int32的别名rune替代代码点,因此go中int32类型即可表示uncode字符集的字符数值;'⌘'
为rune
类型,十六进制为0x2318
;
总而言之,以下是要点:
- Go 源代码始终是 UTF-8。
- 字符串包含任意字节。
- 没有包含转义字节的字符串字面量始终是有效的 UTF-8 序列。
- 这些序列代表 Unicode 代码点,称为字符。
- Go 不保证字符串中的字符被规范化,即拥有不符合
unicode、ASCII
的无效任意转义字符。
for loop
范围for
循环
for range 循环是唯一可以在go中以UTF-8 的方式遍历字符串的;
我们已经看到了下标索引 for 循环是遍历字节。 相比之下,for range 循环在每次迭代中解码一个 UTF-8 编码的符文。 每次循环,循环的索引是当前字符的起始位置,以字节为单位,代码点是它的unicode值。 如下,它显示了代码点的 Unicode 值及其打印表示:
const sample = "中国人"
for i, v := range sample {
fmt.Printf("%#U start index: %d\n", v, i)
}
// 输出
// U+4E2D '中' start index: 0
// U+56FD '国' start index: 3
// U+4EBA '人' start index: 6
还有种方法可以快速的访问字符串中的utf8字符,即将字符串转化为rune切片,如下代码所示:
const sample = "中国人"
var sampleSlice = []rune(sample)
for i, v := range sampleSlice {
fmt.Printf("%#U start index: %d\n", v, i)
}
// 输出
// U+4E2D '中' start index: 0
// U+56FD '国' start index: 1
// U+4EBA '人' start index: 2
utf8
库
go中unicode/utf8库提供了utf格式相关的操作,它包含用于验证、反汇编和重新组合 UTF-8 字符串的辅助例程。 如下是一个与上面的 for range 示例等效的程序,但使用该包中的 DecodeRuneInString 函数来完成这项工作。 该函数的返回值是符文及其 UTF-8 编码字节的宽度
const nihongo = "中国人"
for i, w := 0, 0; i < len(nihongo); i += w {
runeValue, width := utf8.DecodeRuneInString(nihongo[i:])
fmt.Printf("%#U start index: %d\n", runeValue, i)
w = width
}
// 输出
// U+4E2D '中' start index: 0
// U+56FD '国' start index: 3
// U+4EBA '人' start index: 6
总结
问题解答:
rune
类型是什么,和string
有什么关联关系?
rune本质是int32类型,代表string中的字符,即utf8形式的unicode数值
string
通过下标index
访问和for range
访问的区别?
下标访问取的是单个的字节,for range访问取的是单个 utf-8 字符
s[i]
是指向字符串s
的第n
个字符?
s[i]指向的是第n个字节,
参考链接:
- https://go.dev/blog/strings
- https://util.unicode.org/UnicodeJsps/character.jsp?a=%E4%B8%AD&B1=Show
- https://go.dev/ref/spec#Rune_literals
- http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html