溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Go36-36,37-字符串

發(fā)布時(shí)間:2020-05-22 03:14:43 來源:網(wǎng)絡(luò) 閱讀:251 作者:騎士救兵 欄目:編程語言

unicode與字符編碼

字符編碼的問題,是計(jì)算機(jī)領(lǐng)域中非?;A(chǔ)的一個(gè)問題。

Unicode編碼

Go語言中的標(biāo)識符可以包含任何Unicode編碼可以表示的字母字符??梢灾苯影岩粋€(gè)整數(shù)數(shù)值轉(zhuǎn)換為一個(gè)string類型的值。被轉(zhuǎn)換的整數(shù)值應(yīng)該是一個(gè)有效的Unicode碼點(diǎn),否則會顯示為一個(gè)“?”字符:

package main

import "fmt"

func main() {
    s1 := '你'  // 這是一個(gè)字符類型,不是字符串
    fmt.Println(int(s1))  // 字符“你”轉(zhuǎn)為整數(shù)是20320
    s2 := rune(20320)
    fmt.Println(string(s2))
    s3 := rune(-1)  // 不用費(fèi)心找一個(gè)不存在的Unicode碼點(diǎn),用-1就好
    fmt.Println(string(s3))  // 不存在的碼點(diǎn)顯示的效果
}

當(dāng)一個(gè)string類型的值被轉(zhuǎn)換為[]rune類型值的時(shí)候,其中的字符串會被拆分成一個(gè)一個(gè)的Unicode字符:

func main() {
    s := "你好,世界!"
    r := []rune(s)
    fmt.Println(r)
    for _, c := range(r) {
        fmt.Printf("%c ", c)
    }
    fmt.Println()
}

Go語言采用的字符編碼方案從屬于Unicode編碼規(guī)范。更準(zhǔn)確的說,Go語言的代碼正是由Unicode字符組成的。所有源代碼,都必須按照Unicode編碼規(guī)范這的UTF-8編碼格式進(jìn)行編碼。
Go語言的源碼文件必須使用UTF-8編碼格式進(jìn)行存儲。如果源碼中出現(xiàn)了非UTF-8編碼的字符,那么在構(gòu)建、安裝以及運(yùn)行的時(shí)候,Go命令就會報(bào)告錯(cuò)誤“illegal UTF-8 encoding”。

Unicode編碼規(guī)范

在計(jì)算機(jī)系統(tǒng)的內(nèi)部,抽象的字符會被編碼為整數(shù)。這些整數(shù)的范圍被稱為代碼空間。在代碼空間之內(nèi),每一個(gè)特定的整數(shù)都被稱為一個(gè)碼點(diǎn)。一個(gè)受支持的抽象字符會被映射并分配給某個(gè)特定的碼點(diǎn)。反過來,一個(gè)碼點(diǎn)總是可以看成一個(gè)被編碼的字符。
Unicode編碼規(guī)范通常使用16進(jìn)制表示法來表示Unicode碼點(diǎn)的整數(shù)數(shù)值,并使用“U+”作為前綴。比如,字母a的Unicode碼點(diǎn)是U+0061。在Unicode編碼規(guī)范中,一個(gè)字符能且只能與它對應(yīng)的那個(gè)碼點(diǎn)表示。

UTF-8

Unicode編碼規(guī)范提供了3種不同的編碼格式:

  • UTF-8
  • UTF-16
  • UTF-32

上面的名稱中,右邊的整數(shù)是有含義的。就是以多少個(gè)比特位作為一個(gè)編碼單元。以UTF-8為例,它會以8個(gè)比特位,就是1個(gè)字節(jié)作為一個(gè)編碼單元。并且,它與標(biāo)準(zhǔn)的ASCII編碼是完全兼容的。在[0x00, 0x7f]的范圍內(nèi),這兩種編碼表示的字符是相同的。
UTF-8是一種可變寬的編碼方案。它會用一個(gè)或多個(gè)字節(jié)的二進(jìn)制數(shù)來表示某個(gè)字符,最多使用4個(gè)字節(jié)。比如,一個(gè)英文字符,僅占用1個(gè)字節(jié),而一個(gè)中文字符,占用3個(gè)字節(jié)。不論怎樣,一個(gè)受支持的字符總是可以用UTF-8進(jìn)行編碼,成為一個(gè)字節(jié)序列。

Go語言中的運(yùn)行

在底層,一個(gè)string類型的值,是由一系列相對應(yīng)的Unicode碼點(diǎn)的UTF-8編碼值來表達(dá)的。
在Go語言中,一個(gè)string類型的值既可以被拆分為一個(gè)包含多個(gè)字符的序列([]runc 類型),也可以被拆分為一個(gè)包含多個(gè)字節(jié)的序列([]byte 類型)。
rune是Go語言特有的一個(gè)基本數(shù)據(jù)類型,它的一個(gè)值就代碼一個(gè)字符,即:Unicode字符。UTF-8編碼方案會把一個(gè)Unicode字符編碼為一個(gè)長度在[1,4]范圍內(nèi)的字節(jié)序列。所以,一個(gè)rune類型的值也可以由一個(gè)或多個(gè)字節(jié)來代表。下面是rune類型的聲明:

type rune = int32

rune類型實(shí)際上是int32類型的一個(gè)別名類型。一個(gè)rune類型的值會由4個(gè)字節(jié)寬度的空間來存儲。一個(gè)rune類型的值在底層就是一個(gè)UTF-8編碼值。
把一個(gè)字符串轉(zhuǎn)換為[]rune類型的話,不論是英文占1個(gè)字節(jié)還是中文占3個(gè)字節(jié),其中每一個(gè)字符,都會獨(dú)立成為一個(gè)rune類型的元素值:

str := "你好,世界! This is Golang."
fmt.Printf("%q\n", []rune(str))

而每個(gè)rune類型的值在底層都是由一個(gè)UTF-8編碼值來表達(dá)的,所以可以換一種方式展示為整數(shù)的序列:

fmt.Printf("%x\n", []rune(str))

還可以再進(jìn)一步的拆分,拆分為字節(jié)序列:

fmt.Printf("[% x]\n", []byte(str))

字節(jié)切片中,英文字符的值和上面的字符切片里是一樣的。都是一個(gè)字節(jié)來表示。
而中文字符占字節(jié)切片中的3個(gè)元素,在字符切片中占1個(gè)元素。以中文字符“你”為例,UTF-8編碼的整數(shù)為0x4f60,就是10進(jìn)制的20320,而在字節(jié)切片中是3個(gè)數(shù):e4、bd、a0。

UTF-8與Unicode的轉(zhuǎn)換

UTF-8是由1至4個(gè)字節(jié)表示,是變長的。在編碼的時(shí)候,第一個(gè)字節(jié)的高位指明了后面還有多少個(gè)字節(jié):

  • 0xxxxxxx, 0開頭,表示后面沒有別的字節(jié),能表示0-127這些字符,就是ASCII字符。
  • 110xxxxx 10xxxxx,110開頭,表示一共2個(gè)字節(jié),后面的字節(jié)都是是10開頭。能表示128-2047的碼點(diǎn)。
  • 1110xxxx 10xxxxxx 10xxxxxx,1110開頭,表示一共3個(gè)字節(jié),能表示2048-65536的碼點(diǎn)。
  • 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx,11110開頭,表示一個(gè)4個(gè)字節(jié),能表示65536-0x10ffff的碼點(diǎn)。

分析一下“你”這個(gè)中文字。UTF-8是0x4f60,就是:
0100 1111 0110 0000
把上面的二進(jìn)制位替換掉1110xxxx 10xxxxxx 10xxxxxx里的x:
11100100 10111101 10100000 就是 e4 bd a0

遍歷字符串

使用range遍歷字符串的時(shí)候,會先把字符串拆成一個(gè)字節(jié)序列,然后再試圖找出每個(gè)字節(jié)對應(yīng)的Unicode字符。用for range迭代的時(shí)候可以返回2個(gè)變量,第一個(gè)是索引值,第二個(gè)就是字符,類型是rune:

func main() {
    s := "Hi 世界"
    for i, c := range(s) {
        fmt.Printf("%d: %q\t[% x]\n", i, c, []byte(string(c)))
    }
}

/* 執(zhí)行結(jié)果
PS G:\Steed\Documents\Go\src\Go36\article36\example04> go run main.go
0: 'H'  [48]
1: 'i'  [69]
2: ' '  [20]
3: '世' [e4 b8 96]
6: '界' [e7 95 8c]
PS G:\Steed\Documents\Go\src\Go36\article36\example04>
*/

這里要注意一下執(zhí)行后的結(jié)果,主要是返回的第一個(gè)變量也就是下標(biāo),或者叫索引值。索引值不是每次都加1的,英文中文字符占3個(gè)字節(jié),所以中文字符后的下一個(gè)索引值是加3的。
這樣的for range可以逐一迭代出字符串里的每一個(gè)Unicode字符。但是相鄰的Unicode字符的索引值并不一定是連續(xù)的。這取決于前一個(gè)Unicode字符的寬度。如果想要得到其中某個(gè)Unicode字符對應(yīng)的UTF-8編碼的寬度,可以不用去了解上面的UTF-8與Unicode的轉(zhuǎn)換的編碼格式。而是可以把下一個(gè)字符的索引值減去當(dāng)前字符的索引值就算好了。

unicode包介紹

標(biāo)準(zhǔn)庫中的unicode包及其子包,提供了很多的函數(shù)和數(shù)據(jù)類型,可以解析各種內(nèi)容中的Unicode字符。這些程序?qū)嶓w都很好用,也都很簡單明了,而且有效的隱藏了Unicode編碼規(guī)范中的一些復(fù)雜的細(xì)節(jié)。不過這部分只是提了一下,沒有展開,也沒有進(jìn)行講解。
另外去找了幾個(gè)unicode包使用的示例,放這里充實(shí)點(diǎn)內(nèi)容。
統(tǒng)計(jì)字符數(shù):

func main() {
    s := "Hi 世界"  // 3個(gè)ASCII字符,2個(gè)中文字符
    fmt.Println(len(s))  // 9
    fmt.Println(utf8.RuneCountInString(s))  // 5
}

返回字符串第一個(gè)字符的編碼和寬度:

func main() {
    s := "Hi 世界"
    for i := 0; i < len(s); {
        r, size := utf8.DecodeRuneInString(s[i:])
        fmt.Printf("%d %c\n", i, r)
        i += size
    }
}

因?yàn)镚o的for range本身就可以直接遍歷Unicode字符,所以其實(shí)要處理字符也不需要借助編碼工具,用好for range就也是可以的:

func main() {
    s := "Hi 世界"  // 3個(gè)ASCII字符,2個(gè)中文字符

    var n uint
    for range s {
        n++
    }
    fmt.Println(n)

    for i, r := range s {
        fmt.Printf("%d\t%c\t%d\n", i, r, len(string(r)))
    }
}

這個(gè)是真正的字?jǐn)?shù)統(tǒng)計(jì)了,用了unicode包里的一個(gè)函數(shù),排除非文字的字符,主要是會有標(biāo)點(diǎn)符號的干擾:

func main() {
    s := "Make the plan. Execute the plan. Expect the plan to go off the rails. Throw away the plan."
    var n uint
    for _, r := range s {
        if unicode.IsLetter(r) {
            n++
        }
    }
    fmt.Println(n)
}

strings包與字符串操作

標(biāo)準(zhǔn)庫中的strings代碼包,在這個(gè)包里用到了不少unicode包和unicode/utf8包中的程序?qū)嶓w。比如,strings.Builder類型的WriteRune方法,strings.Reader類型的ReadRune方法,等等。

string類型

原生的string類型的值出不可變的。如果要獲得一個(gè)不一樣的字符串,就需要生成一個(gè)新的字符串。在底層,string值的內(nèi)容會被存儲到一塊連續(xù)的內(nèi)存空間。同時(shí),這塊內(nèi)存容納的字節(jié)數(shù)量也被記錄下來了,并用于表示string值的長度。
在進(jìn)行字符串拼接的時(shí)候,Go語言會把所有被拼接的字符串一次拷貝到一個(gè)嶄新且足夠大的連續(xù)內(nèi)存空間中,并把持有相應(yīng)指針值的string值作為結(jié)果返回。當(dāng)程序中存在過多的字符串拼接操作的時(shí)候,會對內(nèi)存的分配產(chǎn)生非常大的壓力。雖然string值在內(nèi)部持有一個(gè)指針值,但其類型仍然屬于值類型。不過,由于string值的不可變,其中的指針值也為內(nèi)存空間的節(jié)省做出了貢獻(xiàn)。就是string值會在底層與它所有的副本共用同一個(gè)字節(jié)數(shù)組。不過,由于string值的不可變,所以這樣做是絕對安全的。

strings.Builder類型

strings.Builder是1.10加入strings包中的新類型。如果是舊版本就沒有了。
Golang貌似不支持升級,所以需要卸載,然后安裝新版本。
與string的值相比,strings.Builder類型的值有以下3個(gè)優(yōu)勢:

  • 已存在的內(nèi)容不可變,但可以拼接更多的內(nèi)容
  • 減少內(nèi)存分配和內(nèi)容拷貝的次數(shù)
  • 可將內(nèi)容重置,可重用值

比較string
與string值相比,Builder值的優(yōu)勢主要體現(xiàn)在字符串拼接方面。Builder值中有一個(gè)用于承載內(nèi)容的容器,內(nèi)容容器。它是一個(gè)以byte為元素類型的切片,字節(jié)切片
字節(jié)切片的底層數(shù)據(jù)就是一個(gè)字節(jié)數(shù)組,它與string值存儲內(nèi)容的方式是一樣的。實(shí)際上,它們都是通過一個(gè)unsafe.Pointer類型的字段來持有那個(gè)指向了底層字節(jié)數(shù)組的指針值的。因?yàn)橛羞@樣一樣的構(gòu)造,使得Builder值擁有同樣高效利用內(nèi)存的前提條件。雖然對于字節(jié)切片本身來說,它包含的任何元素值都可以被修改,但是Builder值并不允許這樣做,其中的內(nèi)容只能夠進(jìn)行拼接或者完全被重置。

拼接方法
這樣,已經(jīng)存在的Builder值中的內(nèi)容是不可變的。利用Builder值提供的方法拼接更多的內(nèi)容時(shí)就不用擔(dān)心這些方法會影響到已存在的內(nèi)容。這里所說的方法就是Builder值擁有的一系列指針方法,或者統(tǒng)稱為拼接方法

  • Write
  • WriteByte
  • WriteRune
  • WriteString

拼接方法的示例代碼:

package main

import (
    "fmt"
    "strings"
)

func main() {
    var b1 strings.Builder
    b1.WriteString("Make The Plan.")
    fmt.Println(b1)
    fmt.Println(b1.Len(), b1.String())
    b1.WriteByte(' ')
    b1.WriteString("Execute the plan")
    b1.Write([]byte{'.', ' '})
    s := "Expect the plan to go off the rails."
    for _, r := range s {
        b1.WriteRune(r)
    }
    fmt.Println(b1.Len(), b1.String())
    b1.WriteByte(' ')
    s = "Throw away the plan."
    for _, c := range []byte(s) {
        b1.WriteByte(c)
    }
    fmt.Println(b1.Len(), b1.String())
}

Builder擴(kuò)容

利用上面這些方法,就可以把新的內(nèi)容拼接到已存在的內(nèi)容的尾部。如果需要,Builder值會自動的對自身的內(nèi)容容器進(jìn)行擴(kuò)容。這里的自動擴(kuò)容策略與切片的擴(kuò)容策略一致。
除了Builder值的自動擴(kuò)容,還可以選擇手動擴(kuò)容,這通過調(diào)用Builder值的Grow方法實(shí)現(xiàn)。Grow方法也可以稱為擴(kuò)容方法,它接受一個(gè)int類型的參數(shù)n,參數(shù)表示將要擴(kuò)充的字節(jié)數(shù)量。Grow方法會把內(nèi)容容器的容量增加n個(gè)字節(jié)。就是生成一個(gè)字節(jié)切片作為新的內(nèi)容容器,切片的容量會是原容器容量的2倍再加上n。之后。把原容器中的所有字節(jié)全部拷貝到新容器中。文字描述不如看一下源碼更清楚:

func (b *Builder) grow(n int) {
    buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
    copy(buf, b.buf)
    b.buf = buf
}

即使是手動調(diào)用的Grow方法,也可能什么都不做,這個(gè)還是從源碼里看吧:

func (b *Builder) Grow(n int) {
    b.copyCheck()
    if n < 0 {
        panic("strings.Builder.Grow: negative count")
    }
    if cap(b.buf)-len(b.buf) < n {
        b.grow(n)
    }
}

就是擴(kuò)容前會檢查當(dāng)前容量夠不夠,如果當(dāng)前有足夠的容量就不做擴(kuò)容了。

調(diào)用手動擴(kuò)容的場景
如果只是拼接一次數(shù)據(jù),直接進(jìn)行拼接就好了,不需要手動進(jìn)行擴(kuò)容。如果容量不夠,那么自動擴(kuò)容也是一樣的。
在需要多次拼接大量的數(shù)據(jù)之前,先進(jìn)行手動擴(kuò)容就可以達(dá)到提高性能的效果。如果自動擴(kuò)容,多次拼接的過程中,就會有多次的擴(kuò)容操作。而每次擴(kuò)容操作相對來說都是代價(jià)昂貴的。如果提前就把之后需要的空間準(zhǔn)備好,只進(jìn)行一次擴(kuò)容,減少了擴(kuò)容操作的次數(shù),應(yīng)該是會提高性能的。這里應(yīng)該還可以做一個(gè)性能測試,直觀的看到效果。

調(diào)用擴(kuò)容方法
調(diào)用擴(kuò)容方法很簡單,本想再觀察一下擴(kuò)容前后的效果的,可是封裝的太好,沒有方便的手段查看。關(guān)于Grow方法的效果,關(guān)鍵變量都是私有的,并且包也沒有提供相關(guān)的方法,就看不到效果了:

func main() {
    var b1 strings.Builder
    b1.WriteString("你好")
    fmt.Println(b1.Len(), b1.String())
    b1.Grow(10)
    fmt.Println(b1.Len(), b1.String())
}

strings.Builder類型的Len方法,源碼中是這樣的:

type Builder struct {
    addr *Builder // of receiver, to detect copies by value
    buf  []byte
}

func (b *Builder) Len() int { return len(b.buf) }

Len方法返回的就是buf這個(gè)切片的長度,Grow方法的擴(kuò)容就是對buf切片的擴(kuò)容,檢驗(yàn)的方法需要查看buf切片的容量就是cap(b.buf)。字段不可導(dǎo)出,也沒有提供相應(yīng)的方法,就不深究了。

Builder重用

還有一個(gè)Reset方法,可以讓Builder值重新回到零值狀態(tài),就好像從未被使用過那樣。Reset之后,Builder值中的內(nèi)容會被直接丟棄。之后會被Go語言的垃圾回收器標(biāo)記并回收掉。下面是Reset方法的源碼:

func (b *Builder) Reset() {
    b.addr = nil
    b.buf = nil
}

全部字段設(shè)為零值,就是創(chuàng)建結(jié)構(gòu)體時(shí)的狀態(tài)。所以如果要使用一個(gè)Builder,新創(chuàng)建一個(gè)和重用一個(gè),獲得的Builder都是一樣的。重用的時(shí)候會把之前的內(nèi)容都丟棄掉,釋放了內(nèi)存資源。

復(fù)制檢查copyCheck

Builder在被真正使用后,就不可再被復(fù)制了。
只要調(diào)用了Builder值的拼接方法或擴(kuò)容方法,就意味著真正開始使用它了。一旦調(diào)用了它們,就不能再以任何的方式對其所屬值進(jìn)行復(fù)制。否則只要在任何副本上調(diào)用上述方法,就會引發(fā)panic。在源碼里,這些都是通過一個(gè)copyCheck方法來實(shí)現(xiàn)的:

func (b *Builder) copyCheck() {
    if b.addr == nil {
        b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
    } else if b.addr != b {
        panic("strings: illegal use of non-zero Builder copied by value")
    }
}

在執(zhí)行copuCheck方法后,如果此時(shí)Builder還沒有分配地址,就會設(shè)置一個(gè)地址了。此時(shí)就是真正被使用了。
如果有地址,就會和addr字段進(jìn)行比較。addr字段里存的就是結(jié)構(gòu)體本身的指針地址,copyCheck方法是個(gè)指針方法,本身也是指針,就是比較兩個(gè)指針是否一樣,不過不一樣,就引發(fā)panic。
copyCheck方法會在所有的4個(gè)拼接方法以及擴(kuò)容方法里執(zhí)行。這幾個(gè)方法都是會改變Builder里的內(nèi)容的,擴(kuò)容方法看似不改變內(nèi)容,但是會對buf字段執(zhí)行copy,拷貝到新的內(nèi)存區(qū)域,拷貝前后引用的位置是不同的。如果此時(shí)調(diào)用的方法的對象是一個(gè)副本,就會在檢查指針的時(shí)候引發(fā)panic。
不能復(fù)制是因?yàn)椴荒苁褂酶北菊{(diào)用以上這些方法,而本質(zhì)就是Builder的內(nèi)存地址不能變,會產(chǎn)生這種情況的復(fù)制行為包括但不限于下面這些:

  • 函數(shù)間傳遞值
  • 通過通道傳遞值
  • 把值賦值給變量

這種約束還是有好處的,這樣肯定不會出現(xiàn)多個(gè)Builder值中的內(nèi)容容器,就是buf字段的字節(jié)切面,共用一個(gè)底層數(shù)據(jù)的情況。這樣也就避免了多個(gè)同源的Builder值在拼接內(nèi)容時(shí)可能產(chǎn)生的沖突問題。
從本質(zhì)上看,也不是不能復(fù)制。副本是可以產(chǎn)生的,只有在對副本調(diào)用擴(kuò)容方法和拼接方法的時(shí)候才會引發(fā)panic。
可以把聲明后還沒用過的Builder值,或者是Reset后的Builder值,將它的副本傳到各處。似乎先賦值出去再Reset也是可以的,至少是不會引發(fā)panic,不過會比傳遞空值多復(fù)制2個(gè)指針。另外副本還是可以調(diào)用Len方法和String方法的,包括Reset方法,這些都不會改變原Builder值的內(nèi)容。不過似乎也沒什么用,需要的話,只要復(fù)制一份String方法的結(jié)果保存就可以了。下面試一下復(fù)制后調(diào)用String方法:

func main() {
    var b1 strings.Builder
    b1.WriteString("Test Copy 1")
    b1.Grow(100)  // 消除擴(kuò)容時(shí)copy的情況對底層數(shù)組的影響
    b2 := b1
    fmt.Println(b1.Len(), b1.String())
    fmt.Println(b2.Len(), b2.String())
    b1.WriteString(" 再增加點(diǎn)內(nèi)容")  // 不會對副本的內(nèi)容產(chǎn)生影響
    fmt.Println(b1.Len(), b1.String())
    fmt.Println(b2.Len(), b2.String())  // 副本的內(nèi)容還是原樣
}

副本的內(nèi)容容器里的內(nèi)容不會跟著原Builder而改變。這是一個(gè)切片,不考慮擴(kuò)容的情況,其實(shí)副本和原值還是同一個(gè)底層數(shù)組,但是副本對底層數(shù)組的引用范圍沒變,而且已經(jīng)被引用的這些內(nèi)容是不允許改變的。再考慮到擴(kuò)容的情況,也不可能讓副本感知到原來的內(nèi)容的變化。

并發(fā)沖突

由于其內(nèi)容不是完全不可變的,所以需要調(diào)用方自行解決操作沖突和并發(fā)安全問題。
雖然Builder值不能被復(fù)制,但它的指針值是可以的。無論什么時(shí)候,都可以通過任何方式復(fù)制這樣的指針值。只要記住,這樣的指針值都會是同一個(gè)Builder值。這時(shí)又會產(chǎn)生一個(gè)新問題,Builder值被多方同時(shí)操作,就會有操作沖突和并發(fā)安全問題。
Builder值自己是無法解決問題的。在傳遞其指針共享Builder值的時(shí)候,一定要確保各方對它的使用時(shí)正確、有序的,并且是并發(fā)安全的。最好還是不要共享Builder值以及它的指針值。雖然可以通過某些方法實(shí)現(xiàn)共享Builder值,但是最好不要這么用。

strings.Reader類型

與strings.Builder類型相反,strings.Reader類型是為了高效讀取字符串而存在的。高效主要體現(xiàn)在它對字符串的讀取機(jī)制上,它封裝了很多用于在string值上讀取內(nèi)容的最佳實(shí)踐。
通過Reader值,可以方便地讀取一個(gè)字符串中的內(nèi)容。在讀取過程中,Reader值會保存已讀取的字節(jié)的計(jì)數(shù),就是已讀計(jì)數(shù)。已讀計(jì)數(shù)也代表著下一次讀取的起始索引位置。Reader值正是依靠這樣的一個(gè)計(jì)數(shù),以及針對字符串的切片表達(dá)式,從而實(shí)現(xiàn)快速讀取。這個(gè)已讀計(jì)數(shù)還是讀取回退和位置設(shè)定是的重要依據(jù)。雖然它是Reader值的內(nèi)部結(jié)構(gòu),但是還是可以通過Len方法和Size方法把它計(jì)算出來的:

func main() {
    str := "Make the plan. Execute the plan. Expect the plan to go off the rails. Throw away the plan."
    r1 := strings.NewReader(str)
    fmt.Printf("Size: %d, Len: %d\n", r1.Size(), r1.Len())
    buf := make([]byte, 14)
    n, _ := r1.Read(buf)  // 忽略錯(cuò)誤
    fmt.Println(string(buf))  // 都讀到這里來了
    fmt.Printf("Read: %d\n", n)
    fmt.Printf("Size: %d, Len: %d, Read: %d\n", r1.Size(), r1.Len(), r1.Size()-int64(r1.Len()))
}

Size是整體的長度,Len是剩余未讀內(nèi)容的長度。相減就是已讀計(jì)數(shù)了,這里注意兩個(gè)數(shù)值類型不一樣,需要轉(zhuǎn)一下。
Reader值擁有的大部分用于讀取的方法都會及時(shí)地更新已讀計(jì)數(shù)。比如,ReadByte方法會在讀取成功后講這個(gè)計(jì)數(shù)的值加1,ReadRune方法會在讀取成功后,把讀取到的字符所占的字節(jié)數(shù)作為計(jì)數(shù)的增量。
不過ReadAt方法例外,不會依賴已讀計(jì)數(shù)進(jìn)行讀取,也不會在讀取后更新已讀計(jì)數(shù)。所以讀取的是需要多傳一個(gè)參數(shù),指定起始位置。
還有一個(gè)Seek方法,可以更新已讀計(jì)數(shù)。它的主要作用正式設(shè)定下一次讀取的起始索引位置。它的第二個(gè)參數(shù),可以指定以什么方式和第一個(gè)參數(shù)的offset計(jì)算起始索引位置:

package main

import (
    "fmt"
    "strings"
    "io"
)

func main() {
    str := "Make the plan. Execute the plan. Expect the plan to go off the rails. Throw away the plan."
    r1 := strings.NewReader(str)
    buf := make([]byte, 17)
    offset := int64(15)
    n, _ := r1.ReadAt(buf, offset)
    fmt.Println(n, string(buf))
    r1.Seek(offset + int64(n) + 1, io.SeekStart)

    buf = make([]byte, 36)
    n, _ = r1.Read(buf)
    fmt.Println(n, string(buf))
}

Seek方法還有2個(gè)返回值,返回新的計(jì)數(shù)值和err。

總結(jié)

這篇講了strings包中的兩個(gè)重要的類型:

  • Builder : 用于構(gòu)建字符串
  • Reader : 用于讀取字符串

在字符串拼接方法,Builder值會比原生的string值更有優(yōu)勢。而在字符串的讀取時(shí),Reader值更高效。
在strings包中有用的程序?qū)嶓w不止這2個(gè),還提供了大量的函數(shù):

  • Count
  • IndexRune
  • Mao
  • Replace
  • SolitN
  • Trim

關(guān)于包內(nèi)各種函數(shù)的用法,在下面這篇里有列舉:
https://blog.51cto.com/steed/2299514

向AI問一下細(xì)節(jié)
推薦閱讀:
  1. PHP 字符串
  2. PHP字符串

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI