天天看點

Go字元串的常見操作

作者:jodkreaper

由于字元串的不可變性,針對字元串,我們更多是嘗試對其進行讀取,或者将它作為一個組成單元去建構其他字元串,又或是轉換為其他類型。

下标操作

在字元串的實作中,真正存儲資料的是底層的數組。字元串的下标操作本質上等價于底層數組的下标操作。我們在前面的代碼中實際碰到過針對字元串的下标操作,形式是這樣的:

var s = "中國人"
fmt.Printf("0x%x\n", s[0]) // 0xe4:字元“中” utf-8編碼的第一個位元組           

我們可以看到,通過下标操作,我們擷取的是字元串中特定下标上的位元組,而不是字元。

字元疊代

Go 有兩種疊代形式:正常 for 疊代與 for range 疊代。你要注意,通過這兩種形式的疊代對字元串進行操作得到的結果是不同的。

通過正常 for 疊代對字元串進行的操作是一種位元組視角的疊代,每輪疊代得到的的結果都是組成字元串内容的一個位元組,以及該位元組所在的下标值,這也等價于對字元串底層數組的疊代,比如下面代碼:

var s = "中國人"

for i := 0; i < len(s); i++ {
  fmt.Printf("index: %d, value: 0x%x\n", i, s[i])
}           

運作這段代碼,我們會看到,經過正常 for 疊代後,我們擷取到的是字元串裡字元的 UTF-8 編碼中的一個位元組:

index: 0, value: 0xe4
index: 1, value: 0xb8
index: 2, value: 0xad
index: 3, value: 0xe5
index: 4, value: 0x9b
index: 5, value: 0xbd
index: 6, value: 0xe4
index: 7, value: 0xba
index: 8, value: 0xba           

而像下面這樣使用 for range 疊代,我們得到的又是什麼呢?我們繼續看代碼:

var s = "中國人"

for i, v := range s {
    fmt.Printf("index: %d, value: 0x%x\n", i, v)
}           
index: 0, value: 0x4e2d
index: 3, value: 0x56fd
index: 6, value: 0x4eba           

我們看到,通過 for range 疊代,我們每輪疊代得到的是字元串中 Unicode 字元的碼點值,以及該字元在字元串中的偏移值。我們可以通過這樣的疊代,擷取字元串中的字元個數,而通過 Go 提供的内置函數 len,我們隻能擷取字元串内容的長度(位元組個數)。當然了,擷取字元串中字元個數更專業的方法,是調用标準庫 UTF-8 包中的 RuneCountInString 函數,這點你可以自己試一下。

字元串連接配接

字元串内容是不可變的,但這并不妨礙我們基于已有字元串建立新字元串。Go 原生支援通過 +/+= 操作符進行字元串連接配接,這也是對開發者體驗最好的字元串連接配接操作,你可以看看下面這段代碼:

s := "Rob Pike, "
s = s + "Robert Griesemer, "
s += " Ken Thompson"

fmt.Println(s) // Rob Pike, Robert Griesemer, Ken Thompson           

不過,雖然通過 +/+= 進行字元串連接配接的開發體驗是最好的,但連接配接性能就未必是最快的了。除了這個方法外,Go 還提供了 strings.Builder、strings.Join、fmt.Sprintf 等函數來進行字元串連接配接操作。

字元串比較

Go 字元串類型支援各種比較關系操作符,包括 = =、!= 、>=、<=、> 和 <。在字元串的比較上,Go 采用字典序的比較政策,分别從每個字元串的起始處,開始逐個位元組地對兩個字元串類型變量進行比較。

當兩個字元串之間出現了第一個不相同的元素,比較就結束了,這兩個元素的比較結果就會做為串最終的比較結果。如果出現兩個字元串長度不同的情況,長度比較小的字元串會用空元素補齊,空元素比其他非空元素都小。

func main() {
        // ==
        s1 := "世界和平"
        s2 := "世界" + "和平"
        fmt.Println(s1 == s2) // true

        // !=
        s1 = "Go"
        s2 = "C"
        fmt.Println(s1 != s2) // true

        // < and <=
        s1 = "12345"
        s2 = "23456"
        fmt.Println(s1 < s2)  // true
        fmt.Println(s1 <= s2) // true

        // > and >=
        s1 = "12345"
        s2 = "123"
        fmt.Println(s1 > s2)  // true
        fmt.Println(s1 >= s2) // true
}           

鑒于 Go string 類型是不可變的,是以說如果兩個字元串的長度不相同,那麼我們不需要比較具體字元串資料,也可以斷定兩個字元串是不同的。但是如果兩個字元串長度相同,就要進一步判斷,資料指針是否指向同一塊底層存儲資料。如果還相同,那麼我們可以說兩個字元串是等價的,如果不同,那就還需要進一步去比對實際的資料内容。

字元串轉換

Go 支援字元串與位元組切片、字元串與 rune 切片的雙向轉換,并且這種轉換無需調用任何函數,隻需使用顯式類型轉換就可以了。我們看看下面代碼:

var s string = "中國人"
                      
// string -> []rune
rs := []rune(s) 
fmt.Printf("%x\n", rs) // [4e2d 56fd 4eba]
                
// string -> []byte
bs := []byte(s) 
fmt.Printf("%x\n", bs) // e4b8ade59bbde4baba
                
// []rune -> string
s1 := string(rs)
fmt.Println(s1) // 中國人
                
// []byte -> string
s2 := string(bs)
fmt.Println(s2) // 中國人           

這樣的轉型看似簡單,但無論是 string 轉切片,還是切片轉 string,這類轉型背後也是有着一定開銷的。這些開銷的根源就在于 string 是不可變的,運作時要為轉換後的類型配置設定新記憶體。