原文鏈接:https://gopl-zh.github.io/ch3/ch3-05.html
一個(gè)字符串是一個(gè)不可改變的字節(jié)序列。字符串可以包含任意的數(shù)據(jù),包括byte值0,但是通常是用來包含人類可讀的文本。文本字符串通常被解釋為采用UTF8編碼的Unicode碼點(diǎn)(rune)序列,我們稍后會(huì)詳細(xì)討論這個(gè)問題。
內(nèi)置的len函數(shù)可以返回一個(gè)字符串中的字節(jié)數(shù)目(不是rune字符數(shù)目),索引操作s[i]返回第i個(gè)字節(jié)的字節(jié)值,i必須滿足0 ≤ i< len(s)條件約束。
s := "hello, world"
fmt.Println(len(s)) // "12"
fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')
如果試圖訪問超出字符串索引范圍的字節(jié)將會(huì)導(dǎo)致panic異常:
c := s[len(s)] // panic: index out of range
第i個(gè)字節(jié)并不一定是字符串的第i個(gè)字符,因?yàn)閷τ诜茿SCII字符的UTF8編碼會(huì)要兩個(gè)或多個(gè)字節(jié)。我們先簡單說下字符的工作方式。
子字符串操作s[i:j]基于原始的s字符串的第i個(gè)字節(jié)開始到第j個(gè)字節(jié)(并不包含j本身)生成一個(gè)新字符串。生成的新字符串將包含j-i個(gè)字節(jié)。
fmt.Println(s[0:5]) // "hello"
同樣,如果索引超出字符串范圍或者j小于i的話將導(dǎo)致panic異常。
不管i還是j都可能被忽略,當(dāng)它們被忽略時(shí)將采用0作為開始位置,采用len(s)作為結(jié)束的位置。
fmt.Println(s[:5]) // "hello"
fmt.Println(s[7:]) // "world"
fmt.Println(s[:]) // "hello, world"
其中+操作符將兩個(gè)字符串連接構(gòu)造一個(gè)新字符串:
fmt.Println("goodbye" + s[5:]) // "goodbye, world"
字符串可以用==和<進(jìn)行比較;比較通過逐個(gè)字節(jié)比較完成的,因此比較的結(jié)果是字符串自然編碼的順序。
字符串的值是不可變的:一個(gè)字符串包含的字節(jié)序列永遠(yuǎn)不會(huì)被改變,當(dāng)然我們也可以給一個(gè)字符串變量分配一個(gè)新字符串值??梢韵裣旅孢@樣將一個(gè)字符串追加到另一個(gè)字符串:
s := "left foot"
t := s
s += ", right foot"
這并不會(huì)導(dǎo)致原始的字符串值被改變,但是變量s將因?yàn)?=語句持有一個(gè)新的字符串值,但是t依然是包含原先的字符串值。
fmt.Println(s) // "left foot, right foot"
fmt.Println(t) // "left foot"
因?yàn)樽址遣豢尚薷牡?,因此嘗試修改字符串內(nèi)部數(shù)據(jù)的操作也是被禁止的:
s[0] = 'L' // compile error: cannot assign to s[0]
不變性意味著如果兩個(gè)字符串共享相同的底層數(shù)據(jù)的話也是安全的,這使得復(fù)制任何長度的字符串代價(jià)是低廉的。同樣,一個(gè)字符串s和對應(yīng)的子字符串切片s[7:]的操作也可以安全地共享相同的內(nèi)存,因此字符串切片操作代價(jià)也是低廉的。在這兩種情況下都沒有必要分配新的內(nèi)存。 圖3.4演示了一個(gè)字符串和兩個(gè)子串共享相同的底層數(shù)據(jù)。
字符串值也可以用字符串面值方式編寫,只要將一系列字節(jié)序列包含在雙引號(hào)內(nèi)即可:
"Hello, 世界"
因?yàn)镚o語言源文件總是用UTF8編碼,并且Go語言的文本字符串也以UTF8編碼的方式處理,因此我們可以將Unicode碼點(diǎn)也寫到字符串面值中。
在一個(gè)雙引號(hào)包含的字符串面值中,可以用以反斜杠\
開頭的轉(zhuǎn)義序列插入任意的數(shù)據(jù)。下面的換行、回車和制表符等是常見的ASCII控制代碼的轉(zhuǎn)義方式:
\a 響鈴
\b 退格
\f 換頁
\n 換行
\r 回車
\t 制表符
\v 垂直制表符
\' 單引號(hào)(只用在 '\'' 形式的rune符號(hào)面值中)
\" 雙引號(hào)(只用在 "..." 形式的字符串面值中)
\\ 反斜杠
可以通過十六進(jìn)制或八進(jìn)制轉(zhuǎn)義在字符串面值中包含任意的字節(jié)。一個(gè)十六進(jìn)制的轉(zhuǎn)義形式是\xhh
,其中兩個(gè)h表示十六進(jìn)制數(shù)字(大寫或小寫都可以)。一個(gè)八進(jìn)制轉(zhuǎn)義形式是\ooo
,包含三個(gè)八進(jìn)制的o數(shù)字(0到7),但是不能超過\377
(譯注:對應(yīng)一個(gè)字節(jié)的范圍,十進(jìn)制為255)。每一個(gè)單一的字節(jié)表達(dá)一個(gè)特定的值。稍后我們將看到如何將一個(gè)Unicode碼點(diǎn)寫到字符串面值中。
一個(gè)原生的字符串面值形式是`...`,使用反引號(hào)代替雙引號(hào)。在原生的字符串面值中,沒有轉(zhuǎn)義操作;全部的內(nèi)容都是字面的意思,包含退格和換行,因此一個(gè)程序中的原生字符串面值可能跨越多行(譯注:在原生字符串面值內(nèi)部是無法直接寫`字符的,可以用八進(jìn)制或十六進(jìn)制轉(zhuǎn)義或+"`"連接字符串常量完成)。唯一的特殊處理是會(huì)刪除回車以保證在所有平臺(tái)上的值都是一樣的,包括那些把回車也放入文本文件的系統(tǒng)(譯注:Windows系統(tǒng)會(huì)把回車和換行一起放入文本文件中)。
原生字符串面值用于編寫正則表達(dá)式會(huì)很方便,因?yàn)檎齽t表達(dá)式往往會(huì)包含很多反斜杠。原生字符串面值同時(shí)被廣泛應(yīng)用于HTML模板、JSON面值、命令行提示信息以及那些需要擴(kuò)展到多行的場景。
const GoUsage = `Go is a tool for managing Go source code.
Usage:
go command [arguments]
...`
在很久以前,世界還是比較簡單的,起碼計(jì)算機(jī)世界就只有一個(gè)ASCII字符集:美國信息交換標(biāo)準(zhǔn)代碼。ASCII,更準(zhǔn)確地說是美國的ASCII,使用7bit來表示128個(gè)字符:包含英文字母的大小寫、數(shù)字、各種標(biāo)點(diǎn)符號(hào)和設(shè)備控制符。對于早期的計(jì)算機(jī)程序來說,這些就足夠了,但是這也導(dǎo)致了世界上很多其他地區(qū)的用戶無法直接使用自己的符號(hào)系統(tǒng)。隨著互聯(lián)網(wǎng)的發(fā)展,混合多種語言的數(shù)據(jù)變得很常見(譯注:比如本身的英文原文或中文翻譯都包含了ASCII、中文、日文等多種語言字符)。如何有效處理這些包含了各種語言的豐富多樣的文本數(shù)據(jù)呢?
答案就是使用Unicode( http://unicode.org ),它收集了這個(gè)世界上所有的符號(hào)系統(tǒng),包括重音符號(hào)和其它變音符號(hào),制表符和回車符,還有很多神秘的符號(hào),每個(gè)符號(hào)都分配一個(gè)唯一的Unicode碼點(diǎn),Unicode碼點(diǎn)對應(yīng)Go語言中的rune整數(shù)類型(譯注:rune是int32等價(jià)類型)。
在第八版本的Unicode標(biāo)準(zhǔn)里收集了超過120,000個(gè)字符,涵蓋超過100多種語言。這些在計(jì)算機(jī)程序和數(shù)據(jù)中是如何體現(xiàn)的呢?通用的表示一個(gè)Unicode碼點(diǎn)的數(shù)據(jù)類型是int32,也就是Go語言中rune對應(yīng)的類型;它的同義詞rune符文正是這個(gè)意思。
我們可以將一個(gè)符文序列表示為一個(gè)int32序列。這種編碼方式叫UTF-32或UCS-4,每個(gè)Unicode碼點(diǎn)都使用同樣大小的32bit來表示。這種方式比較簡單統(tǒng)一,但是它會(huì)浪費(fèi)很多存儲(chǔ)空間,因?yàn)榇蠖鄶?shù)計(jì)算機(jī)可讀的文本是ASCII字符,本來每個(gè)ASCII字符只需要8bit或1字節(jié)就能表示。而且即使是常用的字符也遠(yuǎn)少于65,536個(gè),也就是說用16bit編碼方式就能表達(dá)常用字符。但是,還有其它更好的編碼方法嗎?
UTF8是一個(gè)將Unicode碼點(diǎn)編碼為字節(jié)序列的變長編碼。UTF8編碼是由Go語言之父Ken Thompson和Rob Pike共同發(fā)明的,現(xiàn)在已經(jīng)是Unicode的標(biāo)準(zhǔn)。UTF8編碼使用1到4個(gè)字節(jié)來表示每個(gè)Unicode碼點(diǎn),ASCII部分字符只使用1個(gè)字節(jié),常用字符部分使用2或3個(gè)字節(jié)表示。每個(gè)符號(hào)編碼后第一個(gè)字節(jié)的高端bit位用于表示編碼總共有多少個(gè)字節(jié)。如果第一個(gè)字節(jié)的高端bit為0,則表示對應(yīng)7bit的ASCII字符,ASCII字符每個(gè)字符依然是一個(gè)字節(jié),和傳統(tǒng)的ASCII編碼兼容。如果第一個(gè)字節(jié)的高端bit是110,則說明需要2個(gè)字節(jié);后續(xù)的每個(gè)高端bit都以10開頭。更大的Unicode碼點(diǎn)也是采用類似的策略處理。
0xxxxxxx runes 0-127 (ASCII)
110xxxxx 10xxxxxx 128-2047 (values <128 unused)
1110xxxx 10xxxxxx 10xxxxxx 2048-65535 (values <2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused)
變長的編碼無法直接通過索引來訪問第n個(gè)字符,但是UTF8編碼獲得了很多額外的優(yōu)點(diǎn)。首先UTF8編碼比較緊湊,完全兼容ASCII碼,并且可以自動(dòng)同步:它可以通過向前回朔最多3個(gè)字節(jié)就能確定當(dāng)前字符編碼的開始字節(jié)的位置。它也是一個(gè)前綴編碼,所以當(dāng)從左向右解碼時(shí)不會(huì)有任何歧義也并不需要向前查看(譯注:像GBK之類的編碼,如果不知道起點(diǎn)位置則可能會(huì)出現(xiàn)歧義)。沒有任何字符的編碼是其它字符編碼的子串,或是其它編碼序列的字串,因此搜索一個(gè)字符時(shí)只要搜索它的字節(jié)編碼序列即可,不用擔(dān)心前后的上下文會(huì)對搜索結(jié)果產(chǎn)生干擾。同時(shí)UTF8編碼的順序和Unicode碼點(diǎn)的順序一致,因此可以直接排序UTF8編碼序列。同時(shí)因?yàn)闆]有嵌入的NUL(0)字節(jié),可以很好地兼容那些使用NUL作為字符串結(jié)尾的編程語言。
Go語言的源文件采用UTF8編碼,并且Go語言處理UTF8編碼的文本也很出色。unicode包提供了諸多處理rune字符相關(guān)功能的函數(shù)(比如區(qū)分字母和數(shù)字,或者是字母的大寫和小寫轉(zhuǎn)換等),unicode/utf8包則提供了用于rune字符序列的UTF8編碼和解碼的功能。
有很多Unicode字符很難直接從鍵盤輸入,并且還有很多字符有著相似的結(jié)構(gòu);有一些甚至是不可見的字符(譯注:中文和日文就有很多相似但不同的字)。Go語言字符串面值中的Unicode轉(zhuǎn)義字符讓我們可以通過Unicode碼點(diǎn)輸入特殊的字符。有兩種形式:\uhhhh
對應(yīng)16bit的碼點(diǎn)值,\Uhhhhhhhh
對應(yīng)32bit的碼點(diǎn)值,其中h是一個(gè)十六進(jìn)制數(shù)字;一般很少需要使用32bit的形式。每一個(gè)對應(yīng)碼點(diǎn)的UTF8編碼。例如:下面的字母串面值都表示相同的值:
"世界"
"\xe4\xb8\x96\xe7\x95\x8c"
"\u4e16\u754c"
"\U00004e16\U0000754c"
上面三個(gè)轉(zhuǎn)義序列都為第一個(gè)字符串提供替代寫法,但是它們的值都是相同的。
Unicode轉(zhuǎn)義也可以使用在rune字符中。下面三個(gè)字符是等價(jià)的:
'世' '\u4e16' '\U00004e16'
對于小于256的碼點(diǎn)值可以寫在一個(gè)十六進(jìn)制轉(zhuǎn)義字節(jié)中,例如\x41
對應(yīng)字符'A',但是對于更大的碼點(diǎn)則必須使用\u
或\U
轉(zhuǎn)義形式。因此,\xe4\xb8\x96
并不是一個(gè)合法的rune字符,雖然這三個(gè)字節(jié)對應(yīng)一個(gè)有效的UTF8編碼的碼點(diǎn)。
得益于UTF8編碼優(yōu)良的設(shè)計(jì),諸多字符串操作都不需要解碼操作。我們可以不用解碼直接測試一個(gè)字符串是否是另一個(gè)字符串的前綴:
func HasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
或者是后綴測試:
func HasSuffix(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}
或者是包含子串測試:
func Contains(s, substr string) bool {
for i := 0; i < len(s); i++ {
if HasPrefix(s[i:], substr) {
return true
}
}
return false
}
對于UTF8編碼后文本的處理和原始的字節(jié)處理邏輯是一樣的。但是對應(yīng)很多其它編碼則并不是這樣的。(上面的函數(shù)都來自strings字符串處理包,真實(shí)的代碼包含了一個(gè)用哈希技術(shù)優(yōu)化的Contains 實(shí)現(xiàn)。)
另一方面,如果我們真的關(guān)心每個(gè)Unicode字符,我們可以使用其它處理方式??紤]前面的第一個(gè)例子中的字符串,它混合了中西兩種字符。圖3.5展示了它的內(nèi)存表示形式。字符串包含13個(gè)字節(jié),以UTF8形式編碼,但是只對應(yīng)9個(gè)Unicode字符:
import "unicode/utf8"
s := "Hello, 世界"
fmt.Println(len(s)) // "13"
fmt.Println(utf8.RuneCountInString(s)) // "9"
為了處理這些真實(shí)的字符,我們需要一個(gè)UTF8解碼器。unicode/utf8包提供了該功能,我們可以這樣使用:
for i := 0; i < len(s); {
r, size := utf8.DecodeRuneInString(s[i:])
fmt.Printf("%d\t%c\n", i, r)
i += size
}
每一次調(diào)用DecodeRuneInString函數(shù)都返回一個(gè)r和長度,r對應(yīng)字符本身,長度對應(yīng)r采用UTF8編碼后的編碼字節(jié)數(shù)目。長度可以用于更新第i個(gè)字符在字符串中的字節(jié)索引位置。但是這種編碼方式是笨拙的,我們需要更簡潔的語法。幸運(yùn)的是,Go語言的range循環(huán)在處理字符串的時(shí)候,會(huì)自動(dòng)隱式解碼UTF8字符串。下面的循環(huán)運(yùn)行如圖3.5所示;需要注意的是對于非ASCII,索引更新的步長將超過1個(gè)字節(jié)。
for i, r := range "Hello, 世界" {
fmt.Printf("%d\t%q\t%d\n", i, r, r)
}
我們可以使用一個(gè)簡單的循環(huán)來統(tǒng)計(jì)字符串中字符的數(shù)目,像這樣:
n := 0
for _, _ = range s {
n++
}
像其它形式的循環(huán)那樣,我們也可以忽略不需要的變量:
n := 0
for range s {
n++
}
或者我們可以直接調(diào)用utf8.RuneCountInString(s)函數(shù)。
正如我們前面提到的,文本字符串采用UTF8編碼只是一種慣例,但是對于循環(huán)的真正字符串并不是一個(gè)慣例,這是正確的。如果用于循環(huán)的字符串只是一個(gè)普通的二進(jìn)制數(shù)據(jù),或者是含有錯(cuò)誤編碼的UTF8數(shù)據(jù),將會(huì)發(fā)生什么呢?
每一個(gè)UTF8字符解碼,不管是顯式地調(diào)用utf8.DecodeRuneInString解碼或是在range循環(huán)中隱式地解碼,如果遇到一個(gè)錯(cuò)誤的UTF8編碼輸入,將生成一個(gè)特別的Unicode字符\uFFFD
,在印刷中這個(gè)符號(hào)通常是一個(gè)黑色六角或鉆石形狀,里面包含一個(gè)白色的問號(hào)"?"。當(dāng)程序遇到這樣的一個(gè)字符,通常是一個(gè)危險(xiǎn)信號(hào),說明輸入并不是一個(gè)完美沒有錯(cuò)誤的UTF8字符串。
UTF8字符串作為交換格式是非常方便的,但是在程序內(nèi)部采用rune序列可能更方便,因?yàn)閞une大小一致,支持?jǐn)?shù)組索引和方便切割。
將[]rune類型轉(zhuǎn)換應(yīng)用到UTF8編碼的字符串,將返回字符串編碼的Unicode碼點(diǎn)序列:
// "program" in Japanese katakana
s := "プログラム"
fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
r := []rune(s)
fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]"
(在第一個(gè)Printf中的% x
參數(shù)用于在每個(gè)十六進(jìn)制數(shù)字前插入一個(gè)空格。)
如果是將一個(gè)[]rune類型的Unicode字符slice或數(shù)組轉(zhuǎn)為string,則對它們進(jìn)行UTF8編碼:
fmt.Println(string(r)) // "プログラム"
將一個(gè)整數(shù)轉(zhuǎn)型為字符串意思是生成以只包含對應(yīng)Unicode碼點(diǎn)字符的UTF8字符串:
fmt.Println(string(65)) // "A", not "65"
fmt.Println(string(0x4eac)) // "京"
如果對應(yīng)碼點(diǎn)的字符是無效的,則用\uFFFD
無效字符作為替換:
fmt.Println(string(1234567)) // "?"
標(biāo)準(zhǔn)庫中有四個(gè)包對字符串處理尤為重要:bytes、strings、strconv和unicode包。strings包提供了許多如字符串的查詢、替換、比較、截?cái)唷⒉鸱趾秃喜⒌裙δ堋?
bytes包也提供了很多類似功能的函數(shù),但是針對和字符串有著相同結(jié)構(gòu)的[]byte類型。因?yàn)樽址侵蛔x的,因此逐步構(gòu)建字符串會(huì)導(dǎo)致很多分配和復(fù)制。在這種情況下,使用bytes.Buffer類型將會(huì)更有效,稍后我們將展示。
strconv包提供了布爾型、整型數(shù)、浮點(diǎn)數(shù)和對應(yīng)字符串的相互轉(zhuǎn)換,還提供了雙引號(hào)轉(zhuǎn)義相關(guān)的轉(zhuǎn)換。
unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等類似功能,它們用于給字符分類。每個(gè)函數(shù)有一個(gè)單一的rune類型的參數(shù),然后返回一個(gè)布爾值。而像ToUpper和ToLower之類的轉(zhuǎn)換函數(shù)將用于rune字符的大小寫轉(zhuǎn)換。所有的這些函數(shù)都是遵循Unicode標(biāo)準(zhǔn)定義的字母、數(shù)字等分類規(guī)范。strings包也有類似的函數(shù),它們是ToUpper和ToLower,將原始字符串的每個(gè)字符都做相應(yīng)的轉(zhuǎn)換,然后返回新的字符串。
下面例子的basename函數(shù)靈感源于Unix shell的同名工具。在我們實(shí)現(xiàn)的版本中,basename(s)將看起來像是系統(tǒng)路徑的前綴刪除,同時(shí)將看似文件類型的后綴名部分刪除:
fmt.Println(basename("a/b/c.go")) // "c"
fmt.Println(basename("c.d.go")) // "c.d"
fmt.Println(basename("abc")) // "abc"
第一個(gè)版本并沒有使用任何庫,全部手工硬編碼實(shí)現(xiàn):
gopl.io/ch3/basename1
// basename removes directory components and a .suffix.
// e.g., a => a, a.go => a, a/b/c.go => c, a/b.c.go => b.c
func basename(s string) string {
// Discard last '/' and everything before.
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '/' {
s = s[i+1:]
break
}
}
// Preserve everything before last '.'.
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '.' {
s = s[:i]
break
}
}
return s
}
這個(gè)簡化版本使用了strings.LastIndex庫函數(shù):
gopl.io/ch3/basename2
func basename(s string) string {
slash := strings.LastIndex(s, "/") // -1 if "/" not found
s = s[slash+1:]
if dot := strings.LastIndex(s, "."); dot >= 0 {
s = s[:dot]
}
return s
}
path和path/filepath包提供了關(guān)于文件路徑名更一般的函數(shù)操作。使用斜杠分隔路徑可以在任何操作系統(tǒng)上工作。斜杠本身不應(yīng)該用于文件名,但是在其他一些領(lǐng)域可能會(huì)用于文件名,例如URL路徑組件。相比之下,path/filepath包則使用操作系統(tǒng)本身的路徑規(guī)則,例如POSIX系統(tǒng)使用/foo/bar,而Microsoft Windows使用c:\foo\bar
等。
讓我們繼續(xù)另一個(gè)字符串的例子。函數(shù)的功能是將一個(gè)表示整數(shù)值的字符串,每隔三個(gè)字符插入一個(gè)逗號(hào)分隔符,例如“12345”處理后成為“12,345”。這個(gè)版本只適用于整數(shù)類型;支持浮點(diǎn)數(shù)類型的留作練習(xí)。
gopl.io/ch3/comma
// comma inserts commas in a non-negative decimal integer string.
func comma(s string) string {
n := len(s)
if n <= 3 {
return s
}
return comma(s[:n-3]) + "," + s[n-3:]
}
輸入comma函數(shù)的參數(shù)是一個(gè)字符串。如果輸入字符串的長度小于或等于3的話,則不需要插入逗號(hào)分隔符。否則,comma函數(shù)將在最后三個(gè)字符前的位置將字符串切割為兩個(gè)子串并插入逗號(hào)分隔符,然后通過遞歸調(diào)用自身來得出前面的子串。
一個(gè)字符串是包含只讀字節(jié)的數(shù)組,一旦創(chuàng)建,是不可變的。相比之下,一個(gè)字節(jié)slice的元素則可以自由地修改。
字符串和字節(jié)slice之間可以相互轉(zhuǎn)換:
s := "abc"
b := []byte(s)
s2 := string(b)
從概念上講,一個(gè)[]byte(s)轉(zhuǎn)換是分配了一個(gè)新的字節(jié)數(shù)組用于保存字符串?dāng)?shù)據(jù)的拷貝,然后引用這個(gè)底層的字節(jié)數(shù)組。編譯器的優(yōu)化可以避免在一些場景下分配和復(fù)制字符串?dāng)?shù)據(jù),但總的來說需要確保在變量b被修改的情況下,原始的s字符串也不會(huì)改變。將一個(gè)字節(jié)slice轉(zhuǎn)換到字符串的string(b)操作則是構(gòu)造一個(gè)字符串拷貝,以確保s2字符串是只讀的。
為了避免轉(zhuǎn)換中不必要的內(nèi)存分配,bytes包和strings同時(shí)提供了許多實(shí)用函數(shù)。下面是strings包中的六個(gè)函數(shù):
func Contains(s, substr string) bool
func Count(s, sep string) int
func Fields(s string) []string
func HasPrefix(s, prefix string) bool
func Index(s, sep string) int
func Join(a []string, sep string) string
bytes包中也對應(yīng)的六個(gè)函數(shù):
func Contains(b, subslice []byte) bool
func Count(s, sep []byte) int
func Fields(s []byte) [][]byte
func HasPrefix(s, prefix []byte) bool
func Index(s, sep []byte) int
func Join(s [][]byte, sep []byte) []byte
它們之間唯一的區(qū)別是字符串類型參數(shù)被替換成了字節(jié)slice類型的參數(shù)。
bytes包還提供了Buffer類型用于字節(jié)slice的緩存。一個(gè)Buffer開始是空的,但是隨著string、byte或[]byte等類型數(shù)據(jù)的寫入可以動(dòng)態(tài)增長,一個(gè)bytes.Buffer變量并不需要初始化,因?yàn)榱阒狄彩怯行У模?
gopl.io/ch3/printints
// intsToString is like fmt.Sprint(values) but adds commas.
func intsToString(values []int) string {
var buf bytes.Buffer
buf.WriteByte('[')
for i, v := range values {
if i > 0 {
buf.WriteString(", ")
}
fmt.Fprintf(&buf, "%d", v)
}
buf.WriteByte(']')
return buf.String()
}
func main() {
fmt.Println(intsToString([]int{1, 2, 3})) // "[1, 2, 3]"
}
當(dāng)向bytes.Buffer添加任意字符的UTF8編碼時(shí),最好使用bytes.Buffer的WriteRune方法,但是WriteByte方法對于寫入類似'['和']'等ASCII字符則會(huì)更加有效。
bytes.Buffer類型有著很多實(shí)用的功能,我們在第七章討論接口時(shí)將會(huì)涉及到,我們將看看如何將它用作一個(gè)I/O的輸入和輸出對象,例如當(dāng)做Fprintf的io.Writer輸出對象,或者當(dāng)作io.Reader類型的輸入源對象。
練習(xí) 3.10: 編寫一個(gè)非遞歸版本的comma函數(shù),使用bytes.Buffer代替字符串鏈接操作。
練習(xí) 3.11: 完善comma函數(shù),以支持浮點(diǎn)數(shù)處理和一個(gè)可選的正負(fù)號(hào)的處理。
練習(xí) 3.12: 編寫一個(gè)函數(shù),判斷兩個(gè)字符串是否是相互打亂的,也就是說它們有著相同的字符,但是對應(yīng)不同的順序。
除了字符串、字符、字節(jié)之間的轉(zhuǎn)換,字符串和數(shù)值之間的轉(zhuǎn)換也比較常見。由strconv包提供這類轉(zhuǎn)換功能。
將一個(gè)整數(shù)轉(zhuǎn)為字符串,一種方法是用fmt.Sprintf返回一個(gè)格式化的字符串;另一個(gè)方法是用strconv.Itoa(“整數(shù)到ASCII”):
x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"
FormatInt和FormatUint函數(shù)可以用不同的進(jìn)制來格式化數(shù)字:
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
fmt.Printf函數(shù)的%b、%d、%o和%x等參數(shù)提供功能往往比strconv包的Format函數(shù)方便很多,特別是在需要包含有附加額外信息的時(shí)候:
s := fmt.Sprintf("x=%b", x) // "x=1111011"
如果要將一個(gè)字符串解析為整數(shù),可以使用strconv包的Atoi或ParseInt函數(shù),還有用于解析無符號(hào)整數(shù)的ParseUint函數(shù):
x, err := strconv.Atoi("123") // x is an int
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits
ParseInt函數(shù)的第三個(gè)參數(shù)是用于指定整型數(shù)的大小;例如16表示int16,0則表示int。在任何情況下,返回的結(jié)果y總是int64類型,你可以通過強(qiáng)制類型轉(zhuǎn)換將它轉(zhuǎn)為更小的整數(shù)類型。
有時(shí)候也會(huì)使用fmt.Scanf來解析輸入的字符串和數(shù)字,特別是當(dāng)字符串和數(shù)字混合在一行的時(shí)候,它可以靈活處理不完整或不規(guī)則的輸入。
![]() | ![]() |
更多建議: