Go語言進階之路(二):字符串和指針

上一篇文章《 》我們學習了Go語言基礎的一些變量和條件控制語句,結構體等。

這一篇主要學習一下Go語言中的字符串和指針。

一 字符串

字符串是每一門編程語言學習中必不可少部分。

在Python中,字符串可以用單引號包起來,也可以用雙引號包起來,多行字符串可以使用三個單引號或三個雙引號包起來。看下面的代碼:

<code>s = "hello"
s = 'hello'
s = '''I am the first line.
I am the second line.
I am the third line.'''

println(s)/<code>

在Go語言中,單行字符串只能用雙引號包起來(單引號包起來的只能是單個字符),多行字符串用反引號包起來。看下面的代碼:

<code>a := "string"
// b := 'string' // 此行編譯錯誤,單引號包含的只能是單個字符
s := `I am the first line.
I am the second line.
I am the third line.`

fmt.Println(s)/<code>

編碼

Python中默認的字符編碼是Unicode,有必要先來了解一下Unicode字符:

Unicode字符編碼是國際組織指定的一種編碼標準,可以用來表示任意字符。Unicode編碼是一種編碼標準,但並沒有規定字符如何存儲。以漢字“漢”為例,它的 Unicode 碼點是 0x6c49,對應的二進制數是 110110001001001,二進制數有 15 位,這也就說明了它至少需要 2 個字節來表示。可以想象,在 Unicode 字典中往後的字符可能就需要 3 個字節或者 4 個字節,甚至更多字節來表示了。

這就導致了一些問題,計算機怎麼知道這個 2 個字節表示的是一個字符,而不是分別表示兩個字符呢?這裡我們可能會想到,那就取個最大的,假如 Unicode 中最大的字符用 4 字節就可以表示了,那麼我們就將所有的字符都用 4 個字節來表示,不夠的就往前面補 0。這樣確實可以解決編碼問題,但是卻造成了空間的極大浪費,如果是一個英文文檔,那文件大小就大出了 3 倍,這顯然是無法接受的。


這個時候就出現了UTF-8可變長編碼。UTF-8對於不同的字符存儲需要,可以使用不同的字節長度來存儲。比如,ASCII碼的碼值範圍為0~127,只需要一個字節來存儲即可,對於中文,絕大多數中文字都是3個字節即可存儲。這樣,就不用每個字符都使用4個字節來存儲,極大的節省了空間。


Go語言的默認字符編碼是UTF-8。字符串底層使用字節數組來存儲,那麼我們就可以使用len()函數來獲取字符串的長度了。同時,Go語言可以使用[]byte(s)把字符串輕鬆的轉換成字節切片。

底層使用字節數組來存儲,因此字符串可以使用和切片類似的很多操作,比如:

<code>s := "Hello, 世界"
bytes := []byte(s)
fmt.Println(bytes) // 輸出:[72 101 108 108 111 44 32 228 184 150 231 149 140]

for i := 0; i < len(s);i++ {
fmt.Printf("%d %c\\n", i, s[i])
}/<code>

for循環裡那行輸出的是:

Go語言進階之路(二):字符串和指針

等一下,最後輸出的從第7行到第12行有一些奇怪的字符。這是因為,“世界”中的每個字符在UTF-8編碼下佔3個字節,只取出每個字符的其中一個字節來輸出,當然會是亂碼。“Hello, 世界”這個字符串在底層是這樣存儲的:

Go語言進階之路(二):字符串和指針

如果我們想輸出字符串的每個字符,該怎麼辦?

可以使用rune類型,把字符串轉換成rune切片:

<code>s := "Hello, 世界"
r := []rune(s)
fmt.Println(r) // 輸出:[72 101 108 108 111 44 32 19990 30028]

for i := 0; i < len(r);i++ {
fmt.Printf("%d %c\\n", i, r[i])
}
// 輸出:
0 H
1 e
2 l
3 l
4 o
5 ,
6
7 世
8 界/<code>

或者直接使用range來循環字符串:

<code>s := "Hello, 世界"

for i, c := range s {
fmt.Printf("%d %c\\n", i, c)
}
// 輸出:
0 H
1 e
2 l
3 l
4 o
5 ,
6
7 世
10 界/<code>

range會自動把字符串的字符按照UTF-8解碼出來,這樣循環字符串得到的字符就是一個完整的UTF-8字符了,而不是字符中的一個字節。

rune類型

在Go語言中,rune類型就是int的別名。看看官方解釋:

<code>// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.

//int32的別名,幾乎在所有方面等同於int32
//它用來區分字符值和整數值

type rune = int32/<code>

rune是用來區分字符值和整數值的。怎麼區分?對於整數,直接使用int類型就好了。對於字符呢?我們上面說到,Go語言中字符都是使用UTF-8編碼存儲的,從1個到4個字節不等。那麼每一個UTF-8字符,最多也就使用4個字節(32比特),也就是int32的長度,那麼我們就可以使用int32類型來表示任意UTF-8字符的值。因此,我們得到rune類型的字符值之後,直接把這個值當成字符輸出,就可以得到我們想要的字符了,而不是亂碼了。有了這個結論,我們就可以知道,rune在處理中文時特別有用。

字符串方法

Go語言strings模塊中內置了非常多的字符串方法。

Compare(a, b string) int

<code>// Compare returns an integer comparing two strings lexicographically.
// The result will be 0 if a==b, -1 if a < b, and +1 if a > b.
//

// Compare is included only for symmetry with package bytes.
// It is usually clearer and always faster to use the built-in
// string comparison operators ==, , and so on.
func Compare(a, b string) int {...}/<code>

字符串比較方法,傳入a、b參數,返回比較結果。如果a小於b,則返回-1;如果a等於b,返回0;如果a大於b,返回1。Compare函數會按照字符的字典順序比較字符串,見下圖:

<code>s1 := "abc"
s2 := "bac"
fmt.Println(strings.Compare(s1, s2)) // 輸出-1/<code>

但是,官方並不建議使用Compare函數來比較字符串,看Compare源碼中的結束,比較字符串可以直接使用大於、小於、等於號進行比較,並不需要使用Compare函數。

Index(s, substr string) int

子串查找/定位。如果s中包含substr這個子串,函數會返回substr在s中第一次出現的位置;如果不包含,會返回-1。看一下源碼解釋:

<code>// Index returns the index of the first instance of substr in s, or -1 if substr is not present in s.
func Index(s, substr string) int {...}/<code>

使用案例:

<code>s := "I am 中國人"
s1 := "國"
fmt.Println(strings.Index(s, s1)) // 輸出:8/<code>

為什麼輸出是8而不是5呢?這是因為中Index函數返回的是子串所在的字節數位置。“I am ”佔了5個字節,“中”佔了3個字節,因此“國”所在的起始位置就是8。

IndexAny(s, chars string) int

在s中查找chars中的任意字符,如果找到了就返回其位置,沒找到就返回-1。

看一下源碼解釋:

<code>// IndexAny returns the index of the first instance of any Unicode code point
// from chars in s, or -1 if no Unicode code point from chars is present in s.
func IndexAny(s, chars string) int {...}/<code>

注意,這是在s中查找chars裡的任意Unicode字符,不是Unicode字符則會找不到。見下面的例子:

<code>s := "中國人"
s1 := s[3:4]
s2 := s[3:6]
fmt.Println(s1) // 輸出:�
fmt.Println(s2) // 輸出:國
fmt.Println(strings.IndexAny(s, s1)) // 輸出:-1
fmt.Println(strings.IndexAny(s, s2)) // 輸出:3/<code>

LastIndex(s, substr string) int

和Index相反,LastIndex函數在s中查找substr最後一次出現的位置,沒找到則返回-1。看一下官方解釋:

<code>// LastIndex returns the index of the last instance of substr in s, or -1 if substr is not present in s.
func LastIndex(s, substr string) int {...}/<code>

使用案例:

<code>s := "中國 國人"
s2 := s[3:6]
fmt.Println(s2) // 輸出:國
fmt.Println(strings.LastIndex(s, s2)) // 輸出:7/<code>

Join(elems []string, sep string) string

把切片字符串elems用sep字符串連接起來,在處理文件路徑時會非常有用。看一下官方解釋:

<code>// Join concatenates the elements of its first argument to create a single string. The separator
// string sep is placed between elements in the resulting string.
func Join(elems []string, sep string) string {...}/<code>

典型使用案例:

<code>s := []string{"a", "b", "c"}
s1 := "/"
fmt.Println(strings.Join(s, s1)) // 輸出:a/b/c/<code>

HasPrefix(s, prefix string) bool

這個函數和Java中字符串的startsWith方法一樣,看看字符串s是不是以字符串prefix開頭的。看一下源碼和官方解釋:

<code>// HasPrefix tests whether the string s begins with prefix.
func HasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[0:len(prefix)] == prefix
}/<code>

典型使用案例:

<code>s := "中國"
s1 := "中"
s2 := "國"
fmt.Println(strings.HasPrefix(s, s1)) // 輸出:true
fmt.Println(strings.HasPrefix(s, s2)) // 輸出:false/<code>

HasSuffix(s, suffix string) bool

這個函數和Java中字符串的endsWith方法一樣,看看字符串s是不是以字符串suffix結尾的。看一下源碼和官方解釋:

<code>// HasSuffix tests whether the string s ends with suffix. 

func HasSuffix(s, suffix string) bool {
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}/<code>

典型使用案例:

<code>s := "中國"
s1 := "中"
s2 := "國"
fmt.Println(strings.HasSuffix(s, s1)) // 輸出:false
fmt.Println(strings.HasSuffix(s, s2)) // 輸出:true/<code>

Trim(s string, cutset string) string

移除處於s兩端的cutset中包含的任意字符:

<code>s := "中國中國中國"
fmt.Println(strings.Trim(s, "國")) // 輸出:中國中國中
fmt.Println(strings.Trim(s, "中國")) // 輸出:(空)/<code>

我們知道,在Web項目中處理用戶輸入時,經常需要處理掉用戶輸入的空白字符,如果想移除字符串兩邊的空白字符呢?我們試試:

<code>s := "\t  中國中國中國  \t"
fmt.Println(strings.Trim(s, "")) // 輸出:\t 中國中國中國
fmt.Println(strings.Trim(s, " ")) // 輸出:\t 中國中國中國 \t/<code>

怎麼好像沒啥作用?其實,移除兩邊的空白字符應該使用TrimSpace函數。

TrimSpace(s string) string

去除s兩端的空白字符(包括空格、回車、換行、製表符)。

<code>// TrimSpace returns a slice of the string s, with all leading
// and trailing white space removed, as defined by Unicode.
func TrimSpace(s string) string {...}/<code>

看一下使用案例:

<code>s := "\t  中國中國中國  \t"
fmt.Println(strings.TrimSpace(s)) // 輸出:中國中國中國/<code>

Split(s, sep string) []string

用sep來分割字符串s,返回分割後的字符串切片。如果s中不包含sep並且sep不為空,則返回的切片結果中只會有一個元素,那就是s本身;如果sep為空,那麼會把字符串s的每個UTF-8字符都切分開,放入切片結果中返回。來看一下官方解釋:

<code>// Split slices s into all substrings separated by sep and returns a slice of
// the substrings between those separators.
//
// If s does not contain sep and sep is not empty, Split returns a
// slice of length 1 whose only element is s.
//
// If sep is empty, Split splits after each UTF-8 sequence. If both s
// and sep are empty, Split returns an empty slice.
//
// It is equivalent to SplitN with a count of -1.
func Split(s, sep string) []string {...}/<code>

看一下典型使用案例:

<code>s := "中國中國中國"
fmt.Println(strings.Split(s, "")) // 輸出:[中 國 中 國 中 國]
fmt.Println(strings.Split(s, "2")) // 輸出:[中國中國中國]/<code>

那麼,我如果只想分割2次呢?可以使用SplitN函數:

<code>s := "中國中國中國"
fmt.Println(strings.SplitN(s, "", 2)) // 輸出:[中 國中國中國]
fmt.Println(strings.SplitN(s, "2", 2)) // 輸出:[中國中國中國]/<code>


還記得Java的split函數中還可以按照正則表達式來分割字符串,但是在Go語言中字符串函數卻不行。這需要用到Go語言中的正則表達式。在接下來的文章中會重點講這一塊。

二 指針

Go語言和Java、Python顯著不同的一點就是指針。如果你是從Java和Python轉過來的,學起來會費力些,如果你是C++工作者,學起來會非常爽。


Go語言中指針和C++中指針一樣操作,通過取地址符就可以取到變量的地址,賦給一個指針變量。

<code>s := "中國中國中國"
p := &s
fmt.Println(p) // 輸出:0xc0000381f0
fmt.Println(*p) // 輸出:中國中國中國/<code>

p裡面存放的是s的地址,直接輸出p會看到一串16進制的字符串,*p才是指針p所指向的內容。


Go語言中指針的二進制類型是int類型,看一下源碼:

<code>// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr/<code>

一個int類型的變量能夠尋址到計算機的任意位置,因此int類型用來用來存放變量地址綽綽有餘。


函數參數指針

指針在函數傳參時會非常有用。Go語言在調用函數時,會把函數的參數複製一份傳遞過去。如果函數參數是值類型,這時,在函數內部修改參數就不會反映到函數外部了。見下面的例子:

<code>func modifyArray(a [3]int) [3]int{ // 數組是值類型,傳遞給函數時,會複製一份傳過去。
a[1] = 0
return a
}
func main() {
s := [3]int{1, 2, 3}
modifyArray(s)
fmt.Println(s) // 輸出:[1 2 3]
}/<code>

Go語言中,數組是值類型,在調用函數的時候,會把數組拷貝一份傳遞給函數modifyArray,這樣在modifyArray內部修改就不會反映到函數外面去了。

Go語言進階之路(二):字符串和指針

那麼怎麼讓它在函數內部能修改呢?把函數參數改成數組指針類型就可以了(或者可以使用切片類型,在後續文章會詳解切片)。

<code>func modifyArray(a *[3]int) *[3]int{ // 數組是值類型,傳遞給函數時,會複製一份傳過去。
a[1] = 0
return a
}
func main() {
s := [3]int{1, 2, 3}
modifyArray(&s)
fmt.Println(s)
}/<code>

由於調用函數會導致函數參數複製一份,如果函數參數非常大,複製成本會很高,導致性能下降,這時候指針參數就非常有用了。指針中存放的是參數的地址,傳遞參數時直接複製一份指針就可以了。

Go語言進階之路(二):字符串和指針

結構體指針方法

指針在結構體的方法中也很有用。先來看一下下面的例子:

<code>type rect struct { 

width, height int
}

func (r rect) modifyWidth(){ // rect實現了area方法
r.width = 3
}

func (r *rect) modifyWidthPointer(){ // rect實現了perim方法,則rect實現了geometry接口
r.width = 3
}

func main() {
var rec = rect{
width: 0,
height: 0,
}
rec.modifyWidth()
fmt.Println(rec) // 輸出:{0 0}
rec.modifyWidthPointer()
fmt.Println(rec) // 輸出:{3 0}
}/<code>

可以看到,把指針作為方法接收者,則可以在方法中任意修改接收者的屬性並這種修改反映到方法外面。而使用非指針接收者的方法卻修改不了。這是因為,非指針接收者在調用方法modifyWidth時,把本身的值賦值了一份。


下一篇文章我們講一下函數和接口,講一下怎麼用Go語言實現Java中最常見的多態。



分享到:


相關文章: