您好,登錄后才能下訂單哦!
字符編碼的問題,是計(jì)算機(jī)領(lǐng)域中非?;A(chǔ)的一個(gè)問題。
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”。
在計(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)表示。
Unicode編碼規(guī)范提供了3種不同的編碼格式:
上面的名稱中,右邊的整數(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é)序列。
在底層,一個(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是由1至4個(gè)字節(jié)表示,是變長的。在編碼的時(shí)候,第一個(gè)字節(jié)的高位指明了后面還有多少個(gè)字節(jié):
分析一下“你”這個(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)前字符的索引值就算好了。
標(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)
}
標(biāo)準(zhǔn)庫中的strings代碼包,在這個(gè)包里用到了不少unicode包和unicode/utf8包中的程序?qū)嶓w。比如,strings.Builder類型的WriteRune方法,strings.Reader類型的ReadRune方法,等等。
原生的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是1.10加入strings包中的新類型。如果是舊版本就沒有了。
Golang貌似不支持升級,所以需要卸載,然后安裝新版本。
與string的值相比,strings.Builder類型的值有以下3個(gè)優(yōu)勢:
比較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)稱為拼接方法:
拼接方法的示例代碼:
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())
}
利用上面這些方法,就可以把新的內(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)的方法,就不深究了。
還有一個(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)存資源。
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ù)制行為包括但不限于下面這些:
這種約束還是有好處的,這樣肯定不會出現(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)容的變化。
由于其內(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.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。
這篇講了strings包中的兩個(gè)重要的類型:
在字符串拼接方法,Builder值會比原生的string值更有優(yōu)勢。而在字符串的讀取時(shí),Reader值更高效。
在strings包中有用的程序?qū)嶓w不止這2個(gè),還提供了大量的函數(shù):
關(guān)于包內(nèi)各種函數(shù)的用法,在下面這篇里有列舉:
https://blog.51cto.com/steed/2299514
免責(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)容。