《快學 Go 語言》第 7 課 —— 字符串

字符串一般有兩種設計,一種是「字符」串,一種是「字節」串。「字符」串中的每一個字都是定長的,而「字節」串中每一個字是不定長的。Go 語言裏的字符串是「字節」串,英文字符佔用 1 個字節,非英文字符佔多個字節。這意味着沒法經過位置來快速定位出一個完整的字符來,而必須經過遍歷的方式來逐個獲取單個字符。編程

咱們所說的字符一般是指 unicode 字符,你能夠認爲全部的英文和漢字在 unicode 字符集中都有一個惟一的整數編號,一個 unicode 一般用 4 個字節來表示,對應的 Go 語言中的字符 rune 佔 4 個字節。在 Go 語言的源碼中能夠找到下面這行代碼,rune 類型是一個衍生類型,它在內存裏面使用 int32 類型的 4 個字節存儲。數組

type rune int32
複製代碼

使用「字符」串來表示字符串勢必會浪費空間,由於全部的英文字符原本只須要 1 個字節來表示,用 rune 字符來表示的話那麼剩餘的 3 個字節都是零。可是「字符」串有一個好處,那就是能夠快速定位。bash

爲了進一步方便讀者理解字節 byte 和 字符 rune 的關係,我花了下面這張圖網絡

其中 codepoint 是每一個「字」的其實偏移量。Go 語言的字符串採用 utf8 編碼,中文漢字一般須要佔用 3 個字節,英文只須要 1 個字節。len() 函數獲得的是字節的數量,經過下標來訪問字符串獲得的是「字節」。函數

按字節遍歷

字符串能夠經過下標來訪問內部字節數組具體位置上的字節,字節是 byte 類型ui

package main

import "fmt"

func main() {
	var s = "嘻哈china"
	for i:=0;i<len(s);i++ {
		fmt.Printf("%x ", s[i])
	}
 
}

-----------
e5 98 bb e5 93 88 63 68 69 6e 61
複製代碼

按字符 rune 遍歷

package main

import "fmt"

func main() {
	var s = "嘻哈china"
	for codepoint, runeValue := range s {
		fmt.Printf("%d %d ", codepoint, int32(runeValue))
	}
}

-----------
0 22075 3 21704 6 99 7 104 8 105 9 110 10 97
複製代碼

對字符串進行 range 遍歷,每次迭代出兩個變量 codepoint 和 runeValue。codepoint 表示字符起始位置,runeValue 表示對應的 unicode 編碼(類型是 rune)。編碼

字節串的內存表示

若是字符串僅僅是字節數組,那字符串的長度信息是怎麼獲得呢?要是字符串都是字面量的話,長度尚能夠在編譯期計算出來,可是若是字符串是運行時構造的,那長度又是如何獲得的呢?spa

var s1 = "hello" // 靜態字面量
var s2 = ""
for i:=0;i<10;i++ {
  s2 += s1 // 動態構造
}
fmt.Println(len(s1))
fmt.Println(len(s2))
複製代碼

爲解釋這點,就必須瞭解字符串的內存結構,它不單單是前面提到的那個字節數組,編譯器還爲它分配了頭部字段來存儲長度信息和指向底層字節數組的指針,圖示以下,結構很是相似於切片,區別是頭部少了一個容量字段。設計

當咱們將一個字符串變量賦值給另外一個字符串變量時,底層的字節數組是共享的,它只是淺拷貝了頭部字段。指針

字符串是隻讀的

你可使用下標來讀取字符串指定位置的字節,可是你沒法修改這個位置上的字節內容。若是你嘗試使用下標賦值,編譯器在語法上直接拒絕你。

package main

func main() {
	var s = "hello"
	s[0] = 'H'
}
--------
./main.go:5:7: cannot assign to s[0]
複製代碼

切割切割

字符串在內存形式上比較接近於切片,它也能夠像切片同樣進行切割來獲取子串。子串和母串共享底層字節數組。

package main

import "fmt"

func main() {
	var s1 = "hello world"
	var s2 = s1[3:8]
	fmt.Println(s2)
}

-------
lo wo

複製代碼

字節切片和字符串的相互轉換

在使用 Go 語言進行網絡編程時,常常須要未來自網絡的字節流轉換成內存字符串,同時也須要將內存字符串轉換成網絡字節流。Go 語言直接內置了字節切片和字符串的相互轉換語法。

package main

import "fmt"

func main() {
	var s1 = "hello world"
	var b = []byte(s1)  // 字符串轉字節切片
	var s2 = string(b)  // 字節切片轉字符串
	fmt.Println(b)
	fmt.Println(s2)
}

--------
[104 101 108 108 111 32 119 111 114 108 100]
hello world
複製代碼

從節省內存的角度出發,你可能會認爲字節切片和字符串的底層字節數組是共享的。可是事實不是這樣的,底層字節數組會被拷貝。若是內容很大,那麼轉換操做是須要必定成本的。

那爲何須要拷貝呢?由於字節切片的底層數組內容是能夠修改的,而字符串的底層字節數組是隻讀的,若是共享了,就會致使字符串的只讀屬性再也不成立。

閱讀《快學 Go 語言》更多章節,關注公衆號「碼洞」

相關文章
相關標籤/搜索