Go語言之父帶你從新認識字符串、字節、rune和字符

如下文章翻譯自羅伯·派克發表在Go Blog的文章,文章中爲讀者詳述了Go語言中字符串與咱們常常提起的字節、字符還有rune的關係和相互之間的不一樣。正如派克在文中所說php

字符串這個話題對於一篇博客文章來講彷佛太簡單了,可是要很好地使用它們,不只須要瞭解它們的工做原理,還須要瞭解字節,字符和 rune 的區別,以及 Unicode 和 UTF- 8,字符串和字符串直接量之間的區別,以及其餘甚至更細微的區別。

原文地址:https://blog.golang.org/stringsgolang

文章篇幅仍是挺長的,你們時間都很寶貴因此我先把文章探究的問題的結論放在前面,有時間的同窗仍是建議整篇讀一下。瀏覽器

  • Go 源代碼始終爲 UTF-8。
  • 字符串能夠包含任意字節。
  • 字符串文字中不包含字節級轉義符時字符串始終包含有效的 UTF-8 序列。
  • 表明 Unicode 碼點的字節序列稱爲 rune
  • 在 Go 中不會保證字符串中的字符被規範化。

原文的語法、句式都很好學習Go 語言的同時還能增強一下英文閱讀推薦去讀英文原文,有翻譯不清楚的歡迎指正。服務器

介紹

上一篇博客文章使用許多示例說明了切片在其實現背後的機制,從而說明了切片在 Go 中的工做方式。以此爲背景,本文會討論 Go 中的字符串。一開始會讓人以爲,字符串這個話題對於一篇博客文章來講彷佛太簡單了,可是要很好地使用它們,不只須要瞭解它們的工做原理,還須要瞭解字節,字符和 rune 的區別,以及 Unicode 和 UTF- 8,字符串和字符串直接量之間的區別,以及其餘甚至更細微的區別。jsp

展開討論這個話題的一種方法是將其視爲對如下常見問題的解答:「當我索引 Go 字符串時,在 n 個位置爲何沒有獲得第 n 個字符?」 如您所見,這個問題將咱們引向了許多文本在現實世界中是如何工做的細節中。編輯器

獨立於 Go 語言以外,Joel Spolsky 的著名博客文章絕對絕對是每一個軟件開發人員絕對絕對確定地瞭解 Unicode 和字符集 (無藉口!) 很好地介紹了這些問題的細節。他提出的許多觀點將在這裏進行闡述。函數

什麼是字符串?

讓咱們從一些基礎知識開始。oop

在 Go 中,字符串其實是隻讀的字節切片。若是你徹底不知道一個字節切片是什麼以及它是如何工做的,請閱讀上一篇博客文章 ; 咱們在這裏假設你已經知道這些。學習

預先說明字符串能夠包含任意字節很重要,字符串沒有規定只能包含 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中間件

tWbHIMFsM3.png

相關文章
相關標籤/搜索