string 與 UTF-8
Go 中使用 UTF-8 對字符進行編碼
首先,我們需要對字符編碼有一定相關的了解,並明白為什么 Go 選中 UTF-8 作為字符編碼方式。
ASCII 和 Unicode
在計算機行業在美國興起時,人們使用「ASCII」對字符集進行處理:ASCII 使用 7 位 128 個字符(大小寫英文字母、數字、標點以及設備控制符)。這對當時的行業來說已經足夠使用了,但隨着計算機行業的興起,世界上使用其他語言的人無法在計算機上使用自己的文書體系。
為了解決這個問題,人們開始使用「Unicode」,如今已經定義到了第 8 版,定義了超過一百種語言文字的 12 萬個字符的碼點。Unicode 需要 32 位比特,也就是 4 個字節,計算機中的int32
便很適合保存這種數據類型,Go 中便是這樣認為的,因此為int32
設置了別名rune
。
但如果我們將所有的字符都按照「Unicode」進行編碼,這種編碼方式稱為 UTF-32 或者 UCS-4,每個 Unicode 碼點都需要占 4 個字節;但,大多數計算機的可讀文本為 ASCII,只需要 1 個字節便可以滿足編碼要求,而廣泛使用的字符也只需要 16 位字符即可,因此這種方式導致了不必要的存儲空間消耗。
UTF-8
UTF-8 以字節為單位對 Unicode 碼點進行變長編碼,是現行的一種 Unicode 標准。它每個符號用 1~4 個字節表示,例如 ASCII 的編碼僅需 1 個字節,其他常用的文字編碼是 2 或者 3 個字節。
在 UTF-8 中,「首字節的最高位」指明后面還有多少字節:
-
若最高位為 0,則表示它是 7 位的 ASCII 碼,那么它只需要使用一個字節;
-
若最高幾位是 110,那么它占用了兩個字節,則文字符號占用 2 個字節進行編碼,第二個字節以 10 開始,更長的編碼也是以此類推。
因此,對於需要不同空間的字符,UTF-8 的編碼方式如下:
0xxxxxxx 文字符號 0 ~ 127 ASCII
110xxxxx 10xxxxxx 128 ~ 2047 少於 128 個未使用的值
1110xxxx 110xxxxx 10xxxxxx 2048 ~ 65535 少於 2048 個未使用的值
11110xxx 1110xxxx 110xxxxx 10xxxxxx 65536 ~ 0x10ffff 其他未使用的值
顯然,對於 UTF-8,我們不能按下標直接訪問第 n 個字符,以此為代價,我們得到了許多方便的特性:
-
UTF-8 編碼緊湊,兼容 ASCII,且自同步:最多追溯 3 字節,就能定位一個字符的起始位置;
-
UTF-8是前綴編碼,故能夠從左往右解碼而不產生歧義,也無需超前預讀;
-
UTF-8 的編碼順序與字典序一致(Unicode 的碼點順序和字典序一致);
-
UTF-8編碼本身不會嵌入 NUL 字節(0 值),因此我們可以使用 NUL 標記字符串結尾。
Go 中的 UTF-8
Go 的源文件總是以 UTF-8 進行編碼,同時,其操作的文本字符串也是優先使用 UTF-8。
如何表示 UTF-8 字符
Go 中,string 字面量的轉義讓我們可以使用碼點來指明 Unicode 字符。有兩種形式:\uhhhh
表示 16 位碼點,\uhhhhhhhh
表示 32 位碼點(h 表示一個十六進制的數字),32 位的碼點基本用不到。這兩種形式都能用 UTF-8 表示給定的碼點,因此,下面三個字符串表示的是長度為 6 的相同串:
"世界"
"\xe4\xb8\x96\xe7\x95\x8c"
"\u4e16\u754c"
"\U00004e16\U0000754c"
「碼點值小於 256 的文字符號」(也就是 ASCII 碼)可以寫成單個十六進制轉義的形式,如將'A'
寫成'\x41'
;更高的碼點必須使用\u
或者\U
進行轉義,這也導致前面的\xe4\xb8\x96
不是合法的文字符號。
常用操作
由於 UTF-8 的優良特性,許多字符串操作都無需解碼,下面是strings
包中一些源碼。
可以直接判斷某個字符串是否為另一個前綴:
func HasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
或者判斷是否為另一個字符串的后綴:
func HasSuffix(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}
或者是否為另一個字符串的字串(實際上的實現使用了散列讓搜索更高效):
func Contains(s, substr string) bool {
for i := 0; i < len(s)-len(substr); i++ {
if HasPrefix(s[i:], substr) {
return true
}
}
return false
}
處理 Unicode 字符
Go 中的unicode
包擁有對單個文字符號的函數(例如區分字母和數字,轉換大小寫),unicode/utf8
包提供了按 UTF-8 編碼和解碼文字符號的函數。
在實際處理 Unicode 字符時,我們需要注意它實際上的字節數;看下面的例子:
import "unicode/utf8"
s := "世界"
fmt.Println(len(s)) // 輸出:6
fmt.Println(utf8.RuneCountInStrings(s)) // 輸出:2
可以看到,我們需要按做 UTF-8 解讀,才能得到符合常規認知的字符長度。
如果我們需要逐個處理這些字符,就需要使用 UTF-8 的解碼器,例如unicode/utf8
中的:
s := "世界, hello"
for i := 0; i < len(s) {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%d\t%c\n", i, r)
i += size
}
每次調用DecodeRuneInString
的調用都會返回 r(文字符號本身)和一個值 size(表示 r 按照 UTF-8 所占的字節數)。我們用 size 來更新 slice 的下標,這樣就能夠正確的打印字符:
0 世
3 界
6 ,
7
8 h
9 e
10 l
11 l
12 o
幸好 Go 中的「range 循環」也適用於字符串,對 UTF-8 進行隱式解碼,所以下述語句也能達到同樣的效果:
for i, r := range s {
fmt.Printf("%d\t%q\t%d\n", i, r, r)
}
這里的r
可以用%q
或者%d
來表示,前者會打印字符(如世
),后者打印對應的 unicode(如19990
)。
也因為 range 循環有對 UTF-8 的隱式編碼,因此我們可以直接使用它來統計字符串中的文字符號數:
n := 0
for range s {
n++
}
Go 中的相關標准庫
Go 語言中 4 個標准包對字符串操作很重要:bytes、strings、strconv 與 unicode
-
「strings」:提供用於搜索、替換、比較、修整、切分與連接字符串的函數
-
「bytes」:用於操作字節slice([]byte 類型的某些屬性和字符串相同)。例如可以使用
bytes.Buffer
高效地按增量方式構建字符串。 -
「strconv」:主要用於 string 與布爾值、整數、浮點數之間的相互轉換,或者是用於為字符串添加/去除引號。
-
「unicode」:主要用於判別文字符號特性;例如
IsDigit
、IsLetter
、IsUpper
和IsLower
。這些函數以單個字符作為參數,並返回布爾值。
下面我們用一些例子說明這些包的用法。
移除文件的系統路徑和后綴
下例中,basename 函數模仿 UNIX shell 中的同名實用程序,移除文件的系統路徑和可能存在的后綴:
1.首先我們看看不依賴任何庫的初版 basename:
/*
basename 移除路徑部分以及 .后綴
e.g., a=>a, a.go=>a, a/b/c.go=>c
*/
func basename(s string) string {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '/' {
s = s[i + 1:]
break
}
}
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '.' {
s = s[:i]
break
}
}
return s
}
2.接下來我們使用庫函數string.LastIndex
來簡化代碼:
func basename(s string) string {
slash := strings.LastIndex(s, "/") // 如果沒找到"\",slash 的取值為 -1
s = s[slash+1:]
if dot := string.LastIndex(s, "."); dot >= 0 {
s = s[:dot]
}
return s
}
規范化整數字符串
這個例子中,我們對子字符串進行操作:接受一個表示整數的字符串,如12345
,從右側開始每隔三個數字就插入一個逗號,形如12,345
:
func comma(s string) string {
n := len(3)
if n <= 3 {
return s
}
return comma(s[:n-3]) + "," + s[n-3:]
}
在 Go 語言中,字符串可以和字節 slice 相互轉換:
s := "abc"
b := []byte(s)
s2 := string(b)
正常情況下,這種 string 和 slice 的相互轉換都會進行拷貝,這樣可以保證即使 b 的字節在轉換后發生改變,s 也不會一起變化。
但如果我們不需要這種特性,就會產生不必要的內存消耗,為了避免這種情況,bytes
和strings
包中都包含了相應的使用函數,它們兩兩對應。例如,string
包中有下面 6 個函數:
func Contains(s, substr string) bool
func Count(s, sep string) bool
func Fields(s string) []string
func HasPrefix(s, prefix string) bool
func Index(s, sep string) int
func Join(a []string, sep string) string
bytes
包中的對應函數為:
func Contains(b, subslice []byte) bool
func Count(b, sep []byte) bool
func Fields(b []byte) [][]byte
func HasPrefix(b, prefix []byte) bool
func Index(b, sep []byte) int
func Join(a [][]byte, sep []byte) []byte
唯一不同的是,操作對象由字符串變為了 slice
bytes
包為高效處理字節 slice 提供了「Buffer」類型。它起始為空,大小隨着各種類型數據的寫入而增長,如 string、byte 和 []byte。如下例,bytes.Buffer
變量無需初始化,因為零值本來就有效:
// intsToString 與 fmt.Sprintf(values) 類似,但插入了逗號
func intsToString(values []int) string {
var buf bytes.Buffer
buf.WriteByte('[')
for i, v := range values {
if i > 0 {
buf.WriteString(", ")
}
fmt.Fprintf(&buf, "%d", v)
}
buf.WriteByte(']')
}
func main() {
fmt.Println(intsToString([]int{1, 2, 3})) // 輸出: [1, 2, 3]
}
如果要在byte.Buffer
變量后添加任意文字符號的 UTF-8 編碼,最好使用WriteRune
方法,而追加 ASCII 字符,則使用WriteByte
即可。
字符串和數字的相互轉換
通常,要將整數轉換成字符串,一種選擇是使用fmt.Sprintf
,另一種做法是用函數strconv.Itoa
:
x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv(x)) // 輸出: 123 123
而FormatInt
和FormatUnit
可以按不同的進位制格式化數字:
fmt.Println(strconv.FormatInt(int64(x), 2)) // 輸出 x 的二進制表示: 1111011
golang字符串比較的三種常見方法
// 1. 自建方法“==”,區分大小寫,最簡單的方法
fmt.Println("go"=="go") // true
fmt.Println("GO"=="go") // false
// 2. Compare函數,區分大小寫,比自建方法“==”的速度要快,下面是注釋
// Compare is included only for symmetry with package bytes.
// It is usually clearer and always faster to use the built-in
// string comparison operators ==, <, >, and so on.
// func Compare(a, b string) int
fmt.Println(strings.Compare("GO","go")) // -1 ,也就是 "GO" < "go" (因為是字典序)
fmt.Println(strings.Compare("go","go")) // 0
// 3. 比較UTF-8編碼在小寫的條件下是否相等,不區分大小寫,下面是注釋
// EqualFold reports whether s and t, interpreted as UTF-8 strings,
// are equal under Unicode case-folding.
// func EqualFold(s, t string) bool
fmt.Println(strings.EqualFold("GO","go")) // true,因為不區分大小寫
輸出:
true
false
-1
0
true