以下文章來源于公眾號程序猿架構 ,作者程序猿架構
一 字符串
字符串是每一門編程語言學習中必不可少部分。
在Python中民褂,字符串可以用單引號包起來泼舱,也可以用雙引號包起來宪睹,多行字符串可以使用三個單引號或三個雙引號包起來。看下面的代碼:
s = "hello"
s = 'hello'
s = '''I am the first line.
I am the second line.
I am the third line.'''
println(s)
在Go語言中,單行字符串只能用雙引號包起來(單引號包起來的只能是單個字符)额湘,多行字符串用反引號包起來卿吐。看下面的代碼:
a := "string"
// b := 'string' // 此行編譯錯誤锋华,單引號包含的只能是單個字符
s := `I am the first line.
I am the second line.
I am the third line.`
fmt.Println(s)
編碼
Python中默認的字符編碼是Unicode嗡官,有必要先來了解一下Unicode字符:
Unicode字符編碼是國際組織指定的一種編碼標準,可以用來表示任意字符毯焕。Unicode編碼是一種編碼標準衍腥,但并沒有規(guī)定字符如何存儲。以漢字“漢”為例纳猫,它的 Unicode 碼點是 0x6c49婆咸,對應的二進制數(shù)是 110110001001001,二進制數(shù)有 15 位续担,這也就說明了它至少需要 2 個字節(jié)來表示擅耽』詈ⅲ可以想象物遇,在 Unicode 字典中往后的字符可能就需要 3 個字節(jié)或者 4 個字節(jié),甚至更多字節(jié)來表示了憾儒。
這就導致了一些問題询兴,計算機怎么知道這個 2 個字節(jié)表示的是一個字符,而不是分別表示兩個字符呢起趾?這里我們可能會想到诗舰,那就取個最大的,假如 Unicode 中最大的字符用 4 字節(jié)就可以表示了训裆,那么我們就將所有的字符都用 4 個字節(jié)來表示眶根,不夠的就往前面補 0。這樣確實可以解決編碼問題边琉,但是卻造成了空間的極大浪費属百,如果是一個英文文檔,那文件大小就大出了 3 倍变姨,這顯然是無法接受的族扰。
這個時候就出現(xiàn)了UTF-8可變長編碼。UTF-8對于不同的字符存儲需要定欧,可以使用不同的字節(jié)長度來存儲渔呵。比如,ASCII碼的碼值范圍為0~127砍鸠,只需要一個字節(jié)來存儲即可扩氢,對于中文,絕大多數(shù)中文字都是3個字節(jié)即可存儲爷辱。這樣类茂,就不用每個字符都使用4個字節(jié)來存儲耍属,極大的節(jié)省了空間。
Go語言的默認字符編碼是UTF-8巩检。字符串底層使用字節(jié)數(shù)組來存儲厚骗,那么我們就可以使用len()函數(shù)來獲取字符串的長度了。同時兢哭,Go語言可以使用[]byte(s)把字符串輕松的轉換成字節(jié)切片领舰。
底層使用字節(jié)數(shù)組來存儲,因此字符串可以使用和切片類似的很多操作迟螺,比如:
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])
}
for循環(huán)里那行輸出的是:
等一下冲秽,最后輸出的從第7行到第12行有一些奇怪的字符。這是因為矩父,“世界”中的每個字符在UTF-8編碼下占3個字節(jié)锉桑,只取出每個字符的其中一個字節(jié)來輸出,當然會是亂碼窍株∶裰幔“Hello, 世界”這個字符串在底層是這樣存儲的:
如果我們想輸出字符串的每個字符,該怎么辦球订?
可以使用rune類型后裸,把字符串轉換成rune切片:
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 界
或者直接使用range來循環(huán)字符串:
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 界
range會自動把字符串的字符按照UTF-8解碼出來,這樣循環(huán)字符串得到的字符就是一個完整的UTF-8字符了冒滩,而不是字符中的一個字節(jié)微驶。
rune類型
在Go語言中,rune類型就是int的別名开睡∫蚱唬看看官方解釋:
// 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
//它用來區(qū)分字符值和整數(shù)值
type rune = int32
rune是用來區(qū)分字符值和整數(shù)值的篇恒。怎么區(qū)分扶檐?對于整數(shù),直接使用int類型就好了婚度。對于字符呢蘸秘?我們上面說到,Go語言中字符都是使用UTF-8編碼存儲的蝗茁,從1個到4個字節(jié)不等醋虏。那么每一個UTF-8字符,最多也就使用4個字節(jié)(32比特)哮翘,也就是int32的長度颈嚼,那么我們就可以使用int32類型來表示任意UTF-8字符的值。因此饭寺,我們得到rune類型的字符值之后阻课,直接把這個值當成字符輸出叫挟,就可以得到我們想要的字符了,而不是亂碼了限煞。有了這個結論抹恳,我們就可以知道,rune在處理中文時特別有用署驻。
字符串方法
Go語言strings模塊中內置了非常多的字符串方法奋献。
Compare(a, b string) int
// 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 {...}
字符串比較方法,傳入a旺上、b參數(shù)瓶蚂,返回比較結果。如果a小于b宣吱,則返回-1窃这;如果a等于b,返回0征候;如果a大于b杭攻,返回1。Compare函數(shù)會按照字符的字典順序比較字符串倍奢,見下圖:
s1 := "abc"
s2 := "bac"
fmt.Println(strings.Compare(s1, s2)) // 輸出-1
但是朴上,官方并不建議使用Compare函數(shù)來比較字符串垒棋,看Compare源碼中的結束卒煞,比較字符串可以直接使用大于、小于叼架、等于號進行比較畔裕,并不需要使用Compare函數(shù)。
Index(s, substr string) int
子串查找/定位乖订。如果s中包含substr這個子串扮饶,函數(shù)會返回substr在s中第一次出現(xiàn)的位置;如果不包含乍构,會返回-1甜无。看一下源碼解釋:
// 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 {...}
使用案例:
s := "I am 中國人"
s1 := "國"
fmt.Println(strings.Index(s, s1)) // 輸出:8
為什么輸出是8而不是5呢哥遮?這是因為中Index函數(shù)返回的是子串所在的字節(jié)數(shù)位置岂丘。“I am ”占了5個字節(jié)眠饮,“中”占了3個字節(jié)奥帘,因此“國”所在的起始位置就是8。
IndexAny(s, chars string) int
在s中查找chars中的任意字符仪召,如果找到了就返回其位置寨蹋,沒找到就返回-1松蒜。
看一下源碼解釋:
// 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 {...}
注意,這是在s中查找chars里的任意Unicode字符已旧,不是Unicode字符則會找不到秸苗。見下面的例子:
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
LastIndex(s, substr string) int
和Index相反,LastIndex函數(shù)在s中查找substr最后一次出現(xiàn)的位置运褪,沒找到則返回-1难述。看一下官方解釋:
// 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 {...}
使用案例:
s := "中國 國人"
s2 := s[3:6]
fmt.Println(s2) // 輸出:國
fmt.Println(strings.LastIndex(s, s2)) // 輸出:7
Join(elems []string, sep string) string
把切片字符串elems用sep字符串連接起來吐句,在處理文件路徑時會非常有用胁后。看一下官方解釋:
// 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 {...}
典型使用案例:
s := []string{"a", "b", "c"}
s1 := "/"
fmt.Println(strings.Join(s, s1)) // 輸出:a/b/c
HasPrefix(s, prefix string) bool
這個函數(shù)和Java中字符串的startsWith方法一樣嗦枢,看看字符串s是不是以字符串prefix開頭的攀芯。看一下源碼和官方解釋:
// 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
}
典型使用案例:
s := "中國"
s1 := "中"
s2 := "國"
fmt.Println(strings.HasPrefix(s, s1)) // 輸出:true
fmt.Println(strings.HasPrefix(s, s2)) // 輸出:false
HasSuffix(s, suffix string) bool
這個函數(shù)和Java中字符串的endsWith方法一樣文虏,看看字符串s是不是以字符串suffix結尾的侣诺。看一下源碼和官方解釋:
// 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
}
典型使用案例:
s := "中國"
s1 := "中"
s2 := "國"
fmt.Println(strings.HasSuffix(s, s1)) // 輸出:false
fmt.Println(strings.HasSuffix(s, s2)) // 輸出:true
Trim(s string, cutset string) string
移除處于s兩端的cutset中包含的任意字符:
s := "中國中國中國"
fmt.Println(strings.Trim(s, "國")) // 輸出:中國中國中
fmt.Println(strings.Trim(s, "中國")) // 輸出:(空)
我們知道氧秘,在Web項目中處理用戶輸入時年鸳,經常需要處理掉用戶輸入的空白字符,如果想移除字符串兩邊的空白字符呢丸相?我們試試:
s := " 中國中國中國 "
fmt.Println(strings.Trim(s, "")) // 輸出:中國中國中國
fmt.Println(strings.Trim(s, " ")) // 輸出:中國中國中國
怎么好像沒啥作用搔确?其實,移除兩邊的空白字符應該使用TrimSpace函數(shù)灭忠。
TrimSpace(s string) string
去除s兩端的空白字符(包括空格膳算、回車、換行弛作、制表符)涕蜂。
// 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 {...}
看一下使用案例:
s := " 中國中國中國 "
fmt.Println(strings.TrimSpace(s)) // 輸出:中國中國中國
Split(s, sep string) []string
用sep來分割字符串s,返回分割后的字符串切片映琳。如果s中不包含sep并且sep不為空机隙,則返回的切片結果中只會有一個元素,那就是s本身萨西;如果sep為空有鹿,那么會把字符串s的每個UTF-8字符都切分開,放入切片結果中返回原杂。來看一下官方解釋:
// 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 {...}
看一下典型使用案例:
s := "中國中國中國"
fmt.Println(strings.Split(s, "")) // 輸出:[中 國 中 國 中 國]
fmt.Println(strings.Split(s, "2")) // 輸出:[中國中國中國]
那么印颤,我如果只想分割2次呢?可以使用SplitN函數(shù):
s := "中國中國中國"
fmt.Println(strings.SplitN(s, "", 2)) // 輸出:[中 國中國中國]
fmt.Println(strings.SplitN(s, "2", 2)) // 輸出:[中國中國中國]
還記得Java的split函數(shù)中還可以按照正則表達式來分割字符串穿肄,但是在Go語言中字符串函數(shù)卻不行年局。這需要用到Go語言中的正則表達式际看。在接下來的文章中會重點講這一塊。
二 指針
Go語言和Java矢否、Python顯著不同的一點就是指針仲闽。如果你是從Java和Python轉過來的,學起來會費力些僵朗,如果你是C++工作者赖欣,學起來會非常爽。
Go語言中指針和C++中指針一樣操作验庙,通過取地址符就可以取到變量的地址顶吮,賦給一個指針變量。
s := "中國中國中國"
p := &s
fmt.Println(p) // 輸出:0xc0000381f0
fmt.Println(*p) // 輸出:中國中國中國
p里面存放的是s的地址粪薛,直接輸出p會看到一串16進制的字符串悴了,*p才是指針p所指向的內容。
Go語言中指針的二進制類型是int類型违寿,看一下源碼:
// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
一個int類型的變量能夠尋址到計算機的任意位置湃交,因此int類型用來用來存放變量地址綽綽有余。
函數(shù)參數(shù)指針
指針在函數(shù)傳參時會非常有用藤巢。Go語言在調用函數(shù)時搞莺,會把函數(shù)的參數(shù)復制一份傳遞過去。如果函數(shù)參數(shù)是值類型掂咒,這時才沧,在函數(shù)內部修改參數(shù)就不會反映到函數(shù)外部了。見下面的例子:
func modifyArray(a [3]int) [3]int{ // 數(shù)組是值類型俏扩,傳遞給函數(shù)時糜工,會復制一份傳過去弊添。
a[1] = 0
return a
}
func main() {
s := [3]int{1, 2, 3}
modifyArray(s)
fmt.Println(s) // 輸出:[1 2 3]
}
Go語言中录淡,數(shù)組是值類型,在調用函數(shù)的時候油坝,會把數(shù)組拷貝一份傳遞給函數(shù)modifyArray嫉戚,這樣在modifyArray內部修改就不會反映到函數(shù)外面去了。
那么怎么讓它在函數(shù)內部能修改呢澈圈?把函數(shù)參數(shù)改成數(shù)組指針類型就可以了(或者可以使用切片類型彬檀,在后續(xù)文章會詳解切片)。
func modifyArray(a *[3]int) *[3]int{ // 數(shù)組是值類型瞬女,傳遞給函數(shù)時窍帝,會復制一份傳過去。
a[1] = 0
return a
}
func main() {
s := [3]int{1, 2, 3}
modifyArray(&s)
fmt.Println(s)
}
由于調用函數(shù)會導致函數(shù)參數(shù)復制一份诽偷,如果函數(shù)參數(shù)非常大坤学,復制成本會很高疯坤,導致性能下降,這時候指針參數(shù)就非常有用了深浮。指針中存放的是參數(shù)的地址压怠,傳遞參數(shù)時直接復制一份指針就可以了。
結構體指針方法
指針在結構體的方法中也很有用飞苇。先來看一下下面的例子:
type rect struct {
width, height int
}
func (r rect) modifyWidth(){ // rect實現(xiàn)了area方法
r.width = 3
}
func (r *rect) modifyWidthPointer(){ // rect實現(xiàn)了perim方法菌瘫,則rect實現(xiàn)了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}
}
可以看到,把指針作為方法接收者布卡,則可以在方法中任意修改接收者的屬性并這種修改反映到方法外面雨让。而使用非指針接收者的方法卻修改不了。這是因為忿等,非指針接收者在調用方法modifyWidth時宫患,把本身的值賦值了一份。