佳木斯湛栽影视文化发展公司

主頁(yè) > 知識(shí)庫(kù) > 簡(jiǎn)單談?wù)凣olang中的字符串與字節(jié)數(shù)組

簡(jiǎn)單談?wù)凣olang中的字符串與字節(jié)數(shù)組

熱門標(biāo)簽:AI電銷 網(wǎng)站排名優(yōu)化 呼叫中心市場(chǎng)需求 Linux服務(wù)器 百度競(jìng)價(jià)排名 服務(wù)外包 鐵路電話系統(tǒng) 地方門戶網(wǎng)站

前言

字符串是 Go 語(yǔ)言中最常用的基礎(chǔ)數(shù)據(jù)類型之一,雖然字符串往往都被看做是一個(gè)整體,但是實(shí)際上字符串是一片連續(xù)的內(nèi)存空間,我們也可以將它理解成一個(gè)由字符組成的數(shù)組,Go 語(yǔ)言中另外一個(gè)與字符串關(guān)系非常密切的類型就是字節(jié)(Byte)了,相信各位讀者也都非常了解,這里也就不展開介紹。

我們?cè)谶@一節(jié)中就會(huì)詳細(xì)介紹這兩種基本類型的實(shí)現(xiàn)原理以及它們的轉(zhuǎn)換關(guān)系,但是這里還是會(huì)將介紹的重點(diǎn)主要放在字符串上,因?yàn)檫@是我們接觸最多的一種基本類型并且后者就是一個(gè)簡(jiǎn)單的 uint8 類型,所以會(huì)給予 string 最大的篇幅,需要注意的是這篇文章不會(huì)使用大量的篇幅介紹 UTD-8 以及編碼等知識(shí),主要關(guān)注的還是字符串的結(jié)構(gòu)以及常見操作的實(shí)現(xiàn)。

字符串雖然在 Go 語(yǔ)言中是基本類型 string ,但是它其實(shí)就是字符組成的數(shù)組,C 語(yǔ)言中的字符串就可以用 char[] 來(lái)表示,作為數(shù)組來(lái)說(shuō)它會(huì)占用一片連續(xù)的內(nèi)存空間,這片連續(xù)的內(nèi)存空間就存儲(chǔ)了一些 字節(jié) ,這些字節(jié)共同組成了字符串, Go 語(yǔ)言中的字符串是一個(gè)只讀的字節(jié)數(shù)組切片 ,下面就是一個(gè)只讀的 "hello" 字符串在內(nèi)存中的結(jié)構(gòu):

如果是代碼中存在的字符串,會(huì)在編譯期間被標(biāo)記成只讀數(shù)據(jù) SRODATA 符號(hào),假設(shè)我們有以下的一段代碼,其中包含了一個(gè)字符串,當(dāng)我們將這段代碼編譯成匯編語(yǔ)言時(shí),就能夠看到 hello 字符串有一個(gè) SRODATA 的標(biāo)記:

$ cat main.go
package main

func main() {
 str := "hello"
 println([]byte(str))
}

$ GOOS=linux GOARCH=amd64 go tool compile -S main.go
...
go.string."hello" SRODATA dupok size=5
 0x0000 68 65 6c 6c 6f     hello
...

不過(guò)這只能表明編譯期間存在的字符串會(huì)被直接分配到只讀的內(nèi)存空間并且這段內(nèi)存不會(huì)被更改,但是在運(yùn)行時(shí)我們其實(shí)還是可以將這段內(nèi)存拷貝到其他的堆或者棧上,同時(shí)將變量的類型修改成 []byte 在修改之后再通過(guò)類型轉(zhuǎn)換變成 string ,不過(guò)如果想要直接修改 string 類型變量的內(nèi)存空間,Go 語(yǔ)言是不支持這種操作的。

除了今天的主角字符串之外,另外的配角 byte 也還是需要簡(jiǎn)單介紹一下的,byte 其實(shí)非常好理解,每一個(gè) byte 就是 8 個(gè) bit,相信稍微對(duì)編程有所了解的人應(yīng)該都對(duì)這個(gè)概念一清二楚,而字節(jié)數(shù)組也沒什么值得介紹的,所以這里就直接跳過(guò)了。

字符串在 Go 語(yǔ)言中的接口其實(shí)非常簡(jiǎn)單,每一個(gè)字符串在運(yùn)行時(shí)都會(huì)使用如下的 StringHeader 結(jié)構(gòu)體去表示,在運(yùn)行時(shí)包的內(nèi)部其實(shí)有一個(gè)私有的結(jié)構(gòu) stringHeader ,它有著完全相同的結(jié)構(gòu)只是用于存儲(chǔ)數(shù)據(jù)的 Data 字段使用了 unsafe.Pointer 類型:

type StringHeader struct {
 Data uintptr
 Len int
}

為什么我們會(huì)說(shuō)字符串其實(shí)是一個(gè)只讀類型的切片 呢,我們可以看一下切片在 Go 語(yǔ)言中的運(yùn)行時(shí)表示:

type SliceHeader struct {
 Data uintptr
 Len int
 Cap int
}

這個(gè)表示切片的結(jié)構(gòu) SliceHeader 和字符串的結(jié)構(gòu) StringHeader 非常類似,與切片的結(jié)構(gòu)相比,字符串少了一個(gè)表示容量的 Cap 字段,這是因?yàn)樽址鳛橹蛔x的類型,我們并不會(huì)直接向字符串直接追加元素改變其本身的內(nèi)存空間,所有追加的操作都是通過(guò)拷貝來(lái)完成的。

字符串的解析一定是解析器在詞法分析 時(shí)就完成的,詞法分析階段會(huì)對(duì)源文件中的字符串進(jìn)行切片和分組,將原有無(wú)意義的字符流轉(zhuǎn)換成 Token 序列,在 Go 語(yǔ)言中,有兩種字面量的方式可以聲明一個(gè)字符串,一種是使用雙引號(hào),另一種是使用反引號(hào):

str1 := "this is a string"
str2 := `this is another 
string`

使用雙引號(hào)聲明的字符串其實(shí)和其他語(yǔ)言中的字符串沒有太多的區(qū)別,它只能用于簡(jiǎn)單、單行的字符串并且如果字符串內(nèi)部出現(xiàn)雙引號(hào)時(shí)需要使用 \ 符號(hào)避免編譯器的解析錯(cuò)誤,而反引號(hào)聲明的字符串就可以擺脫單行的限制,因?yàn)殡p引號(hào)不再標(biāo)記字符串的開始和結(jié)束,我們可以在字符串內(nèi)部直接使用 " ,在遇到需要寫 JSON 或者其他數(shù)據(jù)格式的場(chǎng)景下非常方便。

兩種不同的聲明方式其實(shí)也意味著 Go 語(yǔ)言的編譯器需要在解析的階段能夠區(qū)分并且正確解析這兩種不同的字符串格式,解析字符串使用的 scanner 掃描器,它的主要作用就是將輸入的字符流轉(zhuǎn)換成 Token 流, stdString 方法就是它用來(lái)解析使用雙引號(hào)包裹的標(biāo)準(zhǔn)字符串:

func (s *scanner) stdString() {
 s.startLit()

 for {
 r := s.getr()
 if r == '"' {
 break
 }
 if r == '\\' {
 s.escape('"')
 continue
 }
 if r == '\n' {
 s.ungetr()
 s.error("newline in string")
 break
 }
 if r  0 {
 s.errh(s.line, s.col, "string not terminated")
 break
 }
 }

 s.nlsemi = true
 s.lit = string(s.stopLit())
 s.kind = StringLit
 s.tok = _Literal
}

從這個(gè)方法中我們其實(shí)能夠看出 Go 語(yǔ)言處理標(biāo)準(zhǔn)字符串的邏輯:

1.標(biāo)準(zhǔn)字符串使用雙引號(hào)表示開頭和結(jié)尾;

2. 標(biāo)準(zhǔn)字符串中需要使用反斜杠 \ 來(lái) escape 雙引號(hào);

3. 標(biāo)準(zhǔn)字符串中不能出現(xiàn)換行符號(hào) \n ;

原始字符串解析的規(guī)則就非常簡(jiǎn)單了,它會(huì)將非反引號(hào)的所有字符都劃分到當(dāng)前字符串的范圍中,所以我們可以使用它來(lái)支持復(fù)雜的多行字符串字面量,例如 JSON 等數(shù)據(jù)格式。

func (s *scanner) rawString() {
 s.startLit()

 for {
 r := s.getr()
 if r == '`' {
 break
 }
 if r  0 {
 s.errh(s.line, s.col, "string not terminated")
 break
 }
 }

 s.nlsemi = true
 s.lit = string(s.stopLit())
 s.kind = StringLit
 s.tok = _Literal
}

無(wú)論是標(biāo)準(zhǔn)字符串還是原始字符串最終都會(huì)被標(biāo)記成 StringLit 類型的 Token 并傳遞到編譯的下一個(gè)階段 —語(yǔ)法分析,在語(yǔ)法分析的階段,與字符串相關(guān)的表達(dá)式都會(huì)使用如下的方法 BasicLit 對(duì)字符串進(jìn)行處理:

func (p *noder) basicLit(lit *syntax.BasicLit) Val {
 switch s := lit.Value; lit.Kind {
 case syntax.StringLit:
 if len(s) > 0  s[0] == '`' {
 s = strings.Replace(s, "\r", "", -1)
 }
 u, _ := strconv.Unquote(s)
 return Val{U: u}
 }
}

無(wú)論是 import 語(yǔ)句中包的路徑、結(jié)構(gòu)體中的字段標(biāo)簽還是表達(dá)式中的字符串都會(huì)使用這個(gè)方法將原生字符串中最后的換行符刪除并對(duì)字符串 Token 進(jìn)行 Unquote,也就是去掉字符串兩遍的引號(hào)等無(wú)關(guān)干擾,還原其本來(lái)的面目。

strconv.Unquote 方法處理了很多邊界條件導(dǎo)致整個(gè)函數(shù)非常復(fù)雜,不僅包括各種不同引號(hào)的處理,還包括 UTF-8 等編碼的相關(guān)問題,所以在這里也就不展開介紹了,感興趣的讀者可以在 Go 語(yǔ)言中找到 strconv.Unquote 方法詳細(xì)了解它的執(zhí)行過(guò)程。

介紹完了字符串的的解析過(guò)程,這一節(jié)就會(huì)繼續(xù)介紹字符串的常見操作了,我們?cè)谶@里要介紹的字符串常見操作包括字符串的拼接和類型轉(zhuǎn)換,字符串相關(guān)功能的主要是通過(guò) Go 語(yǔ)言運(yùn)行時(shí)或者 strings 包完成的,我們會(huì)重點(diǎn)介紹運(yùn)行時(shí)字符串的操作,想要了解 strings 包的讀者可以閱讀相關(guān)的代碼,這里就不多介紹了。

Go 語(yǔ)言中拼接字符串會(huì)使用 + 符號(hào),當(dāng)我們使用這個(gè)符號(hào)對(duì)字符串進(jìn)行拼接時(shí),編譯器會(huì)在類型檢查階段將 OADD 節(jié)點(diǎn)轉(zhuǎn)換成 OADDSTR 類型的節(jié)點(diǎn),隨后在 SSA 中間代碼生成的階段調(diào)用 addstr 函數(shù):

func walkexpr(n *Node, init *Nodes) *Node {
 switch n.Op {
 // ...
 case OADDSTR:
 n = addstr(n, init)
 }
}

addstr 函數(shù)就是幫助我們?cè)诰幾g期間選擇合適的函數(shù)對(duì)字符串進(jìn)行拼接,如果需要拼接的字符串小于或者等于 5 個(gè),那么就會(huì)直接調(diào)用 concatstring{2,3,4,5} 等一系列函數(shù),如果超過(guò) 5 個(gè)就會(huì)直接選擇 concatstrings 傳入一個(gè)數(shù)組切片。

func addstr(n *Node, init *Nodes) *Node {
 c := n.List.Len()

 buf := nodnil()
 args := []*Node{buf}
 for _, n2 := range n.List.Slice() {
 args = append(args, conv(n2, types.Types[TSTRING]))
 }

 var fn string
 if c = 5 {
 fn = fmt.Sprintf("concatstring%d", c)
 } else {
 fn = "concatstrings"

 t := types.NewSlice(types.Types[TSTRING])
 slice := nod(OCOMPLIT, nil, typenod(t))
 slice.List.Set(args[1:])
 args = []*Node{buf, slice}
 }

 cat := syslook(fn)
 r := nod(OCALL, cat, nil)
 r.List.Set(args)
 // ...

 return r
}

其實(shí)無(wú)論使用 concatstring{2,3,4,5} 中的哪一個(gè),最終都會(huì)調(diào)用 concatstrings ,在這個(gè)函數(shù)中我們會(huì)先對(duì)傳入的切片參數(shù)進(jìn)行遍歷,首先會(huì)過(guò)濾空字符串并獲取拼接后字符串的長(zhǎng)度。

func concatstrings(buf *tmpBuf, a []string) string {
 idx := 0
 l := 0
 count := 0
 for i, x := range a {
 n := len(x)
 if n == 0 {
 continue
 }
 if l+n  l {
 throw("string concatenation too long")
 }
 l += n
 count++
 idx = i
 }
 if count == 0 {
 return ""
 }

 if count == 1  (buf != nil || !stringDataOnStack(a[idx])) {
 return a[idx]
 }
 s, b := rawstringtmp(buf, l)
 for _, x := range a {
 copy(b, x)
 b = b[len(x):]
 }
 return s
}

如果非空字符串的數(shù)量為 1 并且當(dāng)前的字符串不在棧上或者沒有逃逸出調(diào)用堆棧,那么就可以直接返回該字符串,不需要進(jìn)行任何的耗時(shí)操作。

但是在正常情況下,原始的多個(gè)字符串都會(huì)被調(diào)用 copy 將所有的字符串拷貝到目標(biāo)字符串所在的內(nèi)存空間中,新的字符串其實(shí)就是一片新的內(nèi)存空間,與原來(lái)的字符串沒有任何關(guān)聯(lián)。

類型轉(zhuǎn)換

當(dāng)我們使用 Go 語(yǔ)言做一些 JSON 等數(shù)據(jù)格式的解析和序列化時(shí),可能經(jīng)常會(huì)將這些變量在字符串和字節(jié)數(shù)組之間來(lái)回轉(zhuǎn)換,類型之間轉(zhuǎn)換的開銷并沒有想象的這么小,我們經(jīng)常會(huì)看到 slicebytetostring 等函數(shù)出現(xiàn)在火焰圖中,這個(gè)函數(shù)就是將字節(jié)數(shù)組轉(zhuǎn)換成字符串所使用的函數(shù),也就是一個(gè)類似 string(bytes) 的操作會(huì)在編譯期間轉(zhuǎn)換成 slicebytetostring 的函數(shù)調(diào)用,這個(gè)函數(shù)在函數(shù)體中首先會(huì)處理兩種比較常見的情況,也就是字節(jié)長(zhǎng)度為 0 或者 1 的情況:

func slicebytetostring(buf *tmpBuf, b []byte) (str string) {
 l := len(b)
 if l == 0 {
 return ""
 }
 if l == 1 {
 stringStructOf(str).str = unsafe.Pointer(staticbytes[b[0]])
 stringStructOf(str).len = 1
 return
 }

 var p unsafe.Pointer
 if buf != nil  len(b) = len(buf) {
 p = unsafe.Pointer(buf)
 } else {
 p = mallocgc(uintptr(len(b)), nil, false)
 }
 stringStructOf(str).str = p
 stringStructOf(str).len = len(b)
 memmove(p, (*(*slice)(unsafe.Pointer(b))).array, uintptr(len(b)))
 return
}

處理過(guò)后會(huì)根據(jù)傳入的緩沖區(qū)大小決定是否需要為新的字符串分配一片內(nèi)存空間, stringStructOf 會(huì)將傳入的字符串指針轉(zhuǎn)換成 stringStruct 結(jié)構(gòu)體指針,然后設(shè)置結(jié)構(gòu)體持有的指針 str 和字符串長(zhǎng)度 len ,最后通過(guò) memmove 將原字節(jié)數(shù)組中的字節(jié)全部復(fù)制到新的內(nèi)存空間中。

從字符串到字節(jié)數(shù)組的轉(zhuǎn)換使用的就是 stringtoslicebyte 函數(shù)了,這個(gè)函數(shù)的實(shí)現(xiàn)非常簡(jiǎn)單:

func stringtoslicebyte(buf *tmpBuf, s string) []byte {
 var b []byte
 if buf != nil  len(s) = len(buf) {
 *buf = tmpBuf{}
 b = buf[:len(s)]
 } else {
 b = rawbyteslice(len(s))
 }
 copy(b, s)
 return b
}

它會(huì)使用傳入的緩沖區(qū)或者根據(jù)字符串的長(zhǎng)度調(diào)用 rawbyteslice 創(chuàng)建一個(gè)新的字節(jié)切片, copy 關(guān)鍵字就會(huì)將字符串中的內(nèi)容拷貝到新的字節(jié)數(shù)組中。

字符串和字節(jié)數(shù)組中的內(nèi)容雖然一樣,但是字符串的內(nèi)容是只讀的,我們不能通過(guò)下標(biāo)或者其他形式改變其內(nèi)存存儲(chǔ)的數(shù)據(jù),而字節(jié)切片中的內(nèi)容都是可以讀寫的,所以無(wú)論是從哪種類型轉(zhuǎn)換到另一種都需要對(duì)其中的內(nèi)容進(jìn)行拷貝,內(nèi)存拷貝的性能損耗會(huì)隨著字符串?dāng)?shù)組和字節(jié)長(zhǎng)度的增長(zhǎng)而增長(zhǎng),所以在做這種類型轉(zhuǎn)換時(shí)一定要注意性能上的問題。

字符串是 Go 語(yǔ)言中相對(duì)來(lái)說(shuō)比較簡(jiǎn)單的一種數(shù)據(jù)結(jié)構(gòu),作為只讀的數(shù)據(jù)類型,我們無(wú)法改變其本身的結(jié)構(gòu),但是在做類型轉(zhuǎn)換的操作時(shí)一定要注意性能上的瓶頸,遇到需要極致性能的場(chǎng)景一定要盡量減少不同類型的轉(zhuǎn)換,避免額外的開銷。

相關(guān)文章

本作品采用進(jìn)行許可。 轉(zhuǎn)載時(shí)請(qǐng)注明原文鏈接,圖片在使用時(shí)請(qǐng)保留圖片中的全部?jī)?nèi)容,可適當(dāng)縮放并在引用處附上圖片所在的文章鏈接,圖片使用 Sketch 進(jìn)行繪制。如果對(duì)本文的內(nèi)容有疑問,請(qǐng)?jiān)谙旅娴脑u(píng)論系統(tǒng)中留言,謝謝。

好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。

您可能感興趣的文章:
  • golang 中獲取字符串個(gè)數(shù)的方法
  • Golang 中整數(shù)轉(zhuǎn)字符串的方法
  • Golang 統(tǒng)計(jì)字符串字?jǐn)?shù)的方法示例
  • Golang中文字符串截取函數(shù)實(shí)現(xiàn)原理
  • Golang實(shí)現(xiàn)字符串倒序的幾種解決方案
  • Golang 語(yǔ)言高效使用字符串的方法

標(biāo)簽:湖南 崇左 湘潭 蘭州 銅川 仙桃 黃山 衡水

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《簡(jiǎn)單談?wù)凣olang中的字符串與字節(jié)數(shù)組》,本文關(guān)鍵詞  ;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請(qǐng)?zhí)峁┫嚓P(guān)信息告之我們,我們將及時(shí)溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無(wú)關(guān)。
  • 相關(guān)文章
  • 收縮
    • 微信客服
    • 微信二維碼
    • 電話咨詢

    • 400-1100-266
    大冶市| 寿阳县| 白银市| 舒兰市| 岳普湖县| 买车| 蕲春县| 林甸县| 新兴县| 吴忠市| 安宁市| 临邑县| 高阳县| 泗阳县| 舟山市| 正阳县| 威信县| 陇南市| 汉源县| 兴义市| 永善县| 金昌市| 云阳县| 安丘市| 鄂尔多斯市| 高州市| 长岛县| 长岭县| 茶陵县| 库伦旗| 巍山| 东平县| 兴安盟| 翁源县| 龙井市| 平果县| 乌鲁木齐县| 项城市| 卫辉市| 龙州县| 藁城市|