Go 語言 字符串

2023-03-14 16:52 更新

原文鏈接:https://gopl-zh.github.io/ch3/ch3-05.html


3.5. 字符串

一個(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ù)。

3.5.1. 字符串面值

字符串值也可以用字符串面值方式編寫,只要將一系列字節(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]
...`

3.5.2. Unicode

在很久以前,世界還是比較簡單的,起碼計(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á)常用字符。但是,還有其它更好的編碼方法嗎?

3.5.3. UTF-8

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)) // "?"

3.5.4. 字符串和Byte切片

標(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)不同的順序。

3.5.5. 字符串和數(shù)字的轉(zhuǎn)換

除了字符串、字符、字節(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ī)則的輸入。



以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)