




版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領
文檔簡介
第深度解密Go語言中字符串的使用目錄Go字符串實現原理字符串的截取字符串和切片的轉換字符串和切片共享底層數組什么是萬能指針字符串和其它數據結構的轉化整數和字符串相互轉換Parse系列函數Format系列函數小結
Go字符串實現原理
Go的字符串有個特性,不管長度是多少,大小都是固定的16字節(jié)。
package
main
import
(
"fmt"
"unsafe"
func
main()
{
fmt.Println(
unsafe.Sizeof("komeiji
satori"),
)
//
16
fmt.Println(
unsafe.Sizeof("satori"),
)
//
16
}
顯然用鼻子也能猜到原因,Go的字符串底層并沒有實際保存這些字符,而是保存了一個指針,該指針指向的內存區(qū)域負責存儲具體的字符。由于指針的大小是固定的,所以不管字符串多長,大小都是相等的。
另外字符串大小是16字節(jié),指針是8字節(jié),那么剩下的8字節(jié)是什么呢?不用想,顯然是長度。下面來驗證一下我們結論:
以上是Go字符串的底層結構,位于runtime/string.go中。字符串在底層是一個結構體,包含兩個字段,其中str是一個8字節(jié)的萬能指針,指向一個數組,數組里面存儲的就是實際的字符;而len則表示長度,也是8字節(jié)。
因此結構很清晰了:
str指向的數組里面存儲的就是所有的字符,并且類型是uint8,因為Go的字符串默認采用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
//
查看底層數組存儲的值
//
可以轉成切片查看
fmt.Println(
[]byte(name),
)
//
[231
144
170
233
156
178
232
175
186]
}
結果和我們想的一樣,并且內置函數len在統計字符串長度時,計算的是底層數組的長度。
字符串的截取
如果要截取字符串的某個子串,要怎么做呢?如果是Python的話很簡單:
name
=
"琪露諾"
name[0]
name[:
2]
因為Python字符串里面的每個字符的大小都是相同的,可能是1字節(jié)、2字節(jié)、4字節(jié)。但不管是哪種,一個字符串里面的所有字符都具有相同的大小,因此才能通過索引準確定位。
但在Go里面這種做法行不通,Go的字符串采用utf-8編碼,不同字符占用的大小不同,ASCII字符占1字節(jié),漢字占3字節(jié),所以無法通過索引準確定位。
package
main
import
"fmt"
func
main()
{
name
:=
"琪露諾"
fmt.Println(
name[0],
name[1],
name[2],
)
//
231
144
170
fmt.Println(name[:
3])
//
琪
}
如果一個字符串里面既有英文又有中文,那么想通過索引準確定位是不可能的。因此這個時候我們需要進行轉換,讓它像Python一樣,每個字符都具有相同的大小。
package
main
import
"fmt"
func
main()
{
name
:=
"琪露諾"
//
rune
等價于
int32
//
此時每個元素統一占
4
字節(jié)
//
并且
[]rune(name)
的長度才是字符串的字符個數
fmt.Println(
[]rune(name),
)
//
[29738
38706
35834]
//
然后再進行截取
fmt.Println(
string([]rune(name)[0]),
string([]rune(name)[:
2]),
)
//
琪
琪露
}
所以對于字符串憨pi而言,如果是utf-8存儲,那么只需要5個字節(jié)。但很明顯,基于索引查找指定的字符是不可能的,除非事先知道字符串長什么樣子。如果是轉成[]rune的話,那么需要12字節(jié)存儲,內存占用變大了,但可以很方便地查找某個字符或者某個子串。
字符串和切片的轉換
字符串和切片之間是可以互轉的,但切片只能是uint8或者int32類型,另外uint8也可以寫成byte,int32可以寫成rune。
由于byte是1字節(jié),那么當字符串包含漢字,轉成[]byte切片時,一個漢字需要3個byte表示。因此字符串憨pi轉成[]byte之后,長度為5。
而rune是4字節(jié),可以容納所有的字符,那么轉成[]rune切片時,不管什么字符,都只需要一個rune表示即可。所以字符串憨pi轉成[]rune之后,長度為3。
因此當你想統計字符串的字符個數時,最好轉成[]rune數組之后再統計。如果是字節(jié)個數,那么直接使用內置函數len即可。
我們舉例說明,先來看一段Python代碼:
s
=
"憨pi"
#
采用utf-8編碼(等價于Go的[]byte數組)
#
"憨"
需要
230
134
168
三個整數來表示
#
而
"p"
和
"i"
均只需
1
個字節(jié),分別為112和105
[c
for
c
in
s.encode("utf-8")]
[230,
134,
168,
112,
105]
#
采用
unicode
編碼(類似于Go的[]rune數組)
#
所有字符都只需要1個整數表示
#
但對于ASCII字符而言,不管什么編碼,對應的數值不變
[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]
}
結果是一樣的,當然這個過程我們也可以反向進行:
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
}
結果沒有任何問題。
字符串和切片共享底層數組
我們知道字符串和切片內部都有一個指針,指針指向一個數組,該數組存放具體的元素。
//
runtime/string.go
type
stringStruct
struct
{
str
unsafe.Pointer
len
int
//
runtime/slice.go
type
slice
struct
{
array
unsafe.Pointer
len
int
cap
int
}
假設有一個字符串abc,然后基于該字符串創(chuàng)建一個切片,那么兩者的結構如下:
字符串在轉成切片的時候,會將底層數組也拷貝一份。那么問題來了,在基于字符串創(chuàng)建切片的時候,能不能不拷貝數組呢?也就是下面這個樣子:
如果字符串比較大,或者說需要和切片之間來回轉換的話,這種方式無疑會減少大量開銷。Go提供了萬能指針幫我們實現這一點,所以先來了解一下什么是萬能指針。
什么是萬能指針
我們知道C的指針不僅可以相互轉換,而且還可以參與運算,但Go不行,因為Go的指針是類型安全的。Go編譯器對類型的檢測非常嚴格,讓你在享受指針帶來的便利時,又給指針施加了很多制約來保證安全。因此Go的指針不可以相互轉換,也不可以參與運算。
但保證安全是需要以犧牲效率為代價的,如果你能保證寫出的程序就是安全的,那么可以使用Go中的萬能指針,從而繞過類型系統的檢測,讓程序運行的更快。
萬能指針在Go里面叫做unsafe.Pointer,它位于unsafe包下面。當然這個包名看起來有點怪怪的,因為這個包可以讓我們繞過Go類型系統的檢測,直接訪問內存,從而提升效率。所以它有點危險,而Go官方也不推薦開發(fā)者使用,于是起了這個名字。
但實際上unsafe包在底層被大量使用,所以不要被名字誤導了,這個包是一定要掌握的。
回到萬能指針上面來,Go的指針不可以相互轉換,但是它們都可以和萬能指針轉換。舉個例子:
package
main
import
(
"fmt"
"unsafe"
func
main()
{
//
一個
[]int8
類型的切片
s1
:=
[]int8{1,
2,
3,
4}
//
如果直接轉成
[]int16
是會報錯的
//
因為
Go
的類型系統不允許這么做
//
但是有萬能指針,任何指針都可以和它轉換
//
我們可以先將
s1
的指針轉成萬能指針
//
然后再將萬能指針轉成
*[]int16,最后再解引用
s2
:=
*(*[]int16)(unsafe.Pointer(s1))
//
那么問題來了,指針雖然轉換了
//
但是內存地址沒變,內存里的值也沒變
//
由于
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*即可。
那么讓字符串和切片共享數組,我們就可以這么做:
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
雖然轉換成功了,但是還有點問題,容量不太對勁。至于原因也很簡單,字符串和切片在底層都是結構體,并且它們的前兩個字段相同,所以轉換之后打印沒有問題。但字符串沒有容量的概念,它是定長的,所以轉成切片的時候cap就丟失了,打印的就是亂七八糟的值。
所以我們需要再完善一下:
package
main
import
(
"fmt"
"unsafe"
func
StringToBytes(s
string)
[]byte{
//
既然字符串轉切片,會丟失容量
//
那么加上去就好了,做法也很簡單
//
新建一個結構體,將容量(等于長度)加進去
return
*(*[]byte)(unsafe.Pointer(
struct
{
string
Cap
int
}{s,
len(s)},
))
func
BytesToString(b
[]byte)
string{
//
切片轉字符串就簡單了,直接轉即可
//
轉的過程中,切片的
Cap
字段會丟棄
return
*(*string)(unsafe.Pointer(b))
func
main()
{
fmt.Println(
StringToBytes("abc"),
)
//
[97
98
99]
fmt.Println(
BytesToString([]byte{97,
98,
99}),
)
//
abc
}
結果沒有問題,但我們怎么證明它們是共享數組的呢?很簡單:
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
}
操作切片等于操作底層數組,而str前后的打印結果不一致,所以確實是共享同一個數組。但需要注意的是,這里是先創(chuàng)建的切片,因此底層數組是可以修改的,沒有問題。
但如果創(chuàng)建的是字符串,然后基于字符串得到切片,那么切片就不可以修改了。因為字符串是不可修改的,所以底層數組也不可修改,也意味著切片不可以修改。
字符串和其它數據結構的轉化
以上我們就介紹完了字符串的原理,再來看看工作中一些常見的字符串操作。
整數和字符串相互轉換
如果想把一個整數轉成字符串,那么該怎做呢?比如將97轉成字符串。有過Python經驗的,應該下意識會想到string(97),但這是不行的,它返回的是字符串a,因為97對應的字符是a。
如果將整數轉成字符串,應該使用strconv包下的Itoa函數,這個和C語言類似。
package
main
import
(
"fmt"
"strconv"
func
main()
{
fmt.Println(strconv.Itoa(97))
fmt.Println(strconv.Itoa(97)
==
"97")
/*
97
true
*/
//
同理,將字符串轉成整數則是
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專門用于整數和字符串之間的轉換,strconv這個包還提供了Format系列和Parse系列的函數,用于其它數據結構和字符串之間的轉換,當然里面也包括整數。
Parse系列函數
Parse一類函數用于轉換字符串為給定類型的值。
ParseBool
將指定字符串轉換為對應的bool類型,只接受1、0、t、f、T、F、true、false、True、False、TRUE、FALSE,否則返回錯誤;
package
main
import
(
"fmt"
"strconv"
func
main()
{
//因為字符串轉換時可能發(fā)生失敗,因此都會帶一個error
//而這里解析成功了,所以
error
是
nil
fmt.Println(strconv.ParseBool("1"))
//
true
nil
fmt.Println(strconv.ParseBool("F"))
//
false
nil
}
ParseInt
函數原型:funcParseInt(sstring,baseint,bitSizeint)(iint64,errerror)
s:轉成int的字符串;base:指定進制(2到36),如果base為0,那么會從字符串的前綴來判斷,如0x表示16進制等等,如果前綴也沒有那么默認是10進制;bistSize:整數類型,0、8、16、32、64分別代表int、int8、int16、int32、int64;
返回的err是*NumErr類型,如果語法有誤,err.Error=ErrSyntax;如果結果超出范圍,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,但是字符串出現了
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
//還可以指定正負號
fmt.Println(
strconv.ParseInt("-0x16",
0,
0),
)
//
-22
nil
fmt.Println(
strconv.ParseInt("-016",
0,
0),
)
//
-14
nil
}
ParseUint
ParseUint類似ParseInt,但不接受正負號,用于無符號整型。
ParseFloat
函數原型:funcParseFloat(sstring,bitSizeint)(ffloat64,errerror),其中bitSize為:32、64,表示對應精度的float
package
main
import
(
"fmt"
"strconv"
func
main()
{
fmt.Println(
strconv.ParseFloat("3.14",
64),
)
//3.14
nil
}
Format系列函數
Format系列函數就比較簡單了,就是將指定類型的數據格式化成字符串,Parse則是將字符串解析成指定數據類型,這兩個是相反的。另外轉成字符串的話,則不需要擔心error了。
FormatBool
package
main
import
(
"fmt"
"strconv"
func
main()
{
//
如果是
Parse
系列的話會返回兩個值,
因為可能會出錯
//
所以多一個
error,
因此需要兩個變量來接收
//
而
Format
系列則無需擔心,
因為轉成字符串是不會出錯的
//
所以只返回一個值,
接收的時候只需要一個變量即可
fmt.Println(
strconv.FormatBool(true),
)
//true
fmt.Println(
strconv.FormatBool(false)
==
"false",
)
//true
}
FormatInt
傳入字符串和指定的進制。
package
main
import
(
"fmt"
"strconv"
?func
main()
{
//
數值是
24,但它是
16
進制的
//
所以對應成
10
進制是
18
fmt.Println(
strconv.FormatInt(24,
16),
)
//
18
}
FormatUint
是F
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
- 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
- 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2025年城市軌道交通起重裝卸機械操作工職業(yè)技能鑒定試卷
- 2025年國家安全生產監(jiān)督管理總局公務員錄用考試面試真題試卷(結構化小組)
- 2025年高壓成套電器項目申請報告
- 2025年保育員(三級)考試試卷深度分析與備考指南
- 與離婚協議書補充協議
- 2025年PETS二級英語聽力理解能力提升試卷(含2025年真題解析)
- 和珅的做人之道
- 2025年保育員實操技能試卷:幼兒教育心理輔導實踐創(chuàng)新案例分析
- 2025年電子商務師(高級)職業(yè)技能鑒定試卷:熱點問題解答與案例分析
- 2025年服裝設計師(服裝設計實踐應用)考試試題
- GB/T 14294-1993組合式空調機組
- GA 1517-2018金銀珠寶營業(yè)場所安全防范要求
- 提高痰留取成功率PDCA課件
- 一級建造師繼續(xù)教育考試題(重點)
- 組合導航與融合導航解析課件
- 伊金霍洛旗事業(yè)編招聘考試《行測》歷年真題匯總及答案解析精選V
- 深基坑支護工程驗收表
- 工期的保證體系及保證措施
- 顱腦CT影像課件
- 同濟大學論文答辯通用PPT模板
- 課程設計-整體式肋梁樓蓋設計
評論
0/150
提交評論