字符串
基本使用
在 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-8
和 Unicode
編碼,對於其他編碼,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
包),它可用於區分字符值和整數值。。
rune
、 byte
和 string
都是 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-8
和 Unicode
的區別。
Unicode
是一種字符集,囊括了目前世界上所有語言的所有字符,與之類似的術語還有 ASCII
字符集(僅包含 256 個字符)、ISO 8859-1
字符集等(包含所有西方拉丁字母),廣義的 Unicode
既包含了字符集,也包含了編碼規則,比如 UTF-8
、UTF-16
、UTF8MB4
、GBK
等。
因此 UTF-8
是 Unicode
字符集的實現方式之一,它會將 Unicode
字符以某種方式進行編碼。在具體實現時,UTF-8
是一種變長的編碼規則,從 1~4 個字節不等,比如英文字符是 1 個字節,中文字符是 3 個字節。通過 UTF-8
編碼的 Unicode
字符以最大長度 4 個字節作為單個字符固定占據的內存空間,在 Go 語言中可以通過 unicode/utf8
包進行 UTF-8
和 Unicode
之間的轉換。
所以如果從 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
編碼不能這樣轉化,英文字符沒問題,因為一個英文字符就是一個字節,中文字符則會亂碼,因為一個中文字符編碼需要三個字節,轉化單個字節會出現亂碼。