本文翻譯自golang官方 ,英文文章原地址 https://blog.golang.org/strings ,主要介紹了 go中的 strings 、bytes、 runes 、characters。
Author: 嶽東衛
Email: [email protected]
介紹
之前的文章介紹了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是其設計的核心部分。.