深度解密Go語言中字符串的使用
Go 字符串實現(xiàn)原理
Go 的字符串有個特性,不管長度是多少,大小都是固定的 16 字節(jié)。
package?main
import?(
????"fmt"
????"unsafe"
)
func?main()?{
????fmt.Println(
????????unsafe.Sizeof("komeiji?satori"),
????)??//?16
????fmt.Println(
????????unsafe.Sizeof("satori"),
????)??//?16
}顯然用鼻子也能猜到原因,Go 的字符串底層并沒有實際保存這些字符,而是保存了一個指針,該指針指向的內(nèi)存區(qū)域負(fù)責(zé)存儲具體的字符。由于指針的大小是固定的,所以不管字符串多長,大小都是相等的。
另外字符串大小是 16 字節(jié),指針是 8 字節(jié),那么剩下的 8 字節(jié)是什么呢?不用想,顯然是長度。下面來驗證一下我們結(jié)論:

以上是 Go 字符串的底層結(jié)構(gòu),位于 runtime/string.go 中。字符串在底層是一個結(jié)構(gòu)體,包含兩個字段,其中 str 是一個 8 字節(jié)的萬能指針,指向一個數(shù)組,數(shù)組里面存儲的就是實際的字符;而 len 則表示長度,也是 8 字節(jié)。
因此結(jié)構(gòu)很清晰了:

str 指向的數(shù)組里面存儲的就是所有的字符,并且類型是 uint8,因為 Go 的字符串默認(rèn)采用 utf-8 編碼。所以一個漢字在 Go 里面占 3 字節(jié),我們先用 Python 舉個例子:
>>>?name?=?"琪露諾"
>>>?[c?for?c?in?name.encode("utf-8")]
[231,?144,?170,?233,?156,?178,?232,?175,?186]
>>>那么對于 Go 而言,底層就是這么存儲的:

我們驗證一下:
package?main
import?"fmt"
func?main()?{
????name?:=?"琪露諾"
????//?長度是?9,不是?3
????fmt.Println(len(name))??//?9
????//?查看底層數(shù)組存儲的值
????//?可以轉(zhuǎn)成切片查看
????fmt.Println(
????????[]byte(name),
????)??//?[231?144?170?233?156?178?232?175?186]
}結(jié)果和我們想的一樣,并且內(nèi)置函數(shù) len 在統(tǒng)計字符串長度時,計算的是底層數(shù)組的長度。
字符串的截取
如果要截取字符串的某個子串,要怎么做呢?如果是 Python 的話很簡單:
>>>?name?=?"琪露諾" >>>?name[0] '琪' >>>?name[:?2] '琪露' >>>
因為 Python 字符串里面的每個字符的大小都是相同的,可能是 1 字節(jié)、2字節(jié)、4字節(jié)。但不管是哪種,一個字符串里面的所有字符都具有相同的大小,因此才能通過索引準(zhǔn)確定位。
但在 Go 里面這種做法行不通,Go 的字符串采用 utf-8 編碼,不同字符占用的大小不同,ASCII 字符占 1 字節(jié),漢字占 3 字節(jié),所以無法通過索引準(zhǔn)確定位。
package?main
import?"fmt"
func?main()?{
????name?:=?"琪露諾"
????fmt.Println(
????????name[0],?name[1],?name[2],
????)??//?231?144?170
????fmt.Println(name[:?3])??//?琪
}如果一個字符串里面既有英文又有中文,那么想通過索引準(zhǔn)確定位是不可能的。因此這個時候我們需要進行轉(zhuǎn)換,讓它像 Python 一樣,每個字符都具有相同的大小。
package?main
import?"fmt"
func?main()?{
????name?:=?"琪露諾"
????//?rune?等價于?int32
????//?此時每個元素統(tǒng)一占?4?字節(jié)
????//?并且?[]rune(name)?的長度才是字符串的字符個數(shù)
????fmt.Println(
????????[]rune(name),
????)?//?[29738?38706?35834]
????//?然后再進行截取
????fmt.Println(
????????string([]rune(name)[0]),
????????string([]rune(name)[:?2]),
????)??//?琪?琪露
}所以對于字符串 "憨pi" 而言,如果是 utf-8 存儲,那么只需要 5 個字節(jié)。但很明顯,基于索引查找指定的字符是不可能的,除非事先知道字符串長什么樣子。如果是轉(zhuǎn)成 []rune 的話,那么需要 12 字節(jié)存儲,內(nèi)存占用變大了,但可以很方便地查找某個字符或者某個子串。
字符串和切片的轉(zhuǎn)換
字符串和切片之間是可以互轉(zhuǎn)的,但切片只能是 uint8 或者 int32 類型,另外 uint8 也可以寫成 byte,int32 可以寫成 rune。
由于 byte 是 1 字節(jié),那么當(dāng)字符串包含漢字,轉(zhuǎn)成 []byte 切片時,一個漢字需要 3 個byte 表示。因此字符串 "憨pi" 轉(zhuǎn)成 []byte 之后,長度為 5。
而 rune 是 4 字節(jié),可以容納所有的字符,那么轉(zhuǎn)成 []rune 切片時,不管什么字符,都只需要一個 rune 表示即可。所以字符串 "憨pi" 轉(zhuǎn)成 []rune 之后,長度為 3。
因此當(dāng)你想統(tǒng)計字符串的字符個數(shù)時,最好轉(zhuǎn)成 []rune 數(shù)組之后再統(tǒng)計。如果是字節(jié)個數(shù),那么直接使用內(nèi)置函數(shù) len 即可。
我們舉例說明,先來看一段 Python 代碼:
>>>?s?=?"憨pi"
#?采用utf-8編碼(等價于Go的[]byte數(shù)組)
#?"憨"?需要?230?134?168?三個整數(shù)來表示
#?而?"p"?和?"i"?均只需?1?個字節(jié),分別為112和105
>>>?[c?for?c?in?s.encode("utf-8")]
[230,?134,?168,?112,?105]
#?采用?unicode?編碼(類似于Go的[]rune數(shù)組)
#?所有字符都只需要1個整數(shù)表示
#?但對于ASCII字符而言,不管什么編碼,對應(yīng)的數(shù)值不變
>>>?[ord(c)?for?c?in?s]
[25000,?112,?105]我們用 Go 再演示一下:
package?main
import?"fmt"
func?main()?{
????s?:=?"憨pi"
????fmt.Println(
????????[]byte(s),
????)?//?[230?134?168?112?105]
????fmt.Println(
????????[]rune(s),
????)??//?[25000?112?105]
}結(jié)果是一樣的,當(dāng)然這個過程我們也可以反向進行:
package?main
import?"fmt"
func?main()?{
????s1?:=?[]byte{230,?134,?168,?112,?105}
????fmt.Println(string(s1))?//?憨pi
????s2?:=?[]rune{25000,?112,?105}
????fmt.Println(string(s2))?//?憨pi
}結(jié)果沒有任何問題。
字符串和切片共享底層數(shù)組
我們知道字符串和切片內(nèi)部都有一個指針,指針指向一個數(shù)組,該數(shù)組存放具體的元素。
//?runtime/string.go
type?stringStruct?struct?{
????str?unsafe.Pointer
????len?int
}
//?runtime/slice.go
type?slice?struct?{
????array?unsafe.Pointer
????len???int
????cap???int
}假設(shè)有一個字符串 "abc",然后基于該字符串創(chuàng)建一個切片,那么兩者的結(jié)構(gòu)如下:

字符串在轉(zhuǎn)成切片的時候,會將底層數(shù)組也拷貝一份。那么問題來了,在基于字符串創(chuàng)建切片的時候,能不能不拷貝數(shù)組呢?也就是下面這個樣子:

如果字符串比較大,或者說需要和切片之間來回轉(zhuǎn)換的話,這種方式無疑會減少大量開銷。Go 提供了萬能指針幫我們實現(xiàn)這一點,所以先來了解一下什么是萬能指針。
什么是萬能指針
我們知道 C 的指針不僅可以相互轉(zhuǎn)換,而且還可以參與運算,但 Go 不行,因為 Go 的指針是類型安全的。Go 編譯器對類型的檢測非常嚴(yán)格,讓你在享受指針帶來的便利時,又給指針施加了很多制約來保證安全。因此 Go 的指針不可以相互轉(zhuǎn)換,也不可以參與運算。
但保證安全是需要以犧牲效率為代價的,如果你能保證寫出的程序就是安全的,那么可以使用 Go 中的萬能指針,從而繞過類型系統(tǒng)的檢測,讓程序運行的更快。
萬能指針在 Go 里面叫做 unsafe.Pointer,它位于 unsafe 包下面。當(dāng)然這個包名看起來有點怪怪的,因為這個包可以讓我們繞過 Go 類型系統(tǒng)的檢測,直接訪問內(nèi)存,從而提升效率。所以它有點危險,而 Go 官方也不推薦開發(fā)者使用,于是起了這個名字。
但實際上 unsafe 包在底層被大量使用,所以不要被名字誤導(dǎo)了,這個包是一定要掌握的。
回到萬能指針上面來,Go 的指針不可以相互轉(zhuǎn)換,但是它們都可以和萬能指針轉(zhuǎn)換。舉個例子:
package?main
import?(
????"fmt"
????"unsafe"
)
func?main()?{
????//?一個?[]int8?類型的切片
????s1?:=?[]int8{1,?2,?3,?4}
????//?如果直接轉(zhuǎn)成?[]int16?是會報錯的
????//?因為?Go?的類型系統(tǒng)不允許這么做
????//?但是有萬能指針,任何指針都可以和它轉(zhuǎn)換
????//?我們可以先將?s1?的指針轉(zhuǎn)成萬能指針
????//?然后再將萬能指針轉(zhuǎn)成?*[]int16,最后再解引用
????s2?:=?*(*[]int16)(unsafe.Pointer(&s1))
????//?那么問題來了,指針雖然轉(zhuǎn)換了
????//?但是內(nèi)存地址沒變,內(nèi)存里的值也沒變
????//?由于?s2?是?[]int16?類型,s1?是?[]int8?類型
????//?所以它會把?s1[0]?和?s1[1]?整體作為?s2[0]
????//?會把?s1[2]?和?s1[3]?整體作為?s2[1]
????fmt.Println(s2)??//?[513?1027?0?0]
????
????//?int8?類型的?1?和?2?組合成?int16?
????//?int8?類型的?3?和?4?組合成?int16?
????fmt.Println(2?<<?8?+?1)??//?513
????fmt.Println(4?<<?8?+?3)??//?1027
}因此把 Go 的萬能指針想象成 C 的空指針 void * 即可。
那么讓字符串和切片共享數(shù)組,我們就可以這么做:
package?main
import?(
????"fmt"
????"unsafe"
)
func?main()?{
????str?:=?"abc"
????slice?:=?*(*[]byte)(unsafe.Pointer(&str))
????fmt.Println(slice)??//?[97?98?99]
????fmt.Println(cap(slice))??//?10036576
}
雖然轉(zhuǎn)換成功了,但是還有點問題,容量不太對勁。至于原因也很簡單,字符串和切片在底層都是結(jié)構(gòu)體,并且它們的前兩個字段相同,所以轉(zhuǎn)換之后打印沒有問題。但字符串沒有容量的概念,它是定長的,所以轉(zhuǎn)成切片的時候 cap 就丟失了,打印的就是亂七八糟的值。
所以我們需要再完善一下:
package?main
import?(
????"fmt"
????"unsafe"
)
func?StringToBytes(s?string)?[]byte {
????//?既然字符串轉(zhuǎn)切片,會丟失容量
????//?那么加上去就好了,做法也很簡單
????//?新建一個結(jié)構(gòu)體,將容量(等于長度)加進去
????return?*(*[]byte)(unsafe.Pointer(
????????&struct?{
????????????string
????????????Cap?int
????????}{s,?len(s)},
????))
}
func?BytesToString(b?[]byte)?string {
????//?切片轉(zhuǎn)字符串就簡單了,直接轉(zhuǎn)即可
????//?轉(zhuǎn)的過程中,切片的?Cap?字段會丟棄
????return?*(*string)(unsafe.Pointer(&b))
}
func?main()?{
????fmt.Println(
????????StringToBytes("abc"),
????)?//?[97?98?99]
????fmt.Println(
????????BytesToString([]byte{97,?98,?99}),
????)?//?abc
}結(jié)果沒有問題,但我們怎么證明它們是共享數(shù)組的呢?很簡單:
package?main
import?(
????"fmt"
????"unsafe"
)
func?main()?{
????slice?:=?[]byte{97,?98,?99}
????str?:=?*(*string)(unsafe.Pointer(&slice))
????fmt.Println(str)??//?abc
????slice[0]?=?'A'
????fmt.Println(str)??//?Abc
}操作切片等于操作底層數(shù)組,而 str 前后的打印結(jié)果不一致,所以確實是共享同一個數(shù)組。但需要注意的是,這里是先創(chuàng)建的切片,因此底層數(shù)組是可以修改的,沒有問題。
但如果創(chuàng)建的是字符串,然后基于字符串得到切片,那么切片就不可以修改了。因為字符串是不可修改的,所以底層數(shù)組也不可修改,也意味著切片不可以修改。
字符串和其它數(shù)據(jù)結(jié)構(gòu)的轉(zhuǎn)化
以上我們就介紹完了字符串的原理,再來看看工作中一些常見的字符串操作。
整數(shù)和字符串相互轉(zhuǎn)換
如果想把一個整數(shù)轉(zhuǎn)成字符串,那么該怎做呢?比如將 97 轉(zhuǎn)成字符串。有過 Python 經(jīng)驗的,應(yīng)該下意識會想到 string(97),但這是不行的,它返回的是字符串 "a",因為 97 對應(yīng)的字符是 'a'。
如果將整數(shù)轉(zhuǎn)成字符串,應(yīng)該使用 strconv 包下的 Itoa 函數(shù),這個和 C 語言類似。
package?main
import?(
????"fmt"
????"strconv"
)
func?main()?{
????fmt.Println(strconv.Itoa(97))
????fmt.Println(strconv.Itoa(97)?==?"97")
????/*
???????97
???????true
????*/
????//?同理,將字符串轉(zhuǎn)成整數(shù)則是?Atoi
????s?:=?"97"
????if?num,?err?:=?strconv.Atoi(s);?err?!=?nil?{
????????fmt.Println(err)
????}?else?{
????????fmt.Println(num?==?97)?//?true
????}
????s?=?"97xx"?
????if?num,?err?:=?strconv.Atoi(s);?err?!=?nil?{
????????fmt.Println(
????????????err,
????????)??//?strconv.Atoi:?parsing?"97xx":?invalid?syntax
????}?else?{
????????fmt.Println(num)
????}
}Atoi 和 Itoa 專門用于整數(shù)和字符串之間的轉(zhuǎn)換,strconv 這個包還提供了 Format 系列和 Parse 系列的函數(shù),用于其它數(shù)據(jù)結(jié)構(gòu)和字符串之間的轉(zhuǎn)換,當(dāng)然里面也包括整數(shù)。
Parse 系列函數(shù)
Parse 一類函數(shù)用于轉(zhuǎn)換字符串為給定類型的值。
ParseBool
將指定字符串轉(zhuǎn)換為對應(yīng)的bool類型,只接受 1、0、t、f、T、F、true、false、True、False、TRUE、FALSE,否則返回錯誤;
package?main
import?(
????"fmt"
????"strconv"
)
func?main()?{
????//因為字符串轉(zhuǎn)換時可能發(fā)生失敗,因此都會帶一個error
????//而這里解析成功了,所以?error?是?nil
????fmt.Println(strconv.ParseBool("1"))??//?true?<nil>
????fmt.Println(strconv.ParseBool("F"))?//?false?<nil>
}ParseInt
函數(shù)原型:func ParseInt(s string, base int, bitSize int) (i int64, err error)
- s:轉(zhuǎn)成 int 的字符串;
- base:指定進制(2 到 36),如果 base 為 0,那么會從字符串的前綴來判斷,如 0x 表示 16 進制等等,如果前綴也沒有那么默認(rèn)是 10 進制;
- bistSize:整數(shù)類型,0、8、16、32、64 分別代表 int、int8、int16、int32、int64;
返回的 err 是 *NumErr 類型,如果語法有誤,err.Error = ErrSyntax;如果結(jié)果超出范圍,err.Error = ErrRange。
package?main
import?(
????"fmt"
????"strconv"
)
func?main()?{
????fmt.Println(
????????strconv.ParseInt("0x16",?0,?0),
????)??//?22?<nil>
????fmt.Println(
????????strconv.ParseInt("16",?16,?0),
????)??//?22?<nil>
????fmt.Println(
????????strconv.ParseInt("16",?0,?0),
????)??//?16?<nil>
????fmt.Println(
????????strconv.ParseInt("016",?0,?0),?
????)??//?14?<nil>
????//進制為?2,但是字符串出現(xiàn)了?6,無法解析
????fmt.Println(
????????strconv.ParseInt("16",?2,?0),?
????)?//?0?strconv.ParseInt:?parsing?"16":?invalid?syntax
????//只指定?8?位,顯然存不下
????fmt.Println(
????????strconv.ParseInt("257",?0,?8),
????)?//?127?strconv.ParseInt:?parsing?"257":?value?out?of?range
????//還可以指定正負(fù)號
????fmt.Println(
????????strconv.ParseInt("-0x16",?0,?0),
????)?//?-22?<nil>
????fmt.Println(
????????strconv.ParseInt("-016",?0,?0),
????)??//?-14?<nil>
}ParseUint
ParseUint 類似 ParseInt,但不接受正負(fù)號,用于無符號整型。
ParseFloat
函數(shù)原型:func ParseFloat(s string, bitSize int) (f float64, err error),其中 bitSize為:32、64,表示對應(yīng)精度的 float
package?main
import?(
????"fmt"
????"strconv"
)
func?main()?{
????fmt.Println(
????????strconv.ParseFloat("3.14",?64),?
????)??//3.14?<nil>
}Format 系列函數(shù)
Format 系列函數(shù)就比較簡單了,就是將指定類型的數(shù)據(jù)格式化成字符串,Parse 則是將字符串解析成指定數(shù)據(jù)類型,這兩個是相反的。另外轉(zhuǎn)成字符串的話,則不需要擔(dān)心 error 了。
FormatBool
package?main
import?(
????"fmt"
????"strconv"
)
func?main()?{
????//?如果是?Parse?系列的話會返回兩個值,?因為可能會出錯
????//?所以多一個?error,?因此需要兩個變量來接收
????//?而?Format?系列則無需擔(dān)心,?因為轉(zhuǎn)成字符串是不會出錯的
????//?所以只返回一個值,?接收的時候只需要一個變量即可
????fmt.Println(
????????strconv.FormatBool(true),
????)??//true
????fmt.Println(
????????strconv.FormatBool(false)?==?"false",
????)??//true
}FormatInt
傳入字符串和指定的進制。
package?main
import?(
????"fmt"
????"strconv"
)
?func?main()?{
????//?數(shù)值是?24,但它是?16?進制的
????//?所以對應(yīng)成?10?進制是?18
????fmt.Println(
????????strconv.FormatInt(24,?16),
????)??//?18
}FormatUint
是 FormatInt 的無符號版本,兩者差別不大。
FormatFloat
函數(shù)原型:func FormatFloat(f float64, fmt byte, prec, bitSize int) string,作用是將浮點數(shù)轉(zhuǎn)成為字符串并返回。
- f:浮點數(shù);
- fmt:表示格式,'f'(-ddd.dddd)、'b'(-ddddp±ddd,指數(shù)為二進制)、'e'(-d.dddde±dd,十進制指數(shù))、'E'(-d.ddddE±dd,十進制指數(shù))、'g'(指數(shù)很大時用'e'格式,否則'f'格式)、'G'(指數(shù)很大時用'E'格式,否則'f'格式);
- prec:prec 控制精度(排除指數(shù)部分),當(dāng) fmt 為 'f'、'e'、'E',它表示小數(shù)點后的數(shù)字個數(shù);為 'g'、'G',它表示總的數(shù)字個數(shù)。如果 prec 為 -1,則代表使用最少數(shù)量的、但又必需的數(shù)字來表示 f;
- bitSize:f 是哪一種精度的 float,32 或者 64;
package?main
import?(
????"fmt"
????"strconv"
)
func?main()?{
????fmt.Println(
????????strconv.FormatFloat(3.1415,?'f',?-1,?64))
????fmt.Println(
????????strconv.FormatFloat(3.1415,?'e',?-1,?64))
????fmt.Println(
????????strconv.FormatFloat(3.1415,?'E',?-1,?64))
????fmt.Println(
????????strconv.FormatFloat(3.1415,?'g',?-1,?64))
????/*
????3.1415
????3.1415e+00
????3.1415E+00
????3.1415
?????*/
}小結(jié)
- 字符串底層是一個結(jié)構(gòu)體,內(nèi)部不存儲實際數(shù)據(jù),而是只保存一個指針和一個長度;
- 字符串采用 utf-8 編碼,這種編碼的特點是省內(nèi)存,但是無法通過索引準(zhǔn)確定位字符和截取子串;
- 字符串可以和 []byte、[]rune 類型的切片互相轉(zhuǎn)換,特別是 []rune,如果想計算字符長度或者截取子串,需要轉(zhuǎn)成 []rune;
- 字符串和切片之間可以共享底層數(shù)組,其實現(xiàn)的核心就在于萬能指針;
以上就是深度解密Go語言中字符串的使用的詳細(xì)內(nèi)容,更多關(guān)于Go語言 字符串的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
使用go net實現(xiàn)簡單的redis通信協(xié)議
本文主要介紹了go net實現(xiàn)簡單的redis通信協(xié)議,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-12-12
Golang中int類型和字符串類型相互轉(zhuǎn)換的實現(xiàn)方法
在日常開發(fā)中,經(jīng)常需要將數(shù)字轉(zhuǎn)換為字符串或者將字符串轉(zhuǎn)換為數(shù)字,在 Golang 中,有一些很簡便的方法可以實現(xiàn)這個功能,接下來就詳細(xì)講解一下如何實現(xiàn) int 類型和字符串類型之間的互相轉(zhuǎn)換,需要的朋友可以參考下2023-09-09
Golang自定義結(jié)構(gòu)體轉(zhuǎn)map的操作
這篇文章主要介紹了Golang自定義結(jié)構(gòu)體轉(zhuǎn)map的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
GO語言標(biāo)準(zhǔn)錯誤處理機制error用法實例
這篇文章主要介紹了GO語言標(biāo)準(zhǔn)錯誤處理機制error用法,實例分析了錯誤處理機制的具體用法,具有一定的參考借鑒價值,需要的朋友可以參考下2014-12-12
Go語言fsnotify接口實現(xiàn)監(jiān)測文件修改
這篇文章主要為大家介紹了Go語言fsnotify接口實現(xiàn)監(jiān)測文件修改的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-06-06
go 迭代string數(shù)組操作 go for string[]
這篇文章主要介紹了go 迭代string數(shù)組操作 go for string[],具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12

