GO 中 string 的實現原理

小魔童哪吒發表於2021-06-19

上次我們分享的內容我們回顧一下

  • 分享了ETCD的簡單單點部署,ETCD 使用到的包安裝,以及會遇到的問題
  • ETCD 的設定 和 獲取KEY
  • ETCD 的WATCH 監控 KEY的簡化
  • ETCD 的租約 和保活機制
  • ETCD 的分散式鎖的簡單實現

要是對GO 對 ETCD 的編碼還有點興趣的話, 歡迎檢視文章 GO 中 ETCD 的編碼案例分享

字串是什麼?

他是一種基本型別(string 型別),並且是一個不可改變的UTF-8字元序列

在眾多程式語言裡面,相信都少不了字串型別

字串,顧名思義就是一串字元,我們要明白,字元也是分為中文字元和英文字元的

例如我們在 C/C++ 中 , 一個英文字元佔 1 個位元組,一箇中文字元有的佔 2 個位元組,有的佔3個位元組

用到 mysql 的中文字元,有的佔 4 個位元組

回過來看 GO 裡面的字串,字元也是根據英文和中文不一樣,一個字元所佔用的位元組數也是不一樣的,大體分為如下 2

  • 英文的字元,按照ASCII 碼來算,佔用 1 個位元組
  • 其他的字元,包括中文字元在內的,根據不同字元,佔用位元組數是 2 – 4個位元組

字串的資料結構是啥樣的?

說到字串的資料結構,我們先來看看 GO 裡面的字串,是在哪個包裡面

不難發現,我們隨便在 GOLANG 裡面 定義個string 變數,就能夠知道 string 型別是在哪個包裡面,例如

var name  string

GO 裡面的字串對應的包是 builtin

// 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.
type string string
  • 字串這個型別,是所有8-bits 字串的集合,通常但不一定表示utf -8編碼的文字

  • 字串可以為空,但不能為 nil ,此處的字串為空是 ""

  • 字串型別的值是不可變的

另外,找到 string 在 GO 裡面對應的原始碼檔案中src/runtime/string.go , 有這麼一個結構體,只提供給包內使用,我們可以看到string的資料結構 stringStruct 是這個樣子的

type stringStruct struct {
    str unsafe.Pointer
    len int
}

整個結構體,就 2 個成員,*string *型別是不是很簡單呢

  • str

是對應到字串的首地址

  • len

這個就是不難理解,是字串的長度

那麼,在建立一個字串變數的時候,stringStruct 是在哪裡使用到的呢?

我們看看 GO string.go 檔案中的原始碼

//go:nosplit
func gostringnocopy(str *byte) string {
   ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}  // 構建成 stringStruct
   s := *(*string)(unsafe.Pointer(&ss))  // 強轉成 string
   return s
}
//go:nosplit
func findnull(s *byte) int {
   if s == nil {
      return 0
   }

   // Avoid IndexByteString on Plan 9 because it uses SSE instructions
   // on x86 machines, and those are classified as floating point instructions,
   // which are illegal in a note handler.
   if GOOS == "plan9" {
      p := (*[maxAlloc/2 - 1]byte)(unsafe.Pointer(s))
      l := 0
      for p[l] != 0 {
         l++
      }
      return l
   }

   // pageSize is the unit we scan at a time looking for NULL.
   // It must be the minimum page size for any architecture Go
   // runs on. It's okay (just a minor performance loss) if the
   // actual system page size is larger than this value.
   const pageSize = 4096

   offset := 0
   ptr := unsafe.Pointer(s)
   // IndexByteString uses wide reads, so we need to be careful
   // with page boundaries. Call IndexByteString on
   // [ptr, endOfPage) interval.
   safeLen := int(pageSize - uintptr(ptr)%pageSize)

   for {
      t := *(*string)(unsafe.Pointer(&stringStruct{ptr, safeLen}))
      // Check one page at a time.
      if i := bytealg.IndexByteString(t, 0); i != -1 {
         return offset + i
      }
      // Move to next page
      ptr = unsafe.Pointer(uintptr(ptr) + uintptr(safeLen))
      offset += safeLen
      safeLen = pageSize
   }
}

簡單分為 2 步:

  • 先將字元資料構建程 stringStruct
  • 再通過 gostringnocopy 函式 轉換成 string

字串中的資料為什麼不能被修改呢?

從上述官方說明中,我們可以看到,字串型別的值是不可變的

可是這是為啥呢?

我們以前在寫C/C++的時候,為啥可以開闢空間存放多個字元,並且還可以修改其中的某些字元呢?

可是在 C/C++裡面的字面量也是不可以改變的

GO 裡面的 string 型別,是不是也和 字面量一樣的呢?我們來看看吧

字串型別,本身也是擁有對應的記憶體空間的,那麼修改string型別的值應該是要支援的。

可是,XDM 在 Go 的實現中,string 型別是不包含記憶體空間,只有一個記憶體的指標,這裡就有點想C/C++裡面的案例:

char * str = "XMTONG"

上述的 str是絕對不能做修改的,str只是作為可讀,不能寫的

在GO 裡面的字串,就與上述類似

這樣做的好處是 string 變得非常輕量,可以很方便的進行傳遞而不用擔心記憶體拷貝(這也避免了記憶體帶來的諸多問題)

GO 中的 string型別一般是指向字串字面量

字串字面量儲存位置是在虛擬記憶體分割槽的只讀段上面,而不是堆或棧上

因此,GO 的 string 型別不可修改的

可是我們想一想,要是在GO 裡面字串全都是隻讀的,那麼我們如何動態修改一些我們需要改變的字元呢,這豈不是缺陷了

別慌

GO 裡面還有byte陣列,[]byte

這裡順帶說一下

上述 char * str = "XMTONG"

  • 字串長度,就是字元的個數,為 6
  • 計算str所佔位元組數(C/C++中是通過 sizeof() 來計算的)的話,那就是 7 ,因為尾巴後面還有一個’\0’

計算機中有這樣的對應關係,簡單提一下:

1 Bytes = 8 bit

1 K = 1024 Bytes

1 M = 1024 K

1 G = 1024 M

為什麼有了字串 還要 []byte?

原因正如上述我們說到的,如果全是一些只讀的字面量,那麼我們編碼的時候就沒得玩了

另外,也是根據使用字串的場景原因,單是string無法滿足所有的場景,因此得有一個我們可以修改裡面值的 []byte 來彌補一下

說到這裡,我們應該就知道了,string[]byte都是可以表示字串,沒毛病 ,

不過,他們畢竟對應不同的資料結構,使用方式也有一定的區別,GO 提供的對應方法也是不盡相同

我們來看看什麼場景用 string 型別, 啥場景 使用 []byte 型別

使用到 string 型別的 地方:

  • 需要對字串進行比較的時候,使用string 型別非常方便,直接使用操作符進行比較即可
  • string 型別 型別,為空的時候是 “”,他不能和nil做比較,因此,不用到nil的時候,也可以使用 string 型別

使用到 []byte 型別的 地方:

  • 需要修改字串中字元的應用場景,使用[]byte 型別就相當靈活了,用起來很香
  • []byte 型別 為空的話,會是返回 nil ,需要使用到 nil 的時候,就可以使用他
  • []byte 型別 本身就可以按照切片的方式來玩,因此需要操作切片的時候,也可以用他

就上述場景來看,好像使用 []byte 更加實在和靈活,為啥還要用 string

原因如下:

  • string 型別看起來直觀,用起來簡單
  • []byte,byte 陣列,我們可以知道,裡面都是一個位元組一個位元組的,這個會比較多的用在底層,對操作位元組比較關注的時候

字串 和 []byte 如何互相轉換?

看到這裡,分別瞭解了 string 型別, 和 []byte 型別的應用場景

毋庸置疑,我們編碼過程中,肯定少不了對他們做相互轉換,我們來看看在 GO ,裡面如何使用

字串轉 []byte

package main

import (
   "fmt"
)


func main(){
    var str string

    str = "XMTONG"

    strByte := []byte(str)
    for _,v :=range strByte{
        fmt.Printf("%x ",v)
    }
}

程式碼輸出為:

58 4d 54 4f 4e 47

上述程式碼轉成 []byte 之後是一個位元組,一個位元組的

將每一個位元組的值用十六進位制列印出來,我們可以看到,XMTONG 對應 584d544f4e47

[]byte 轉字串

[]byte轉字串在GO 裡面那就更簡單了

func main(){
   name := []byte("XMTONG")
   fmt.Println(string(name))
}

GO 中 字串都會涉及到哪些函式?

無論什麼語言,對於字串大概涉及如下幾種操作,若有偏差,還請指正:

  • 計算字串長度
  • 拼接
  • 切割
  • 找到字串進行替換,找到字串的具體位置和出現的次數
  • 統計字串
  • 字串進位制轉換

具體的函式使用方法也比較簡單,推薦大家感興趣的可以直接看go 的開發文件,需要的時候去查一下即可。

GO 的標準開發文件,在搜尋引擎裡面還是比較容易搜尋到的

img

總結

  • 分享了字串具體是啥
  • GO 中字串的特性,為什麼不能被修改
  • 字串 GO 原始碼是如何構建的
  • 字串 和 []byte 的由來和應用場景
  • 字串與 []byte 相互轉換
  • 順帶提了GO 的標準開發文件,大家可以用起來哦

歡迎點贊,關注,收藏

朋友們,你的支援和鼓勵,是我堅持分享,提高質量的動力

好了,本次就到這裡,下一次 GO 中 slice 的實現原理分享

技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章