字符串類型是現(xiàn)代編程語言中最常使用的數(shù)據(jù)類型之一。在Go語言的先祖之一C語言當(dāng)中,字符串類型并沒有被顯式定義,而是以字符串字面值常量或以’\0’結(jié)尾的字符類型(char)數(shù)組來呈現(xiàn)的:
#define GOAUTHERS "Robert Griesemer, Rob Pike, and Ken Thompson"
const char * s = "hello world"
char s[] = "hello gopher"
這給C程序員在使用字符串時帶來一些問題,諸如:
● 類型安全性差;
● 字符串操作要時時刻刻考慮結(jié)尾的’\0’;
● 字符串?dāng)?shù)據(jù)可變(主要指以字符數(shù)組形式定義的字符串類型);
● 獲取字符串長度代價大(O(n)的時間復(fù)雜度);
● 未內(nèi)置對非ASCII字符(如中文字符)的處理。
Go語言修復(fù)了C語言的這一“缺陷”,內(nèi)置了string類型,統(tǒng)一了對字符串的抽象。
Go語言的字符串類型
在Go語言中,無論是字符串常量、字符串變量還是代碼中出現(xiàn)的字符串字面量,它們的類型都被統(tǒng)一設(shè)置為string:
const (
s = "string constant"
)
func main() {
var s1 string = "string variable"
fmt.Printf("%T\n", s) // string
fmt.Printf("%T\n", s1) // string
fmt.Printf("%T\n", "temporary string literal") // string
}
Go的string類型設(shè)計充分吸取了C語言字符串設(shè)計的經(jīng)驗(yàn)教訓(xùn),并結(jié)合了其他主流語言在字符串類型設(shè)計上的最佳實(shí)踐,最終呈現(xiàn)的string類型具有如下功能特點(diǎn)。
(1)string類型的數(shù)據(jù)是不可變的
一旦聲明了一個string類型的標(biāo)識符,無論是常量還是變量,該標(biāo)識符所指代的數(shù)據(jù)在整個程序的生命周期內(nèi)便無法更改。下面嘗試修改一下string數(shù)據(jù),看看能得到怎樣的結(jié)果。
我們先來看第一種方法:
func main() {
// 原始字符串
var s string = "hello"
fmt.Println("original string:", s)
// 切片化后試圖改變原字符串
sl := []byte(s)
sl[0] = 't'
fmt.Println("slice:", string(sl))
fmt.Println("after reslice, the original string is:", string(s))
}
該程序的運(yùn)行結(jié)果如下:
original string: hello
slice: tello
after reslice, the original string is: hello
在上面的例子中,我們試圖將string轉(zhuǎn)換為一個切片并通過該切片對其內(nèi)容進(jìn)行修改,但結(jié)果事與愿違。對string進(jìn)行切片化后,Go編譯器會為切片變量重新分配底層存儲而不是共用string的底層存儲,因此對切片的修改并未對原string的數(shù)據(jù)產(chǎn)生任何影響。
我們再來試試通過更為“暴力”一些的手段對string的數(shù)據(jù)發(fā)起“攻擊”:
func main() {
// 原始string
var s string = "hello"
fmt.Println("original string:", s)
// 試圖通過unsafe指針改變原始string
modifyString(&s)
fmt.Println(s)
}
func modifyString(s *string) {
// 取出第一個8字節(jié)的值
p := (*uintptr)(unsafe.Pointer(s))
// 獲取底層數(shù)組的地址
var array *[5]byte = (*[5]byte)(unsafe.Pointer(*p))
var len *int = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(s)) + unsafe.Sizeof((*uintptr)(nil))))
for i := 0; i < (*len); i++ {
fmt.Printf("%p => %c\n", &((*array)[i]), (*array)[i])
p1 := &((*array)[i])
v := (*p1)
(*p1) = v + 1 //try to change the character
}
}
我們試圖通過unsafe指針指向string在運(yùn)行時內(nèi)部表示結(jié)構(gòu)(具體參考本條后面的講解)中的數(shù)據(jù)存儲塊的地址,然后通過指針修改那塊內(nèi)存中存儲的數(shù)據(jù)。
運(yùn)行這段程序得到下面的結(jié)果:
original string: hello
0x10d1b9d => h
unexpected fault address 0x10d1b9d
fatal error: fault
[signal SIGBUS: bus error code=0x2 addr=0x10d1b9d pc=0x109b079]
我們看到,對string的底層的數(shù)據(jù)存儲區(qū)僅能進(jìn)行只讀操作,一旦試圖修改那塊區(qū)域的數(shù)據(jù),便會得到SIGBUS的運(yùn)行時錯誤,對string數(shù)據(jù)的“篡改攻擊”再次以失敗告終。
2)零值可用
Go string類型支持“零值可用”的理念。Go字符串無須像C語言中那樣考慮結(jié)尾’\0’字符,因此其零值為"",長度為0。
var s string
fmt.Println(s) // s = ""
fmt.Println(len(s)) // 0
3)獲取長度的時間復(fù)雜度是O(1)級別
Go string類型數(shù)據(jù)是不可變的,因此一旦有了初值,那塊數(shù)據(jù)就不會改變,其長度也不會改變。Go將這個長度作為一個字段存儲在運(yùn)行時的string類型的內(nèi)部表示結(jié)構(gòu)中(后文有說明)。這樣獲取string長度的操作,即len(s)實(shí)際上就是讀取存儲在運(yùn)行時中的那個長度值,這是一個代價極低的O(1)操作。
4)支持通過+/+=操作符進(jìn)行字符串連接
對開發(fā)者而言,通過+/+=操作符進(jìn)行的字符串連接是體驗(yàn)最好的字符串連接操作,Go語言支持這種操作:
s := "Rob Pike, "
s = s + "Robert Griesemer, "
s += " Ken Thompson"
fmt.Println(s) // Rob Pike, Robert Griesemer, Ken Thompson
5)支持各種比較關(guān)系操作符:==、!= 、>=、<=、>和<
func main() {
// ==
s1 := "世界和平"
s2 := "世界" + "和平"
fmt.Println(s1 == s2) // true
// !=
s1 = "Go"
s2 = "C"
fmt.Println(s1 != s2) // true
// < 和 <=
s1 = "12345"
s2 = "23456"
fmt.Println(s1 < s2) // true
fmt.Println(s1 <= s2) // true
// > 和 >=
s1 = "12345"
s2 = "123"
fmt.Println(s1 > s2) // true
fmt.Println(s1 >= s2) // true
}
由于Go string是不可變的,因此如果兩個字符串的長度不相同,那么無須比較具體字符串?dāng)?shù)據(jù)即可斷定兩個字符串是不同的。如果長度相同,則要進(jìn)一步判斷數(shù)據(jù)指針是否指向同一塊底層存儲數(shù)據(jù)。如果相同,則兩個字符串是等價的;如果不同,則還需進(jìn)一步比對實(shí)際的數(shù)據(jù)內(nèi)容。
6)對非ASCII字符提供原生支持
Go語言源文件默認(rèn)采用的Unicode字符集。Unicode字符集是目前市面上最流行的字符集,幾乎囊括了所有主流非ASCII字符(包括中文字符)。Go字符串的每個字符都是一個Unicode字符,并且這些Unicode字符是以UTF-8編碼格式存儲在內(nèi)存當(dāng)中的。
我們來看一個例子:
func main() {
// 中文字符 Unicode碼點(diǎn) UTF8編碼
// 中 U+4E2D E4B8AD
// 國 U+56FD E59BBD
// 歡 U+6B22 E6ACA2
// 迎 U+8FCE E8BF8E
// 您 U+60A8 E682A8
s := "中國歡迎您"
rs := []rune(s)
sl := []byte(s)
for i, v := range rs {
var utf8Bytes []byte
for j := i * 3; j < (i+1)*3; j++ {
utf8Bytes = append(utf8Bytes, sl[j])
}
fmt.Printf("%s => %X => %X\n", string(v), v, utf8Bytes)
}
}
我們看到字符串變量s中存儲的文本是“中國歡迎您”五個漢字字符(非ASCII字符范疇),這里輸出了每個中文字符對應(yīng)的Unicode碼點(diǎn)(Code Point,見輸出結(jié)果的第二列),一個rune對應(yīng)一個碼點(diǎn)。UTF-8編碼是Unicode碼點(diǎn)的一種字符編碼形式,是最常用的一種編碼格式,也是Go默認(rèn)的字符編碼格式。我們還可以使用其他字符編碼格式來映射Unicode碼點(diǎn),比如UTF-16等。
在UTF-8中,大多數(shù)中文字符都使用三字節(jié)表示。[]byte(s)的轉(zhuǎn)型讓我們獲得了s底層存儲的“復(fù)制品”,從而得到每個漢字字符對應(yīng)的UTF-8編碼字節(jié)(見輸出結(jié)果的第三列)。
中 => 4E2D => E4B8AD
國 => 56FD => E59BBD
歡 => 6B22 => E6ACA2
迎 => 8FCE => E8BF8E
您 => 60A8 => E682A8
7)原生支持多行字符串
Go語言直接提供了通過反引號構(gòu)造“所見即所得”的多行字符串的方法:文章來源:http://www.zghlxwxcb.cn/news/detail-698563.html
const s = `好雨知時節(jié),當(dāng)春乃發(fā)生。
隨風(fēng)潛入夜,潤物細(xì)無聲。
野徑云俱黑,江船火獨(dú)明。
曉看紅濕處,花重錦官城。`
func main() {
fmt.Println(s)
}
運(yùn)行結(jié)果:文章來源地址http://www.zghlxwxcb.cn/news/detail-698563.html
好雨知時節(jié),當(dāng)春乃發(fā)生。
隨風(fēng)潛入夜,潤物細(xì)無聲。
野徑云俱黑,江船火獨(dú)明。
曉看紅濕處,花重錦官城。
到了這里,關(guān)于go基礎(chǔ)09-Go語言的字符串類型的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!