Rob Pike 帶你重新認識字符串、字節、rune和字符

2021-02-19 Go語言中文網

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

字符串這個話題對於一篇博客文章來說似乎太簡單了,但是要很好地使用它們,不僅需要了解它們的工作原理,還需要了解字節,字符和 rune 的區別,以及 Unicode 和 UTF- 8,字符串和字符串直接量之間的區別,以及其他甚至更細微的區別。

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

文章篇幅還是挺長的,大家時間都很寶貴所以我先把文章探究的問題的結論放在前面,有時間的同學還是建議整篇讀一下。

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

介紹

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

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

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

什麼是字符串?

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

在 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。

總結一下,這是要點:

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語言中文網」:

Go語言中文網啟用微信學習交流群,歡迎加微信:274768166,投稿亦歡迎

相關焦點

  • 深入剖析go中字符串的編碼問題——特殊字符的string怎麼轉byte?
    go中的rune筆者在這裡猜測提問者期望的結果是「字符串轉字節切片和字符轉字節的結果保持一致」,這時rune就派上用場了,我們看看使用rune的效果:由上可知用rune切片去轉字符串時,它是直接將每個字符轉為對應的unicode。
  • 【Golang】快速複習指南QuickReview(一)——字符串string
    大家都知道C#和python的語法糖很多,特別是python,有時候讀別人寫過的源碼,不一定都能快速讀懂,甚至幾個開發人員編寫的都不一樣,而Golang不同,Golang只要堅持打牢基礎,就能閱讀原始碼,甚至讀懂,所以需要打牢基礎(這也說明博主基礎並不牢靠)。
  • 一文帶你了解c++和c中字符串的使用
    (2)字符串在內存中其實就是多個字節連續分布構成的(類似於數組,字符串和字符數組非常像)。         (3)不管是C++還是C語言中字符串都有3個核心要點:第一是用一個指針指向字符串頭;第二是固定尾部(字符串總是以'\0'來結尾);第三是組成字符串的各字符彼此地址相連。
  • Swift字符串和字符
    String 也可以用於在常量、變量、字面量和表達式中進行字符串插值,這使得創建用於展示、存儲和列印的字符串變得輕鬆自如。注意:Swift 的 String 類型與 Foundation NSString 類進行了無縫橋接。
  • C語言字符數組和字符串
    字符數組的各個元素依次存放字符串的各字符,字符數組的數組 名代表該數組的首地址,這為處理字符串中個別字符和引用整個字符串提供了極大的方便。一、字符數組字符數組的定義形式與前面介紹的數值數組相同。例如: 字符數組也允許在定義時進行初始化賦值。
  • Golang語言之字符串操作
    %o八進位整數%b二進位整數%f,%g,%e浮點數%t布爾值%c字符%s字符串%q帶雙引號的字符串%v內置格式內容%T類型%p內存地址%%字符%\n換行\t縮進文章內容主要以代碼注釋講解相關知識點字符串與數值相互轉換package mainimport ( "fmt" "strconv")
  • Python基礎:數據類型和變量&字符串和編碼
    Unicode標準也在不斷發展,但最常用的是用兩個字節表示一個字符(如果要用到非常偏僻的字符,就需要4個字節)。現代作業系統和大多數程式語言都直接支持Unicode。現在,捋一捋ASCII編碼和Unicode編碼的區別:ASCII編碼是1個字節,而Unicode編碼通常是2個字節。
  • MATLAB字符和字符串
    3.字符和字符串在MATLAB中,幾個字符(Character)可以構一個字符串(String)。考慮有這兩個字符串:>>str1 = 'hello';>>str2 = 'help';字符串str1和str2並不相等,所以使用strcmp函數來判斷的話,將會返回邏輯0(false)。
  • 字符串匹配的Rabin-Karp算法
    用在編譯器裡的有限自動機,也可以用來做字符串匹配。經典的字符串查找算法,時間複雜度是O(m(n - m + 1) ),粗略估計O(n^2)。m為要查找的字符串的長度,n為被查找的大字符串序列的長度。從字符數組的索引0一直遍歷到n-m,最後m個字符,每個索引比較一次,每次需要比較m個字符。
  • Python高效編程之88條軍規(1):編碼規範、字節序列與字符串
    例如,為了從bar包導入foo模塊,應該使用from bar import foo,而不要使用Import foo;(3)如果必須要使用相對的模塊名,應該顯式使用from . import foo形式;軍規2:了解字節序列(bytes)和字符串(str)的差異在Python語言中,有兩個數據類型可以表示字符序列:字節序列和字符串。
  • 手把手教你,學會Excel字符串提取
    對於需要區分單雙字節的情況,可以使用L EFTB和「B」代表byte,與LEFT和RIGHT函數的區別是,前者的第二參數為「字符」的數量,無論字符是單字節還是雙字節,均按一個字符計算。而加了「B」的LEFTB和RIGHTB函數,第二參數為「字節」的數量。漢字為雙字節字符,字母或數字為單字節字符。
  • 你真的知道 Python的 字符串是什麼嗎?
    預告一下,下一篇《你真的知道Python的字符串怎麼用嗎? 》將會展開介紹,敬請期待……字符串序列是一種不可變序列,這意味著它不能像可變序列一樣,進行就地修改。例如,在字符串「Python」的基礎上拼接「Cat」,得到字符串「PythonCat」,新的字符串是一個獨立的存在,它與基礎字符串「Python」並沒有關聯關係。
  • 詳解數據類型:byte、rune與string
    a 的值: A b 的值: Brune,佔用4個字節,共32位比特位,所以它和 uint32 本質上也沒有區別。它表示的是一個 Unicode字符(Unicode是一個可以表示世界範圍內的絕大部分字符的編碼規範)。
  • PHP中的字符串、編碼、UTF-8
    」、「字符串轉換」、「PHP字符串的本質」、「多字節字符串」。 字符串的定義和使用  PHP 中能夠通過四種方法設置字符串:  單引號字符串  單引號字符串類似於 Python中的原始字符串,也就是說單引號字符串沒有變量解析功能和特殊字符轉義功能。比如$str='hello\nworld',其中的\n並沒有換行功能。
  • Ollydbg之字符串、Windows API搜索
    每一個二進位位(bit)有0和1兩種狀態,因此八個二進位位就可以組合出256種狀態,這被稱為一個字節(byte)。也就是說,一個字節一共可以用來表示256種不同的狀態,每一個狀態對應一個符號,就是256個符號,從0000000到11111111。上個世紀60年代,美國制定了一套字符編碼,對英語字符與二進位位之間的關係,做了統一規定。這被稱為ASCII碼,一直沿用至今。
  • Ruby 字符串(String)
    26str.each_byte { |fixnum| block }傳遞 str 的每個字節給 block,以字節的十進位表示法返回每個字節。(other)如果兩個字符串有先攻的長度和內容,則這兩個字符串相等。30str.gsub(pattern, replacement) [or]str.gsub(pattern) { |match| block }返回 str 的副本,pattern 的所有出現都替換為 replacement 或 block 的值。
  • Python中如何計算字符串的長度
    第七十二節:計算字符串長度在學習計算字符串的長度之前,需要先了解一個概念:字符編碼。字符編碼(Character encoding),即為字集碼,就是把字符集中的字符編碼為指定集合中某一對象,用來以字節為單位實現字符在計算機中的存儲,和便於通過通信網絡的傳遞字符。
  • 【C++】搞懂char與wchar_t字符串
    常規字符串對於str1、str3、str4這種正常的字符串,就可以隨意拿字符串函數和下標訪問,進行各種操作。在windows下,char*的字符串編碼是多字節,用的本地編碼,就是我們的GBK。linux下char*直接就是utf8,所以兩個平臺char*字符串直接交流是不行的。。。1.2.
  • MySQL函數基礎——字符串函數詳解
    本篇將介紹各種字符串函數的功能和用法。計算字符串字符數的函數和字符串長度的函數CHAR_ LENGTH(str)返回值為字符串str 所包含的字符個數。一個多字節字符算作-一個單字符。使用CHAR_ LENGTH函數計算字符串字符個數,輸入語句如下:LENGTH(str)返回值為字符串的字節長度,使用utf8 (UNICODE的一種變 長字符編碼,又稱萬國碼)編碼字符集時,一個漢字是3個字節,一個數字或字母算一個字節。
  • 淺談Java中字符串的初始化及字符串操作類
    當你知道字符串的初始化細節後, 再去寫 Strings="hello"或 Strings=newString("hello")等代碼時, 就能做到心中有數。首先得搞懂字符串常量池的概念。常量池是Java的一項技術, 八種基礎數據類型除了float和double都實現了常量池技術.