Go String 筆記

什么是 string ?

標準庫builtin的解釋:

type string

string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. A string may be empty, but not nil. Values of string type are immutable.

簡單的來說字符串是一系列 8 位字節(jié)的集合,通常但不一定代表 UTF-8 編碼的文本。字符串可以為空,但不能為 nil。而且字符串的值是不能改變的。
不同的語言字符串有不同的實現(xiàn),在 go 的源碼中 src/runtime/string.go,string 在底層的定義如下:

type stringStruct struct {
    str unsafe.Pointer
    len int
}

可以看到 str 其實是個指針,指向某個數(shù)組的首地址,這個數(shù)組就是一個字節(jié)數(shù)組,里面存著 string 的真正內(nèi)容。其實字節(jié)數(shù)組指針更像是 c 語言的字符串形式,而在 go 里,對其進行封裝。不同的是, c 語言的 string 是以 null 或 /0 結(jié)尾,計算長度的時候?qū)ζ浔闅v;而 go 的 string 結(jié)尾沒有特殊符號,只不過用空間換時間,把長度存在了 len 字段里。

那么問題來了,我們平時用的 string 又是什么呢?它的定義如下:

type string string

。。。好像和剛剛說的不太一樣哈(-_-!)。這個 string 就是一個名叫 string 的類型,其實什么也不代表。只不過為了直觀,使用的時候,把 stringStruct 轉(zhuǎn)換成 string 類型。

func gostringnocopy(str *byte) string {
    ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
    s := *(*string)(unsafe.Pointer(&ss))
    return s
}

為了驗證,我們可以試一下:

package main

import (
   "fmt"
   "unsafe"
)

func main()  {
   var a = "nnnn"
   fmt.Println(a)
   var b = (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&a)) + 8));
    // 按照 stringStruct 結(jié)構(gòu),把 a 地址偏移 int 的長度位,得到 len 字段地址
    // 這里我的電腦是 64 位,而系統(tǒng)尋址以一個在節(jié)為單位,所以 +8
   fmt.Println(*b) // 這里輸出的是 a 的長度 4
}

string 操作

拼接

我們可以用 + 來完成字符串的拼接,就像這樣:s := x+y+z+… 底層如何實現(xiàn)的呢?

type tmpBuf [tmpStringBufSize]byte // 這是一個很重要的類型,tmpStringBufSize 為常量 32,但這個值并沒有什么科學依據(jù)(-_-!)

func concatstrings(buf *tmpBuf, a []string) string {// 把所有要拼接的字符串放到 a 里面
    idx := 0
    l := 0
    count := 0
    for i, x := range a { // 這里主要計算總共需要的長度,以便分配內(nèi)存
        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 "" 
        // 需要注意的是,雖然空字符串看起來不占空間,可是底層還是 stringStruct,仍要占兩個 int 空間
    }

    if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
        return a[idx] // count 為 1 表明不需要拼接,直接返回源 string,并且沒有內(nèi)存拷貝
    }
    s, b := rawstringtmp(buf, l) // 這里分配了一個長度為 l 字節(jié)的內(nèi)存,這個內(nèi)存并沒有初始化
    for _, x := range a {
        copy(b, x) // 把每個字符串的內(nèi)容復制到新的字節(jié)數(shù)組里面
        b = b[len(x):]
    }
    return s
}

可是這里有個問題,b 是一個字節(jié)切片,而 x 是字符串,為什么能直接復制呢?

與切片的轉(zhuǎn)換

內(nèi)置函數(shù)copy會有一種特殊情況copy(dst []byte, src string) int,但是兩者并不能直接 copy,需要把 string 轉(zhuǎn)換成 []byte。

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
}

// 申請新的內(nèi)存,返回切片
func rawbyteslice(size int) (b []byte) {
    cap := roundupsize(uintptr(size)) // 使申請內(nèi)存的大小為 8 的倍數(shù)
    p := mallocgc(cap, nil, false) // 第三個參數(shù)為 FALSE 表示不用給分配的內(nèi)存清零
    if cap != uintptr(size) {
        memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size)) // 超出需要的部分內(nèi)存清零
    }

    *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}
    return
}

string 與內(nèi)存

string 字面量

前面提到過,字符串的值是不能改變的,可是為什么呢?

這里說的字符串通常指的是 字符串字面量,因為它的存儲位置不在堆和棧上,通常 string 常量是編譯器分配到只讀段的(.rodata),對應的數(shù)據(jù)地址不可修改。

不過等等,好像有什么不對?下面的代碼為啥改了呢?

var str = "aaaa"
str = "bbbb"

這是因為前面提到過的 stringStruct,我們拿到的 str 實際上是 stringStruct 轉(zhuǎn)換成 string 的。常量aaaa被保存在了只讀段,下面函數(shù)參數(shù) str 就是這個常量的地址:

func gostringnocopy(str *byte) string {
    ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
    s := *(*string)(unsafe.Pointer(&ss))
    return s
}

所以我們拿到的 str 本來是 stringStruct.str ,給 str 賦值相當于給 stringStruct.str 賦值,使其指向 bbbb所在地只讀段地址,而 aaaa本身是沒有改變的。在改變 stringStruct.str 的同時,解釋器也會更新 stringStruct.len 的值。

動態(tài) string

所謂動態(tài)是指字符串 stringStruct.str 指向的地址不在只讀段,而是指向由 malloc 動態(tài)分配的堆地址。盡管如此,直接修改 string 的內(nèi)容還是非法的。要修改內(nèi)容,可以先把 string 轉(zhuǎn)成 []byte,不過這里會有一次內(nèi)存拷貝,這點在轉(zhuǎn)換的代碼中可以看到。不過也可以做到 ‘零拷貝轉(zhuǎn)換’:

func stringtoslicebyte(s string) []byte {
    sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
    bh := reflect.SliceHeader{
        Data: sh.Data,
        Len:  sh.Len,
        Cap:  sh.Len,
    }
    return *(*[]byte)(unsafe.Pointer(&bh))
}

不過這種方法不建議使用,因為一旦 string 指向的內(nèi)存位于只讀段,轉(zhuǎn)換成 []byte 后對其進行寫操作會引發(fā)系統(tǒng)的段錯誤。

臨時 string

有時候我們會把 []byte 轉(zhuǎn)換成 string,通常也會發(fā)生一次內(nèi)存拷貝,但有的時候我們只需要 ‘臨時的’ 字符串,比如:

  • 使用 m[string(k)] 來查找map
  • 用作字符拼接: "<"+string(b)+">"
  • 用于比較: string(b)=="foo"

這些情況下我們都只是臨時的使用一下一個 []byte 的字符串形式的值,如果分配內(nèi)存有點不劃算,所以編譯器會做出一些優(yōu)化,使用如下函數(shù)來轉(zhuǎn)換:

func slicebytetostringtmp(b []byte) string {
    if raceenabled && len(b) > 0 {
        racereadrangepc(unsafe.Pointer(&b[0]),
            uintptr(len(b)),
            getcallerpc(),
            funcPC(slicebytetostringtmp))
    }
    if msanenabled && len(b) > 0 {
        msanread(unsafe.Pointer(&b[0]), uintptr(len(b)))
    }
    // 注意,以上兩個 if 都為假,所以不會執(zhí)行。不知道有什么用(-_-!)
    return *(*string)(unsafe.Pointer(&b))
}

以上是讀了 src/runtime/string.go 代碼的一些個人想法,連蒙帶猜,所以有些地方可能不太對,歡迎指出啦(_)!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,638評論 18 399
  • 前言 最先接觸編程的知識是在大學里面,大學里面學了一些基礎(chǔ)的知識,c語言,java語言,單片機的匯編語言等;大學畢...
    oceanfive閱讀 3,375評論 0 7
  • 1.一幅畫絕非簡單的湊合就能完成的。 2.整體勝于部分的總和。 3.夢想和現(xiàn)實總是沖突,總要承受其中之一。 4.有...
    抹不去的執(zhí)念閱讀 293評論 1 0
  • 當今零售業(yè)態(tài)相互雜糅,新的商業(yè)場景不斷涌現(xiàn),線下和線上的流量被不斷的分化,這是一個不斷顛覆、快速迭代的痛苦的時代,...
    會飛的小龍貓閱讀 674評論 0 1
  • 樹冠的另一面 有一些擁擠的聲音 它們脫掉新的外殼 蒙住蚊子們的眼睛 而空氣像玻璃 阻擋著我的鼻尖
    托爾西奇閱讀 122評論 0 0

友情鏈接更多精彩內(nèi)容