一、字符串
參考
【golang】淺析rune纠永,byte
golang string和[]byte的對比
在源碼中builtin.go中使用了類型別名(基礎知識可參考Golang關鍵字--type 類型定義)
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8
// 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.
type rune = int32
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
rune類型的底層類型是int32類型钢属,而byte類型的底層類型是int8類型,這決定了rune能比byte表達更多的數(shù)。
字符串是一系列8位字節(jié)的集合睡蟋,通常但不一定代表UTF-8編碼的文本。字符串可以為空枷颊,但不能為nil戳杀。而且字符串的值是不能改變的。不同的語言字符串有不同的實現(xiàn)夭苗,在go的源碼中src/runtime/string.go信卡,string的定義如下:
type stringStruct struct {
str unsafe.Pointer
len int
}
可以看到str其實是個指針,指向某個數(shù)組的首地址题造,另一個字段是len長度傍菇。那到這個數(shù)組是什么呢? 在實例化這個stringStruct的時候:
func gostringnocopy(str *byte) string {
ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
s := *(*string)(unsafe.Pointer(&ss))
return s
}
哈哈界赔,其實就是byte數(shù)組丢习,而且要注意string其實就是個struct牵触。
在unicode中,一個中文占兩個字節(jié)咐低,utf-8中一個中文占三個字節(jié)揽思,golang默認的編碼是utf-8編碼,因此默認一個中文占三個字節(jié)见擦,但是golang中的字符串底層實際上是一個byte數(shù)組钉汗。因此可能會出現(xiàn)下面這種奇怪的情況
str := "hello 世界"
fmt.Println(len(str)) //12
我們期望得到的結果應該是8,原因是golang中的string底層是由一個byte數(shù)組實現(xiàn)的鲤屡,而golang默認的編碼是utf-8损痰,因此在這里一個中文字符占3個字節(jié),所以獲得的長度是12酒来,想要獲得我們想要的結果也很簡單卢未,golang中的unicode/utf8包提供了用utf-8獲取長度的方法
str := "hello 世界"
fmt.Println(utf8.RuneCountInString(str)) //8
上面說了byte類型實際上是一個int8類型,int8適合表達ascii編碼的字符堰汉,而int32可以表達更多的數(shù)尝丐,可以更容易的處理unicode字符,因此衡奥,我們可以通過rune類型來處理unicode字符
str := "hello 世界"
str2 := []rune(str)
fmt.Println(len(str2)) //8
這里會將申請一塊內存爹袁,然后將str的內容復制到這塊內存,實際上這塊內存是一個rune類型的切片矮固,而str2拿到的是一個rune類型的切片的引用失息,我們可以很容易的證明這是一個引用
str := "hello 世界"
str2 := []rune(str)
t := str2
t[0] = 'w'
fmt.Println(string(str2)) //“wello 世界”
通過把str2賦值給t,t上改變的數(shù)據(jù)档址,實際上是改變的是t指向的rune切片盹兢,因此,str也會跟著改變
對于字符串守伸,看一下如何遍歷吧绎秒,也許你會覺得遍歷輕而易舉,然而剛接觸golang的時候尼摹,如果這樣遍歷字符串见芹,那么將是非常糟糕的
str := "hello 世界"
for i := 0;i < len(str);i++ {
fmt.Println(string(str[i]))
}
輸出:
h
e
l
l
o
?
?
?
如何解決這個問題呢?
第一個解決方法是用range循環(huán)
str := "hello 世界"
for _,v := range str {
fmt.Println(string(v))
}
輸出
h
e
l
l
o
界
原因是range會隱式的unicode解碼
第二個方法是將str 轉換為rune類型的切片蠢涝,這個方法上面已經(jīng)說過了玄呛,這里就不再贅述了
當然還有很多方法,其本質都是將byte向rune上靠
結論:
byte 等同于int8和二,常用來處理ascii字符
rune 等同于int32,常用來處理unicode或utf-8字符
以下部分參考《快學 Go 語言》第 7 課 —— 字符串
go語言中是按utf8來存儲字符串的徘铝,從在線查看字符編碼中可以看到“嘻哈”的utf8編碼是e598bb e59388。
1.通過下標來訪問內部字節(jié)數(shù)組具體位置上的字節(jié)
func main() {
var s = "嘻哈china"
for i:=0;i<len(s);i++ {
fmt.Printf("%x ", s[i])
}
}
-----------
e5 98 bb e5 93 88 63 68 69 6e 61
2.對字符串進行 range 遍歷惕它,每次迭代出兩個變量 codepoint 和 runeValue怕午。codepoint 表示字符起始位置,runeValue 表示對應的 unicode 編碼(類型是 rune)淹魄。
package main
import "fmt"
func main() {
var s = "嘻哈china"
for codepoint, runeValue := range s {
fmt.Printf("%d %d ", codepoint, int32(runeValue))
}
}
-----------
0 22075 3 21704 6 99 7 104 8 105 9 110 10 97
對字符串進行 range 遍歷郁惜,每次迭代出兩個變量 codepoint 和 runeValue。codepoint 表示字符起始位置揭北,runeValue 表示對應的 unicode 編碼(類型是 rune)。
3.字符串是只讀的
你可以使用下標來讀取字符串指定位置的字節(jié)吏颖,但是你無法修改這個位置上的字節(jié)內容搔体。如果你嘗試使用下標賦值,編譯器在語法上直接拒絕你半醉。
package main
func main() {
var s = "hello"
s[0] = 'H'
}
--------
./main.go:5:7: cannot assign to s[0]
字符串的值是不能改變的疚俱,這句話其實不完整,應該說字符串的值不能被更改缩多,但可以被替換呆奕。 還是以string的結構體來解釋吧,所有的string在底層都是這樣的一個結構體stringStruct{str: str_point, len: str_len}
衬吆,string結構體的str指針指向的是一個字符常量的地址梁钾, 這個地址里面的內容是不可以被改變的,因為它是只讀的逊抡,但是這個指針可以指向不同的地址姆泻,我們來對比一下string、[]byte類型重新賦值的區(qū)別:
s := "A1" // 分配存儲"A1"的內存空間冒嫡,s結構體里的str指針指向這快內存
s = "A2" // 重新給"A2"的分配內存空間拇勃,s結構體里的str指針指向這快內存
其實[]byte和string的差別是更改變量的時候array的內容可以被更改。
s := []byte{1} // 分配存儲1數(shù)組的內存空間孝凌,s結構體的array指針指向這個數(shù)組方咆。
s = []byte{2} // 將array的內容改為2
因為string的指針指向的內容是不可以更改的,所以每更改一次字符串蟀架,就得重新分配一次內存瓣赂,之前分配空間的還得由gc回收,這是導致string操作低效的根本原因片拍。
4.字節(jié)切片和字符串的相互轉換
在使用 Go 語言進行網(wǎng)絡編程時钩述,經(jīng)常需要將來自網(wǎng)絡的字節(jié)流轉換成內存字符串,同時也需要將內存字符串轉換成網(wǎng)絡字節(jié)流穆碎。Go 語言直接內置了字節(jié)切片和字符串的相互轉換語法牙勘。
package main
import "fmt"
func main() {
var s1 = "hello world"
var b = []byte(s1) // 字符串轉字節(jié)切片
var s2 = string(b) // 字節(jié)切片轉字符串
fmt.Println(b)
fmt.Println(s2)
}
--------
[104 101 108 108 111 32 119 111 114 108 100]
hello world
從節(jié)省內存的角度出發(fā),你可能會認為字節(jié)切片和字符串的底層字節(jié)數(shù)組是共享的。但是事實不是這樣的方面,底層字節(jié)數(shù)組會被拷貝放钦。如果內容很大,那么轉換操作是需要一定成本的恭金。
那為什么需要拷貝呢操禀?因為字節(jié)切片的底層數(shù)組內容是可以修改的,而字符串的底層字節(jié)數(shù)組是只讀的横腿,如果共享了颓屑,就會導致字符串的只讀屬性不再成立。
既然string就是一系列字節(jié)耿焊,而[]byte也可以表達一系列字節(jié)揪惦,那么實際運用中應當如何取舍?
- string可以直接比較罗侯,而[]byte不可以器腋,所以[]byte不可以當map的key值。
- 因為無法修改string中的某個字符钩杰,需要粒度小到操作一個字符時纫塌,用[]byte。
- string值不可為nil讲弄,所以如果你想要通過返回nil表達額外的含義措左,就用[]byte。
- []byte切片這么靈活避除,想要用切片的特性就用[]byte媳荒。
- 需要大量字符串處理的時候用[]byte,性能好很多驹饺。
5.修改字符串
在 Go 語言中钳枕,字符串的內容是不能修改的,也就是說赏壹,你不能用 s[0] 這種方式修改字符串中的 UTF-8 編碼鱼炒,如果你一定要修改,那么你可以將字符串的內容復制到一個可寫的緩沖區(qū)中蝌借,然后再進行修改昔瞧。這樣的緩沖區(qū)一般是 []byte 或 []rune。如果要對字符串中的字節(jié)進行修改菩佑,則轉換為 []byte 格式自晰,如果要對字符串中的字符進行修改,則轉換為 []rune 格式稍坯,轉換過程會自動復制數(shù)據(jù)酬荞。
angel := "Heros never die"
angleBytes := []byte(angel)
for i := 5; i <= 10; i++ {
angleBytes[i] = ' '
}
fmt.Println(string(angleBytes))
字符串不可變有很多好處搓劫,如天生線程安全,大家使用的都是只讀對象混巧,無須加鎖枪向;再者,方便內存共享咧党,而不必使用寫時復制(Copy On Write)等技術秘蛔;字符串 hash 值也只需要制作一份。
6.關于中文
(1)使用%q打印傍衡,使用utf8包
for i, r := range "Hello, 世界" {
fmt.Printf("%d\t%q\t%d\n", i, r, r)
}
ss := "Hello, 世界"
fmt.Println(len(ss)) // "13"
fmt.Println(utf8.RuneCountInString(ss)) // "9"
(2)golang截取中文字符串
在golang中可以通過切片截取一個數(shù)組或字符串深员,但是當截取的字符串是中文時,可能會出現(xiàn)的問題是:由于中文一個字不只是由一個字節(jié)組成蛙埂,所以直接通過切片可能會把一個中文字的編碼截成兩半倦畅,結果導致最后一個字符是亂碼。
例如: 想要截取前四個字
name := "我是胡八一"
fmt.Println("name[:4] = ",name[:4])
執(zhí)行后得到的結果會是這樣的:
name[:4] = 我?
解決方法: 先將其轉為[]rune箱残,再截取后滔迈,轉會string
nameRune := []rune(name)
fmt.Println("string(nameRune[:4]) = ",string(nameRune[:4]))
運行結果:
string(nameRune[:4]) = 我是胡八
7.string與int互轉
import "strconv" //先導入strconv包
// string到int
int, err := strconv.Atoi(string)
// string到int64
int64, err := strconv.ParseInt(string, 10, 64)
// int到string
string := strconv.Itoa(int)
// int64到string
string := strconv.FormatInt(int64,10)
Go語言字符串高效拼接(一)
Go語言字符串高效拼接(二)
Go語言字符串高效拼接(三)
二止吁、字典
《快學 Go 語言》第 6 課 —— 字典
1.make
func main() {
var m map[int]string = make(map[int]string)
fmt.Println(m, len(m))
}
----------
map[] 0
如果你可以預知字典內部鍵值對的數(shù)量被辑,那么還可以給 make 函數(shù)傳遞一個整數(shù)值,通知運行時提前分配好相應的內存敬惦。這樣可以避免字典在長大的過程中要經(jīng)歷的多次擴容操作盼理。
var m = make(map[int]string, 16)
2.初始化
func main() {
var m map[int]string = map[int]string{
90: "優(yōu)秀",
80: "良好",
60: "及格", // 注意這里逗號不可缺少,否則會報語法錯誤
}
fmt.Println(m, len(m))
}
---------------
map[90:優(yōu)秀 80:良好 60:及格] 3
3.讀寫
func main() {
var fruits = map[string]int {
"apple": 2,
"banana": 5,
"orange": 8,
}
// 讀取元素
var score = fruits["banana"]
fmt.Println(score)
// 增加或修改元素
fruits["pear"] = 3
fmt.Println(fruits)
// 刪除元素
delete(fruits, "pear")
fmt.Println(fruits)
}
-----------------------
5
map[apple:2 banana:5 orange:8 pear:3]
map[orange:8 apple:2 banana:5]
刪除操作時俄删,如果對應的 key 不存在宏怔,delete 函數(shù)會靜默處理。遺憾的是 delete 函數(shù)沒有返回值畴椰,你無法直接得到 delete 操作是否真的刪除了某個元素臊诊。這時候必須使用字典的特殊語法,如下
func main() {
var fruits = map[string]int {
"apple": 2,
"banana": 5,
"orange": 8,
}
var score, ok = fruits["durin"]
if ok {
fmt.Println(score)
} else {
fmt.Println("durin not exists")
}
fruits["durin"] = 0
score, ok = fruits["durin"]
if ok {
fmt.Println(score)
} else {
fmt.Println("durin still not exists")
}
}
-------------
durin not exists
0
4.遍歷
這個和數(shù)組一樣的
func main() {
var fruits = map[string]int {
"apple": 2,
"banana": 5,
"orange": 8,
}
for name, score := range fruits {
fmt.Println(name, score)
}
for name := range fruits {
fmt.Println(name)
}
}
------------
orange 8
apple 2
banana 5
apple
banana
orange
奇怪的是斜脂,Go 語言的字典沒有提供諸于 keys() 和 values() 這樣的方法抓艳,意味著如果你要獲取 key 列表,就得自己循環(huán)一下帚戳,如下
func main() {
var fruits = map[string]int {
"apple": 2,
"banana": 5,
"orange": 8,
}
var names = make([]string, 0, len(fruits))
var scores = make([]int, 0, len(fruits))
for name, score := range fruits {
names = append(names, name)
scores = append(scores, score)
}
fmt.Println(names, scores)
}
----------
[apple banana orange] [2 5 8]
讀完上面的內容玷或,我的理解就是:如果沒有設置這個隨機數(shù),那么在大多數(shù)情況下片任,golang會表現(xiàn)出map的順序是固定的情況偏友。但是golang底層并沒有保證這一點,或許(現(xiàn)在/以后)會有特殊情況出現(xiàn)順序不固定的情況对供。擔心開發(fā)者們誤解這一點位他,golang就特意去打亂了這個順序,讓開發(fā)者們知道golang底層不保證map每次遍歷都是同一個順序。
5.映射的鍵可以是任何值棱诱。這個值的類型可以是內置類型,也可以是結構類型炬灭。只要可以使用==運算符做比較重归。切片厦凤、函數(shù)以及包含切片的結構類型,由于具有引用語義椎木,不能作為映射的鍵香椎,使用這些類型會造成編譯錯誤禽篱。(Go in Action)
6.Golang map使用注意事項
map中的元素并不是一個變量躺率,而是一個值悼吱。因此,我們不能對map的元素進行取址操作笨枯。
var m = map[int]int {
0 : 0,
1: 1,
}
func main() {
fmt.Println(&m[0])
}
運行報錯:
cannot take the address of m[0]
因此猎醇,當 map 的元素為結構體類型的值硫嘶,那么無法直接修改結構體中的字段值梧税。考察如下示例:
package main
import (
"fmt"
)
type person struct {
name string
age byte
isDead bool
}
func whoIsDead(personMap map[string]person) {
for name, _ := range personMap {
if personMap[name].age < 50 {
personMap[name].isDead = true
}
}
}
func main() {
p1 := person{name: "zzy", age: 100}
p2 := person{name: "dj", age: 99}
p3 := person{name: "px", age: 20}
personMap := map[string]person{
p1.name: p1,
p2.name: p2,
p3.name: p3,
}
whoIsDead(personMap)
for _, v :=range personMap {
if v.isDead {
fmt.Printf("%s is dead\n", v.name)
}
}
}
編譯報錯:
cannot assign to struct field personMap[name].isDead in map
原因是 map 元素是無法取址的刨秆,也就說可以得到 personMap[name]衡未,但是無法對其進行修改家凯。解決辦法有二绊诲,一是 map 的 value用 strct 的指針類型掂之,二是使用臨時變量,每次取出來后再設置回去动雹。
(1)將map中的元素改為struct的指針洽胶。
package main
import (
"fmt"
)
type person struct {
name string
age byte
isDead bool
}
func whoIsDead(people map[string]*person) {
for name, _ := range people {
if people[name].age < 50 {
people[name].isDead = true
}
}
}
func main() {
p1 := &person{name: "zzy", age: 100}
p2 := &person{name: "dj", age: 99}
p3 := &person{name: "px", age: 20}
personMap := map[string]*person {
p1.name: p1,
p2.name: p2,
p3.name: p3,
}
whoIsDead(personMap)
for _, v :=range personMap {
if v.isDead {
fmt.Printf("%s is dead\n", v.name)
}
}
}
輸出結果:
px is dead
(2)使用臨時變量覆蓋原來的元素。
package main
import (
"fmt"
)
type person struct {
name string
age byte
isDead bool
}
func whoIsDead(people map[string]person) {
for name, _ := range people {
if people[name].age < 50 {
tmp := people[name]
tmp.isDead = true
people[name] = tmp
}
}
}
func main() {
p1 := person{name: "zzy", age: 100}
p2 := person{name: "dj", age: 99}
p3 := person{name: "px", age: 20}
personMap := map[string]person {
p1.name: p1,
p2.name: p2,
p3.name: p3,
}
whoIsDead(personMap)
for _, v :=range personMap {
if v.isDead {
fmt.Printf("%s is dead\n", v.name)
}
}
}
輸出結果:
px is dead
7.hash算法