本文翻译自golang官方 ,英文文章原地址 https://blog.golang.org/strings ,主要介绍了 go中的 strings 、bytes、 runes 、characters。
Author: 岳东卫
Email: usher.yue@gmail.com
介绍
之前的文章介绍了go中的切片是如何工作的,我们使用了大量的例子来解释其背后实现的原理和机制. 在这个背景下, 我们在这篇文章讨论go中的字符串.首先 ,字符串对于一个博客文章的主题来说似乎比较简单, 但是为了更好的使用它们不仅需要理解它们是如何工作的, 还要知道他和字节、字符、rune之间的区别,UTF-8编码和Unicode编码之间的区别, 一个字符串和一个字符串字面量的区别, 以及更多细微的区别。
解决问题的一个方法就是将这个问题当成常见问题的答案: "当我用下标索引字符串的时候,为什么不能获取到对应的字符?" 正如你所看到的, 这个问题引导我们去了解更多细节有关于当今世界上的文字在go语言中是如何工作的。
Joel Spolsky的博客 ,有一些关于这些问题的很好的介绍,这些介绍独立于go语言, The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!). 他提出的很多观点我们可以在这里回应.
什么是字符串?
我们从一些基础开始。
在go中,字符串实际上是一个只读的片段,如果你完全不了解什么是一个字节切片,或者他的工作原理, 请阅读 切片 这篇文章; 我们假设你理解这些.
理解一个字符串包含任意字节是非常重要的.不需要保存unicode文本, UTF-8编码的文本, 或则其他任何预定义格式的文本. 就字符串的内容而言,他完全等同一个字节切片。
这里有一个字符串, 它使用 \xNN
符号 去定义一个包含特殊字节值的字符串常量。 (当然, 字节范围从十六进制的0x00到0xFF.)
const sample = "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98"
打印字符串
由于我们例子字符串中的一些字节不是有效的ASCII字符, 甚至没有有效的UTF-8码, 直接打印将会产生一些丑陋的输出. 简单的打印声明
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.Println的%X(十六进制)格式化动词可以将混乱的字符串生成可呈现的输出。 fmt.Printf
. 他只是将字符串的顺序字节转储为十六进制, 每个字节由两个组成.
fmt.Printf("%x\n", sample)
将其输出与上面输出进行比较:
bdb23dbc20e28c98
一个好的技巧是在格式化字符转中使用空格标志,放置一个空格在% 和 X之间. 对比此处的格式化字符串和上面所使用的,
fmt.Printf("% x\n", sample)
并且注意输出字节的时候在他们之间是怎样伴随空格输出的。
bd b2 3d bc 20 e2 8c 98
还有更多 ,%q(引用)动词将转义字符串中任何不可打印的字节序列,因此输出是明确的。
fmt.Printf("%q\n", sample)
当大多数的字符串可以理解为文本时,这种技术是很方便的,但有特殊性要根除;它产生:
"\xbd\xb2=\xbc ⌘"
如果我们斜眯着眼睛看这个字符串,我们可以看到,隐藏在乱码中的是一个ASCII等于符号,以及一个常规的空格,最后出现了着名的瑞典“兴趣点”的标志。 该符号具有Unicode值U + 2318,空格(十六进制值20)之后的UTF-8字节编码为:e2 8c 98。
如果我们对字符串中的奇怪值不熟悉或混淆, 我们可以给%q动词使用+标志. 该标志导致输出不仅可以转义不可打印的字符, 还可以转义任何非ASCII字节, 并且同时解释UTF-8. 结果是它暴露了在字符串中表示非ASCII数据的正确格式的UTF-8的Unicode值:
fmt.Printf("%+q\n", sample)
使用这个格式,瑞典符号的unicode的值显示为\u 转义:
"\xbd\xb2=\xbc \u2318"
当调试字符串的内容的时候这些打印技术很容易被使用,而且在后续的讨论中会非常方便. 值得指出的是所有这些方法对于字节切片和字符串的行为完全一致.
下面是我们列出的全部打印选项, 作为可以在浏览器中运行 (和编辑)的完整程序呈现。
package main import "fmt" func main() { const sample = "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98" fmt.Println("Println:") fmt.Println(sample) fmt.Println("Byte loop:") for i := 0; i < len(sample); i++ { fmt.Printf("%x ", sample[i]) } fmt.Printf("\n") fmt.Println("Printf with %x:") fmt.Printf("%x\n", sample) fmt.Println("Printf with % x:") fmt.Printf("% x\n", sample) fmt.Println("Printf with %q:") fmt.Printf("%q\n", sample) fmt.Println("Printf with %+q:") fmt.Printf("%+q\n", sample) }
Run
[练习: 修改上面的示例使用字节切片而不是一个字符串。 提示:可以通过类型转换来创建切片.]
[练习: 通过%q格式遍历每个字符串. 输出结果告诉你了什么?]
UTF-8和字符串文字
正如我们看到的 ,索引一个字符串索引到的是他的字节而不是字符,:一个字符串只是一堆字节. 这意味着我们在字符串中存储一个字符的时候, 我们是存储的他的字节表示. 让我们来看看更多地例子,看一下为什么会发生这样的事情。
这是一个简单的程序他通过三种不同的方式打印字符串, 一次是纯字符串, 一次是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") }
Run
输出:
plain string: ⌘ quoted string: "\u2318" hex bytes: e2 8c 98
这提醒我们,Unicode字符值U + 2318(“兴趣点”)⌘表示为字节e2 8c 98,这些字节是十六进制值2318的UTF-8编码。.
根据您对UTF-8的熟悉程度,这可能是显而易见的,或者可能是微妙的,请花费一点时间看一下如何创建字符串的UTF-8表示形式。 最简单的事实就是:它是在源代码写入时创建的。
go的源代码文件定义为UTF-8格式; 其他任何格式都不允许. 这意味在源代码中,我们编写we
`⌘`
用于创建程序的文本编辑器将符号⌘的UTF-8编码放入源文本. 当我们打印出十六进制字节时,我们只是将编辑器中的数据转储到文件中。
简单说,go语言的源代码是UTF-8所以go源代码中的字符串也是UTF-8. 如果该字符串文字不包含原始字符串不能的转义序列,则构造的字符串将准确地保留引号之间的源文本. 因此,通过定义和构造,原始字符串将始终包含其内容的有效的UTF-8表示. 类似地,除非它包含像上一节那样的UTF-8终止转义,否则常规字符串文字也将始终包含有效的UTF-8。.
一些人认为go的字符串一直是UTF-8类型,但是并不是这样,它仅仅是字符串字面量是这样. 就像我们在前面小节展示的一样,字符串可以包含任意字节; 正如我们在这里所示,字符串文字总是包含UTF-8文本,只要它们没有字节级转义。
总而言之,字符串可以包含任意字节,但是当从字符串字面量构造字符串时,这些字节(几乎总是)为UTF-8格式。.
Code points, characters, and runes
到目前为止我们使用字节和字符一直非常谨慎 .一部分是因为字符串保存的是字节, 另一部分就是字符的含义很难定义. Unicode标准使用术语“代码点”来表示由单个值表示的项.代码点U + 2318(十六进制值2318)表示符号⌘。 (有关该代码点的更多信息,请参阅其Unicode页面。)
选择一个更简单的例子,Unicode代码点U+0061表示的是小写拉丁字母“A”: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序列。
- T这些序列表示Unicode代码点,称为runes。
- 在go中并不保证字符串四正常的.
Range loops
除了Go源代码是UTF-8的公开细节外,Go只有一种方法可以特别处理UTF-8,也就是在字符串上使用range循环。
我们已经看到常规for循环会发生什么. range 循环每次循环的时候解码UTF8编码的Rune. 每次循环的时候, 循环的索引是当前rune的起始字节位置, 以字节为单位, 并且代码点就是他的值。 这里的shili使用了另一个Printf格式
, %#U
, 其中显示了代码点的Unicode值及其打印值
const nihongo = "日本語" for index, runeValue := range nihongo { fmt.Printf("%#U starts at byte position %d\n", runeValue, index) }
Run
输出显示每个代码点如何占用多个字节:
U+65E5 '日' starts at byte position 0 U+672C '本' starts at byte position 3 U+8A9E '語' starts at byte position 6
[练习:将无效的UTF-8字节序列放入字符串。循环的迭代会发生什么?
Libraries
go语言标准库提供了对utf-8的强大的支持. 如果for循环不能满足您的需求,您可以使用选择golang库中相关的包。.
最重要的这个包是unicode / utf8,它包含帮助程序来验证,反汇编和重新组合UTF-8字符串。 这是一个等同于上面范围范例的程序,但是使用该包中的DecodeRuneInString函数来完成工作。 函数的返回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\n", runeValue, i) w = width }
Run
运行它将看到相同的执行结果. 定义 for循环和DecodeRuneInString
产生相同的迭代序列。
看看unicode / utf8包的文档,看看它提供了什么其他的功能。
结论
要回答起始提出的问题:字符串是从字节构建的,因此索引它们产生字节,而不是字符。 一个字符串可能不会保存字符。实际上字符的定义是模糊的,通过字符来定义字符串,尝试解决歧义是不正确的。
还有更多关于UTF-8、多语言文本处理, 可以等到另一篇文章讨论. 现在,我们希望您更好地了解Go字符串的行为,尽管它们可能包含任意字节,但UTF-8是其设计的核心部分。.
By Rob Pike