Golang 入門 : 字符串及底層字符類型


字符串

基本使用

在 Go 語言中,字符串是一種基本類型,默認是通過 UTF-8 編碼的字符序列,當字符為 ASCII 碼時則占用 1 個字節,其它字符根據需要占用 2-4 個字節,比如中文編碼通常需要 3 個字節。

聲明和初始化

字符串的聲明和初始化非常簡單,舉例如下:

var str string         // 聲明字符串變量
str = "Hello World"    // 變量初始化
str2 := "你好呀"   // 也可以同時進行聲明和初始化

格式化輸出

還可以通過 Go 語言內置的 len() 函數獲取指定字符串的長度,以及通過 fmt 包提供的 Printf 進行字符串格式化輸出:

fmt.Printf("The length of \"%s\" is %d \n", str, len(str)) 
fmt.Printf("The first character of \"%s\" is %c.\n", str, ch)

轉義字符

Go 語言的字符串不支持單引號,只能通過雙引號定義字符串字面值,如果要對特定字符進行轉義,可以通過 \ 實現,就像我們上面在字符串中轉義雙引號和換行符那樣,常見的需要轉義的字符如下所示:

  • \n :換行符
  • \r :回車符
  • \t :tab 鍵
  • \u 或 \U :Unicode 字符
  • \\ :反斜杠自身

所以,上述打印代碼輸出結果為:

The length of "Hello world" is 11 
The first character of "Hello world" is H.

除此之外,你可以通過如下方式在字符串中包含 ":

label := `Search results for "Golang":`

多行字符串

對於多行字符串,也可以通過 ` 構建:

results := `Search results for "Golang":
- Go
- Golang
Golang Programming
`
fmt.Printf("%s", results)

打印結果如下:

Search results for "Golang":
- Go
- Golang
- Golang Programming

當然,使用 + 連接符也是可以的:

results := "Search results for \"Golang\":\n" +
"- Go\n" +
"- Golang\n" +
"- Golang Programming\n"
fmt.Printf("%s", results)

打印結果是一樣的,但是要多輸入不少字符,也不如上一種實現優雅。

不可變值類型

雖然可以通過數組下標方式訪問字符串中的字符:

ch := str[0] // 取字符串的第一個字符 

但是和數組不同,在 Go 語言中,字符串是一種不可變值類型,一旦初始化之后,它的內容不能被修改,比如看下面這個例子:

str := "Hello world"
str[0] = 'X' // 編譯錯誤

編譯器會報類似如下的錯誤:

cannot assign to str[0]

字符編碼

Go 語言中字符串默認是 UTF-8 編碼的 Unicode 字符序列,所以可以包含非 ANSI 字符,比如「Hello, 世界」可以出現在 Go 代碼中。

但需要注意的是,如果你的 Go 代碼需要包含非 ANSI 字符,保存源文件時請注意編碼格式必須選擇 UTF-8。特別是在 Windows 下一般編輯器都默認保存為本地編碼,比如中國地區可能是 GBK 編碼而不是 UTF-8,如果沒注意到這點在編譯和運行時就會出現一些意料之外的情況。

字符串的編碼轉換是處理文本文檔(比如 TXT、XML、HTML 等)時非常常見的需求,不過 Go 語言默認僅支持 UTF-8Unicode 編碼,對於其他編碼,Go 語言標准庫並沒有內置的編碼轉換支持。所幸的是我們可以很容易基於 iconv 庫包裝一個,這里有一個開源項目可供參考:https://github.com/qiniu/iconv

字符串操作

字符串連接

Go 內置提供了豐富的字符串函數,常見的操作包含連接、獲取長度和指定字符,獲取長度和指定字符前面已經介紹過,字符串連接只需要通過 + 連接符即可:

str = str + ", 世界"
str += ", 世界"  // 上述語句也可以簡寫為這樣,效果完全一樣

另外,還有一點需要注意的是如果字符串長度較長,需要換行,則 + 連接符必須出現在上一行的末尾,否則會報錯:

str = str +
        ", 世界"

字符串切片

在 Go 語言中,可以通過字符串切片實現獲取子串的功能:

str := "hello, world"
str1 := str[:5]  // 獲取索引5(不含)之前的子串
str2 := str[7:]  // 獲取索引7(含)之后的子串
str3 := str[0:5]  // 獲取從索引0(含)到索引5(不含)之間的子串
fmt.Println("str1:", str1)
fmt.Println("str2:", str2)
fmt.Println("str3:", str3)

Go 切片區間可以對比數學中的區間概念來理解,它是一個左閉右開的區間,比如上述 str[0:5] 對應到字符串元素的區間是 [0,5)str[:5] 對應的區間是 [0,5)(數組索引從 0 開始),str[7:] 對應的區間是 [7:len(str)](這是閉區間,是個例外,因為沒有指定區間結尾)。

所以,上述代碼打印結果如下:

str1: hello
str2: world
str3: hello

綜上所述,字符串切片通過 : 連接的起始點和結束點索引對字符串進行切片,冒號之前的數字代表起始點,為空表示從 0 開始,之后的數字代表結束點,為空表示到字符串最后,而不是子串的長度。所以 str[:] 會打印出完整的字符串來。

此外 Go 字符串也支持字符串比較、是否包含指定字符/子串、獲取指定子串索引位置、字符串替換、大小寫轉換、trim 等操作,更多操作 API,請參考標准庫 strings 包,這里就不一一展示了。

字符串遍歷

Go 語言支持兩種方式遍歷字符串。

一種是以字節數組的方式遍歷:

str := "Hello, 世界" 
n := len(str) 
for i := 0; i < n; i++ {
    ch := str[i]    // 依據下標取字符串中的字符,ch 類型為 byte
    fmt.Println(i, ch) 
}

這個例子的輸出結果為:

0 72 
1 101 
2 108 
3 108 
4 111 
5 44 
6 32 
7 228 
8 184 
9 150 
10 231 
11 149 
12 140

可以看出,這個字符串長度為 13,盡管從直觀上來說,這個字符串應該只有 9 個字符。這是因為每個中文字符在 UTF-8 中占 3 個字節,而不是 1 個字節。

另一種是以 Unicode 字符遍歷:

str := "Hello, 世界" 
for i, ch := range str { 
    fmt.Println(i, ch)    // ch 的類型為 rune 
}

輸出結果為:

0 72 
1 101 
2 108 
3 108 
4 111 
5 44 
6 32 
7 19990 
10 30028

這個時候,打印的就是 9 個字符了,因為以 Unicode 字符方式遍歷時,每個字符的類型是 rune,而不是 byte

看到這里可能你有點懵,會好奇 Go 底層到底是如何存儲字符串的,為什么不同遍歷方式獲取的結果不同呢?下面就來給大家簡單掰扯掰扯。

底層字符類型

Go 語言對字符串中的單個字符進行了單獨的類型支持,在 Go 語言中支持兩種字符類型:

  • 一種是 byte,代表 UTF-8 編碼中單個字節的值(它也是 uint8 類型的別名,兩者是等價的,因為正好占據 1 個字節的內存空間),它可用於區分字節值8位無符號整數值。;
  • 另一種是 rune,代表單個 Unicode 字符(它也是 uint32 類型的別名,因為正好占據 4 個字節的內存空間。關於 rune 相關的操作,可查閱 Go 標准庫的 unicode 包),它可用於區分字符值和整數值。。

runebytestring 都是 Go 的內置類型。string 是所有8位字節字符串的集合,通常但不一定代表UTF-8編碼的文本,字符串可能為空,但是不能為 nil,字符串類型的值是不可變的。

由上面得解釋我們大概可以明白,rune 可以表示得比 byte 多,string 類型的底層是一個byte 數組

字節和字符

剛剛上面標注了字節和字符,現在我們來梳理字符字節的概念

存儲單位 字節

  • 計算機存儲信息的最小單位,稱之為位 bit,二進制的一個0或1叫一位
  • 計算機存儲容量基本單位是字節 Byte,8個二進制位組成 1 個字節

信息表示單位 字符

  • 字符是一種符號,像 英文a和中文阿 就是不同字符
  • 不同的字符在不同的編碼格式下,所需要的存儲單位不一樣
  • ASCLII 編碼中一個英文字母一字節,一個漢字兩字節
  • UTF-8 編碼中 一個英文字母一字節,一個常見漢字3字節,不常用的超大字符集漢字4字節

UTF-8 和 Unicode 的區別

說到這里,我們需要區分 UTF-8Unicode 的區別。

Unicode 是一種字符集,囊括了目前世界上所有語言的所有字符,與之類似的術語還有 ASCII 字符集(僅包含 256 個字符)、ISO 8859-1 字符集等(包含所有西方拉丁字母),廣義的 Unicode 既包含了字符集,也包含了編碼規則,比如 UTF-8UTF-16UTF8MB4GBK 等。

因此 UTF-8Unicode 字符集的實現方式之一,它會將 Unicode 字符以某種方式進行編碼。在具體實現時,UTF-8 是一種變長的編碼規則,從 1~4 個字節不等,比如英文字符是 1 個字節,中文字符是 3 個字節。通過 UTF-8 編碼的 Unicode 字符以最大長度 4 個字節作為單個字符固定占據的內存空間,在 Go 語言中可以通過 unicode/utf8 包進行 UTF-8Unicode 之間的轉換。

所以如果從 Unicode 字符集的視角看,字符串的每個字符都是一個字符的獨立單元,但如果從 UTF-8 編碼的視角看,一個字符可能是由多個字節編碼而來的。

我們通過 len 函數獲取到的是字符串的字節長度,再據此通過字符數組的方式遍歷字符串時,是以 UTF-8 編碼的角度切入的;而當我們通過 range 關鍵字遍歷字符串時,又是從 Unicode 字符集的角度切入的,如此一來就得到了不同的結果。

出於簡化語言的考慮,Go 語言的多數 API 都假設字符串為 UTF-8 編碼。

Go 源碼文件默認采用Unicode字符集,Unicode碼點和內存中字節序列的變換實現使用了UTF-8,這使得Go編程無需考慮編碼轉換的問題非常方便

從編碼上來分析

  • byte 用來強調一個字節代表的數據(例如字符 a 就是 97),而不是數字;
  • byte 的操作單位是一個字節,可以理解為一個英文字符
  • rune 用來表示Unicode的碼點,即一個字符
  • rune 的操作單位是一個字符,不管這個字符是什么字符

通俗一點

  • byte 只能操作簡單的字符,不支持中文操作
  • rune 能操作任何字符

將 Unicode 編碼轉化為可打印字符

如果你想要將 Unicode 字符編碼轉化為對應的字符,可以使用 string 函數進行轉化::

str := "Hello, 世界" 
for i, ch := range str { 
    fmt.Println(i, string(ch))
}

對應的打印結果如下:

0 H
1 e
2 l
3 l
4 o
5 ,
6  
7 世
10 界

UTF-8 編碼不能這樣轉化,英文字符沒問題,因為一個英文字符就是一個字節,中文字符則會亂碼,因為一個中文字符編碼需要三個字節,轉化單個字節會出現亂碼。


免責聲明!

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



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