【译】Go语言之父带你从新认识字符串、字节、rune和字符

如下文章翻译自罗伯·派克发表在Go Blog的文章,文章中为读者详述了Go语言中字符串与咱们常常提起的字节、字符还有rune的关系和相互之间的不一样。正如派克在文中所说php

字符串这个话题对于一篇博客文章来讲彷佛太简单了,可是要很好地使用它们,不只须要了解它们的工做原理,还须要了解字节,字符和 rune 的区别,以及 Unicode 和 UTF- 8,字符串和字符串直接量之间的区别,以及其余甚至更细微的区别。golang

原文地址:blog.golang.org/strings浏览器

文章篇幅仍是挺长的,你们时间都很宝贵因此我先把文章探究的问题的结论放在前面,有时间的同窗仍是建议整篇读一下。服务器

  • Go 源代码始终为 UTF-8。
  • 字符串能够包含任意字节。
  • 字符串文字中不包含字节级转义符时字符串始终包含有效的 UTF-8 序列。
  • 表明 Unicode 码点的字节序列称为 rune
  • 在 Go 中不会保证字符串中的字符被规范化。

原文的语法、句式都很好学习Go 语言的同时还能增强一下英文阅读推荐去读英文原文,有翻译不清楚的欢迎指正。jsp

介绍

上一篇博客文章使用许多示例说明了切片在其实现背后的机制,从而说明了切片在 Go 中的工做方式。以此为背景,本文会讨论 Go 中的字符串。一开始会让人以为,字符串这个话题对于一篇博客文章来讲彷佛太简单了,可是要很好地使用它们,不只须要了解它们的工做原理,还须要了解字节,字符和 rune 的区别,以及 Unicode 和 UTF- 8,字符串和字符串直接量之间的区别,以及其余甚至更细微的区别。编辑器

展开讨论这个话题的一种方法是将其视为对如下常见问题的解答:“当我索引 Go 字符串时,在 n 个位置为何没有获得第 n 个字符?” 如您所见,这个问题将咱们引向了许多文本在现实世界中是如何工做的细节中。函数

独立于 Go 语言以外,Joel Spolsky 的著名博客文章绝对绝对是每一个软件开发人员绝对绝对确定地了解 Unicode 和字符集 (无借口!) 很好地介绍了这些问题的细节。他提出的许多观点将在这里进行阐述。oop

什么是字符串?

让咱们从一些基础知识开始。学习

在 Go 中,字符串其实是只读的字节切片。若是你彻底不知道一个字节切片是什么以及它是如何工做的,请阅读上一篇博客文章 ; 咱们在这里假设你已经知道这些。ui

预先说明字符串能够包含任意字节很重要,字符串没有规定只能包含 Unicode 文本,UTF-8 文本或任何其余预约义格式。就字符串的内容而言,它彻底至关于一个字节切片。

下面一个字符串文字 (稍后将进一步介绍),该文字使用.NN 表示法定义了一个包含某些特殊字节值的字符串常量。 (固然,一个字节的范围是十六进制值 00 到 FF)。

const sample =“ .bd.b2.3d.bc.20.e2.8c.98复制代码

打印字符串

因为字符串常量 sample 中的某些字节不是有效的 ASCII,甚至不是有效的 UTF-8,所以直接打印字符串将产生诡异的输出。下面使用简单的打印语句打印 sample

fmt.Println(sample)
复制代码

输出这一堆乱码(输出会因运行环境不一样而有所不一样)

��=� ⌘
复制代码

要找出该字符串真正包含了什么,咱们须要将其分解并检查每一部分。有几种方法能够作到这一点。最明显的是遍历其内容并单独取出每一个字节,如如下 for 循环所示

for i := 0; i < len(sample); i++ {
    fmt.Printf("%x ", sample[i])
}
复制代码

如前所述,索引字符串访问的是单个字节,而不是字符。咱们将在下面详细讨论该主题。如今,让咱们关注点保持在字节上。下面是逐字节循环的输出:

bd b2 3d bc 20 e2 8c 98
复制代码

注意各个字节与定义字符串的十六进制转义符匹配是如此地匹配。

为混乱的字符串生成可显示的输出的一种较短方法是使用 fmt.Printf%x(十六进制) 格式标记符(或者叫格式动词)。它只是将字符串的字节按顺序转换为十六进制数字,每一个字节两个。

fmt.Printf("%x.", sample)
复制代码

将其输出与上面的输出进行比较:

bdb23dbc20e28c98
复制代码

一个不错的技巧是在格式标记符中使用 “空格” 标志,在x 之间放置一个空格。而后将此处使用的格式字符串与上面的格式字符串进行比较,

fmt.Printf("% x.", sample)
复制代码

注意字节之间留有的空格,从而使结果不那么难以理解:

bd b2 3d bc 20 e2 8c 98
复制代码

还有一件事。 %q(带引号) 动词将转义字符串中全部不可打印的字节序列,会让输出无歧义。

fmt.Printf("%q.", sample)
复制代码

当字符串的大部分为可理解文本,但有一些特殊的含义能够根除时,这个技巧很方便。它会输出:

".bd.b2=.bc ⌘"
复制代码

若是斜视一下,咱们能够看到噪声点中隐藏的是一个 ASCII 等号以及一个规则的空格,最后出现了著名的瑞典 “景点” 符号。该符号的 Unicode 值为 U + 2318,由空格后的字节编码为 UTF-8 (十六进制值 20):e2 8c 98

若是咱们不熟悉字符串或对字符串中奇奇怪怪的值感到困惑,能够在%q 动词上使用 “加号” 标志。此标志使输出在解释 UTF-8 时不只转义不可打印的序列,并且还会转义全部非 ASCII 字节。结果是它输出了格式正确的 UTF-8 的 Unicode 值,该值表示字符串中的非 ASCII 数据:

fmt.Printf("%+q.", sample)
复制代码

使用这种格式时,瑞典符号的 Unicode 值显示为. 转义符:

".bd.b2=.bc .2318"
复制代码

在调试字符串的内容时,这些打印技巧会颇有用,而且在下面的讨论中使用也会很方便。值得指出的是,全部这些方法对于字节切片的行为与对字符串的行为彻底相同。

下面是咱们已列出的全部打印选项的全集,以完整的程序形式呈现出来,您能够在浏览器中直接运行 (和编辑):

译注:指的是在 go playground 的浏览器运行环境中。

package main

import "fmt"

func main() {
    const sample = ".bd.b2.3d.bc.20.e2.8c.98"

    fmt.Println("Println:")
    fmt.Println(sample)

    fmt.Println("Byte loop:")
    for i := 0; i < len(sample); i++ {
        fmt.Printf("%x ", sample[i])
    }
    fmt.Printf(".")

    fmt.Println("Printf with %x:")
    fmt.Printf("%x.", sample)

    fmt.Println("Printf with % x:")
    fmt.Printf("% x.", sample)

    fmt.Println("Printf with %q:")
    fmt.Printf("%q.", sample)

    fmt.Println("Printf with %+q:")
    fmt.Printf("%+q.", sample)
}
复制代码

[练习:修改上面的示例,以使用一个字节切片代替字符串。提示:使用转换来建立切片。]

[练习:循环遍历字符串在每一个字节上使用%q 格式化标记符。看看输出告诉您什么?]

UTF-8和字符串直接量

如咱们所见,索引字符串会产生其字节,而不是其字符:字符串只是一堆字节。这意味着,当咱们将字符存储在字符串中时,将存储其字节表示。让咱们经过一个更容易控制的示例,看看这个过程是如何发生。

下面是一个简单的程序,使用了三种不一样的方式打印一个只有一个字符的字符串常量。一次做为普通字符串,一次是用引号括起来的纯 ASCII 字符串,一次是十六进制的单个字节。为避免混淆,咱们建立了一个 “原始字符串”,并用反引号将其括起来,所以它只能包含文字文本。 (在上面的例子中咱们已经见过,用双引号括起来的常规字符串能够包含转义序列。)

func main() {
    const placeOfInterest = `⌘`

    fmt.Printf("plain string: ")
    fmt.Printf("%s", placeOfInterest)
    fmt.Printf(".")

    fmt.Printf("quoted string: ")
    fmt.Printf("%+q", placeOfInterest)
    fmt.Printf(".")

    fmt.Printf("hex bytes: ")
    for i := 0; i < len(placeOfInterest); i++ {
        fmt.Printf("%x ", placeOfInterest[i])
    }
    fmt.Printf(".")
}
复制代码

输出为:

plain string: ⌘
quoted string: ".2318"
hex bytes: e2 8c 98
复制代码

这使咱们想起 Unicode 字符值 U + 2318,即,由字节 e2 8c 98 表示,而且这些字节是十六进制值 2318 的 UTF-8 编码。

根据你对 UTF-8 的熟悉程度,上面的结果对你来讲可能很明显,也可能很微妙,可是这值得花一点时间来解释字符串的 UTF-8 表示形式是如何被建立。一个简单的事实是:它是在编写源代码时建立的。

Go 中的源代码被定义为 UTF-8 文本;其余字符串表示形式是不被循序的。这意味着当咱们在源代码中编写文本时

`⌘`
复制代码

用于建立程序的文本编辑器将符号⌘的 UTF-8 编码放入源文本中。当咱们打印出十六进制字节时,咱们只是在输出了编辑器放置在源码文件中的数据。

简而言之,Go 源代码为 UTF-8 编码格式的,源代码中的字符串直接量是 UTF-8 文本。若是字符串直接量不包含转移字符序列,就像原始字符串同样,则构造的字符串将精确地保留引号之间的源文本。所以,根据定义和构造,原始字符串将始终包含其内容的有效 UTF-8 表示形式。一样,除非它包含上一节中提到的转义符,不然常规字符串文字也将始终包含有效的 UTF-8 文本。

有人认为 Go 字符串始终是 UTF-8 编码格式的,但不是:只有字符串直接量才始终是 UTF-8 的。如上一节所示,字符串能够包含任意字节;就像咱们在本文中所展现的那样,字符串 literal 只要不包含字节级转义符,就始终包含 UTF-8 文本。

总而言之,字符串能够包含任意字节,可是从字符串直接量构造字符串时,这些字节 (几乎老是) 是 UTF-8 的。

码点,字符和 rune

到目前为止,咱们在使用 “字节” 和 “字符” 这两个词时都很是当心。部分缘由是字符串包含字节,部分缘由是 “字符” 的概念很难定义。 Unicode 标准使用术语 “码点” 来指代由单个 Unicode 值表示的个体。具备十六进制值 2318 的码点 U + 2318 表示符号⌘。 (有关该码点的更多信息,请参见其 Unicode 页面。)

译者注:⌘是一个 Unicode 码点,其 Unicode 值是 U2318

举一个比较平淡的例子,Unicode 代码点 U + 0061 是小写拉丁字母 'A':

可是小写的带有重音符号的字母 'A' 怎么办?这是一个字符,它也是一个代码点 (U + 00E0),可是它还有其余表示形式。例如,咱们可使用 “组合” 重音符号代码点 U + 0300,并将其附加到小写字母 a,U + 0061,以建立相同的字符 à。一般,字符能够由许多不一样的代码点序列表示,所以也能够由 UTF-8 字节的不一样序列表示。

所以,计算中的字符概念是模棱两可的,或者至少是使人困惑的,所以咱们谨慎使用它。为了使事情变得可靠,有标准化技术保证给定字符始终由相同的代码点表示,但该主题目前离咱们这篇博客的主题太远了。稍后的博客文章将解释 Go 库如何解决规范化。

“码点” 有点冗长,所以 Go 为该概念引入了一个较短的术语:rune。该术语出如今库和源代码中,其含义与 “码点” 彻底相同。

Go 语言将单词 rune 定义为类型 int32 的别名,所以当整数值表示码点时,程序会很清晰。此外,你可能会认为是字符常量的常量在 Go 中称为 rune 常量。下面表达式的类型和值

'⌘'
复制代码

rune,它的整数值为 0x2318

总结一下,这是要点:

  • Go 源代码始终为 UTF-8。
  • 字符串能够包含任意字节。
  • 字符串文字中不包含字节级转义符时字符串始终包含有效的 UTF-8 序列。
  • 表明 Unicode 码点的字节序列称为 rune
  • 在 Go 中不会保证字符串中的字符被规范化。

Range 循环

除了关于 Go 源代码为 UTF-8 的细节外,Go 确实有且只有一种特别对待 UTF-8 的方式,那就是在字符串上使用 for range 循环时。

咱们已经看到了常规 for 循环会发生什么。相比之下, range 循环在每次迭代中会解码一个 UTF-8 编码 rune。每次循环时,循环的索引都是当前 rune 的起始位置 (以字节为单位),码点是其值。这是使用另外一个方便的 Printf 格式化占位符%#U 格式化字符串的示例,该格式话输出显示了码点的 Unicode 值及其打印表示形式:

const nihongo = "日本語"
for index, runeValue := range nihongo {
    fmt.Printf("%#U starts at byte position %d.", runeValue, index)
}
复制代码

输出显示每一个码点会占用多个字节:

U+65E5 '日' starts at byte position 0
U+672C '本' starts at byte position 3
U+8A9E '語' starts at byte position 6
复制代码

[练习:将无效的 UTF-8 字节序列放入字符串中。 循环的迭代会发生什么?]

Go 的标准库为解释 UTF-8 文本提供了强大的支持。若是用于 `` range循环的 不足以知足您的目的,则库中的软件包可能会提供您须要的功能。

最重要的软件包是 unicode / utf8,其中包含用于验证,插解和从新组装 UTF-8 字符串的帮助程序。这是一个至关于上面 range 示例的程序,可是使用该包中的 DecodeRuneInString 函数进行工做。该函数的返回值是 rune 及其宽度 (以 UTF-8 编码的字节)。

const nihongo = "日本語"
for i, w := 0, 0; i < len(nihongo); i += w {
    runeValue, width := utf8.DecodeRuneInString(nihongo[i:])
    fmt.Printf("%#U starts at byte position %d.", runeValue, i)
    w = width
}
复制代码

运行它以查看其执行相同的操做。range 循环和普通循环中使用 DecodeRuneInString 会产生彻底相同的迭代序列。

请查看文档中的 unicode / utf8 软件包,以了解它提供了哪些其余功能。

结论

如今回答开始时提出的问题:字符串是由字节构建的,所以对它们进行索引将生成字节,而不是字符。字符串甚至可能不包含字符。实际上,“字符” 的定义是模棱两可的,试图经过定义字符串是由字符组成这种说法来解决歧义是错误的。

关于 Unicode,UTF-8 和多语言文本处理还有不少话要说,可是它能够等待下一篇文章。如今,咱们但愿你对 Go 字符串的行为有更好的了解,尽管它们可能包含任意字节,但 UTF-8 是其设计的核心部分。

关注下方公众号第一时间获取推送,近期文章推荐:

深刻学习用Go编写HTTP服务器

五分钟用Docker快速搭建Go开发环境

十分钟学会用Go编写Web中间件

相关文章
相关标签/搜索