Strings, bytes, runes and characters in Go

本文翻譯自golang官方 ,英文文章原地址 https://blog.golang.org/strings    ,主要介紹了 go中的 strings 、bytes、 runes 、characters。

Author: 嶽東衛

Email:

介紹

之前的文章介紹了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)
}

[練習: 修改上面的示例使用字節切片而不是一個字符串。 提示:可以通過類型轉換來創建切片.]

[練習: 通過%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")
}

輸出:

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)
    }

輸出顯示每個代碼點如何佔用多個字節:

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
    }

運行它將看到相同的執行結果. 定義 for循環和DecodeRuneInString 產生相同的迭代序列。

看看unicode / utf8包的文檔,看看它提供了什麼其他的功能。

結論

要回答起始提出的問題:字符串是從字節構建的,因此索引它們產生字節,而不是字符。 一個字符串可能不會保存字符。實際上字符的定義是模糊的,通過字符來定義字符串,嘗試解決歧義是不正確的。

還有更多關於UTF-8、多語言文本處理, 可以等到另一篇文章討論. 現在,我們希望您更好地瞭解Go字符串的行爲,儘管它們可能包含任意字節,但UTF-8是其設計的核心部分。.

By Rob Pike




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章