Go 中的字符串相關操作


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」:主要用於判別文字符號特性;例如IsDigitIsLetterIsUpperIsLower。這些函數以單個字符作為參數,並返回布爾值。

下面我們用一些例子說明這些包的用法。

移除文件的系統路徑和后綴

下例中,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 也不會一起變化。

但如果我們不需要這種特性,就會產生不必要的內存消耗,為了避免這種情況,bytesstrings包中都包含了相應的使用函數,它們兩兩對應。例如,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

FormatIntFormatUnit可以按不同的進位制格式化數字:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM