深度解密Go語言中字符串的使用_第1頁
深度解密Go語言中字符串的使用_第2頁
深度解密Go語言中字符串的使用_第3頁
深度解密Go語言中字符串的使用_第4頁
深度解密Go語言中字符串的使用_第5頁
已閱讀5頁,還剩14頁未讀 繼續(xù)免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

第深度解密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. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論