一文搞定 Golang 字符串:高效編碼的最佳實踐

本文介紹 Golang 中處理字符串的最佳實踐,確保開發項目中實現最佳的效率、最佳性能並讓代碼更優雅。

字符串可以爲 nil 嗎?

如下所示,在創建字符串變量時,默認值必須是空字符串 ""。

如果我們嘗試使用 nil 值初始化字符串變量,將遇到一個錯誤,提示 nil 不能在變量聲明中用作字符串值。例如:

func main() {
   var s string
   s = nil // Cannot use 'nil' as the type string
   fmt.Println(s)
}

編譯器會提示我們不能將 nil 賦值給字符串類型的變量。因此,我們可以簡單地聲明變量而不賦任何值,或者使用空字符串 "" 作爲默認值:

func main() {
   var s string
   var ss = ""
   fmt.Println(s, ss)
}

如果我們堅持要在字符串類型變量中使用 nil 值,應該使用指針來實現,如下所示:

func main() {
   var s *string
   fmt.Println(s)
}

然而,在採用這種方法時,我們必須謹慎行事。每次給變量賦值時,都需要額外的代碼,並且在分配新值之前必須進行零值或先前值的檢查。

func main() {
   var s *string
   tmp := "hello"
   s = &tmp
   fmt.Printf("address: %+v, value: %s", s, *s)
}

在這一點上,打印出變量 s 的地址以及它指向的值:

address: 0xc00002a030, value: hello

字符串是不可變的

Golang 中的字符串是不可變的,這意味着我們無法更改每個單獨字符的值。例如:

func main() {
   tmp := "hello"
   tmp[0] = 'J'
   fmt.Println(tmp)
}

以上代碼將導致編譯錯誤,因爲不允許對 tmp[0] 賦值。

嘗試修改字符串中單個字符時的常見錯誤如下:

func main() {
   tmp := "hello"
   tmp_str:= []byte(tmp)
   tmp_str[0] = 'J'
   fmt.Println(string(tmp_str))
  
   tmp2 := "world"
   tmp_str2 := []byte(tmp2)
   tmp_str2[0] = 'J'
   fmt.Println(string(tmp_str2))
}

Result:

Jello
Jorld

顯示的輸出符合我們的期望,但這不是修改特定字符的正確方法。

這是因爲我們打算修改的個別部分可能存儲在多個字節中。即使您嘗試將變量轉換爲 rune 類型並修改所需的部分,我必須強調這是不可行的,因爲它可能跨越多個 rune。需要謹慎處理!

Strings 是 byte 數組

在 Golang 中,字符串由字節(字節片段)組成,某些字符可能需要存儲在多個字節中,比如 “♥”。

因此,當確定字符串類型變量的長度時,我們在編碼時必須謹慎處理。例如:

package main

import (
   "fmt"
   "unicode/utf8"
)

func main() {
   tmp := "€"
   fmt.Println("bytes: ", len(tmp))
   fmt.Println("runes: ", utf8.RuneCountInString(tmp))
}

len 函數返回字符串中的字節數,而不是字符數。當我們需要確定字符串中的符文數時,可以使用 utf8.RuneCountInString() 函數。

另一個常見的誤解是使用 utf8.RuneCountInString() 來確定字符數。然而,這並不總是正確的,因爲字符串變量可能跨越多個符文。考慮以下示例:

func main() {
   tmp := "❤️"
   fmt.Println("bytes: ", len(tmp))
   fmt.Println("runes: ", utf8.RuneCountInString(tmp))
}

Result:

bytes:  6
runes:  2

for ... range String

在 Golang 中,使用索引來檢索字符串的單個部分將爲我們提供字符的 uint 值,並且它只能檢索到第一個字節。

然而,在使用字符串變量的 for 循環中,我們可以訪問每個字符的 rune 值:

func main() {
   tmp := "❤€%……&*"
   fmt.Printf("char at 0 index, has type %T and value is %+v\n", tmp[0], tmp[0])
  
   for _, t := range tmp {
      fmt.Printf("value is %+v type is %T\n", t, t)
   }
}

Result:

char at 0 index, has type uint8 and value is 226
value is 10084 type is int32
value is 8364 type is int32
value is 37 type is int32
value is 8230 type is int32
value is 8230 type is int32
value is 38 type is int32
value is 42 type is int32

在迭代字符串時,還需要注意變量中可能存在的非 UTF-8 字符。如果 Golang 無法將它們解釋爲 UTF-8,則會使用 Unicode 替換而不是實際值。

String 比較

在 Golang 中,我們可以始終使用 == 來檢查簡單字符串是否相等。

然而,如果我們的變量包含隱藏的點,建議在比較兩個字符串變量之前使用 Unicode 歸一化包對它們進行規範化:

func main() {
   cafe1 := "Café"
   cafe2 := "Cafe\u0301"
  
   normalizeCafe1 := norm.NFC.String(cafe1)
   normalizeCafe2 := norm.NFC.String(cafe2)
   fmt.Println(cafe1 == cafe2)
   fmt.Println(normalizeCafe1 == normalizeCafe2)
}

高效字符串拼接

使用 + 連接大量字符串可能效率低下。利用 strings.Builder 是構建字符串的最有效方法之一:

func main() {
   sBuild := strings.Builder{}
   for i := 0; i < 1000; i++ {
      sBuild.WriteString("hello ")
   }
   result := sBuild.String()
   fmt.Println(result)
}

與傳統的使用 + 的方法相比,這種方法更快,佔用的內存更少,並且避免了創建不必要的中間字符串。

我們還可以使用 bytes.Buffer 包來實現這個目標。

總結

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/rED7zbk5hk9_jrskPZNoOg