https://www.jb51.net/article/126998.htm
go標准庫文檔https://studygolang.com/pkgdoc
1.
如果想要再本地直接查看go官方文檔,可以再終端中運行:
userdeMacBook-Pro:~ user$ godoc -http=:8000
然后在瀏覽器中運行http://localhost:8000就能夠查看文檔了,如下圖所示:
2.os.Args : Args保管了命令行參數,第一個是程序名
3.所有的go語言代碼都只能放置在包中,每一個go程序都必須包含一個main包以及一個main函數。main()函數作為整個程序的入口,在程序運行時最先被執行。實際上go語言中的包還可能包含init()函數,它先於main()函數被執行
4.包名和函數名之間不會發生命名沖突
5.go語言針對處理的單元是包而非文件,如果所有文件的包聲明都是一樣的,那么他們屬於同一個包
6.
1)
package main import ( "fmt" "os" "strings" "path/filepath" ) //"path/filepath"類的引用只需要其最后一部分,即filepath func main(){ who := "world!" //Args保管了命令行參數,第一個是程序名 fmt.Println(os.Args) //返回:[/var/folders/2_/g5wrlg3x75zbzyqvsd5f093r0000gn/T/go-build178975765/b001/exe/test] fmt.Println(filepath.Base(os.Args[0]))//返回傳入路徑的基礎名,即文件名test if len(os.Args) > 1 {//則運行時是帶有命令行參數的 who = strings.Join(os.Args[1:]," ")//返回一個由" "分隔符將os.Args[1:]中字符串一個個連接生成一個新字符串 } fmt.Println("Hello", who) //返回:Hello world! }
不帶命令行參數的運行返回:
userdeMBP:go-learning user$ go run test.go [/var/folders/2_/g5wrlg3x75zbzyqvsd5f093r0000gn/T/go-build644012817/b001/exe/test] test Hello world!
帶命令行參數的運行返回:
userdeMBP:go-learning user$ go run test.go i am the best [/var/folders/2_/g5wrlg3x75zbzyqvsd5f093r0000gn/T/go-build463867367/b001/exe/test i am the best] test Hello i am the best
2)import
支持兩種加載方式:
1》相對路徑
import "./model"
2》絕對路徑
import "shorturl/model"
下面還有一些特殊的導入方式:
1〉點操作
import( . "fmt" )
這個點操作的作用就是這個包導入后,當你想要調用這個包的函數的時候,你就能夠省略它的包名了,即fmt.Println()可以省略成Println()
2>別名操作
import( f "fmt" )
就是把上面的fmt包重命名成一個容易記憶的名字,調用可以變成f.Println()
3>_操作
import( _ "github.com/ziutek/mymysal/godrv" )
其作用就是引入該包,而不直接使用包里面的函數或變量,會先調用包中的init函數,這種使用方式僅讓導入的包做初始化,而不使用包中其他功能
7.在go語言中,byte類型等同於uint8類型
8.注意:在go中++和--等操作符只會用於語句而非表達式中,可能只可以做成后綴操作符而非前綴操作符。即go中不會出現f(i++)或A[i]=b[++i]這樣的表達式
9.相同結構的匿名類型等價,可以相互替換,但是不能有任何方法
任何命名的自定義類型都可以有方法,並且和這些方法一起構成該類型的接口。命名的自定義類型即時結構完全相同,也不能相互替換
接口也是一種類型,可以通過指定一組方法的方式定義。接口是抽象的,因此不可實例化。如果某個具體類型實現了某個接口所有的方法,那么這個類型就被認為實現了該接口。一個類型可以實現多個接口。
空接口(即沒有定義方法的接口),用interface{}來表示。由於空接口沒有做任何要求(因為它不需要任何方法),它可以用來表示任意值(效果相當於一個指向任意類型值的指針),無論這個值是一個內置類型的值還是一個自定義類型的值,如:
type Stack []interface{}
上面的空接口其實就相當於一個可變長數組的引用
⚠️在go語言中只講類型和值,而非類、對象或實例
10.go語言的函數和方法均可返回單一值或多個值。go語言中報告錯誤的管理是函數或方法的最后一個返回值是一個錯誤值error,如:
item,error := someFunc()
11.無限循環的使用要配合break或return來停止該無限循環
... for{ item,err := haystack.Pop() if err != nil{ break } fmt.Println(item) }
12.
func (stack *Stack) Push(x interface{}){ *stack = append(*stack,x) }
其中(stack *Stack)中的stack即調用Push方法的值,這里成為接收器(在其他語言中是使用this,self來表示的)
如果我們想要修改接收器,就必須將接收器設為一個指針,如上面的*Stack,使用指針的原因是:
- 效率高;如果我們有一個很大的值,那么傳入一個指向該值所在的內存地址的指針會比傳入該值本身更便宜
- 是一個值可以被修改;將一個變量傳入函數中其實只是傳入了他的一個副本,對該值的所有改動都不會影響其原始值(即值傳遞)。所以如果我們想要更改原始值,必須傳入指向原始值的指針
*stack表示解引用該指針變量,即引用的是該指針所指之處的實際Stack類型值13.go中的通道(channel)、映射(map)和切片(slice)等數據結構必須通過make()函數創建,make()函數返回的是該類型的一個引用14.如果棧沒有被修改,那么可以使用值傳遞,如:
func (stack Stack) Top(interface{},error){ if len(stack) == 0{ return nil,errors.New("can't Top an empty stack") } return stack[len(stack)-1],nil }
可見該例子的接收器就沒有使用指針,因為該例子沒有修改棧
15.任何屬於defer語句所對應的語句都保證會被執行,但是其后面對應的函數只會在該defer語句所在的函數返回時被調用(即其不馬上運行,而是推遲到該函數的其他所有語句都運行完后再返回來運行它),如:
func main(){
....
defer inFile.Close()
....
defer outFile.Close()
....
}
在上面的例子可以保證打開的文件可以繼續被它下面的命令使用,然后會在使用完后才關閉,即使是程序崩潰了也會關閉
16.在go語言中,panic是一個運行時錯誤(翻譯為‘異常’)。可以使用內置的panic()函數來觸發一個異常,還可以使用recover()函數來調用棧上阻止該異常的傳播。理論上,go的panic/recover功能可以用於多用途的錯誤處理機制,但是我們並不推薦這么使用。更合理的錯誤處理方式時讓函數或者方法返回一個error值。panic/recover機制的目的是處理真正的異常(即不可預料的異常),而不是常規錯誤
17.PDF中的1.7的例子很好,值得多看
18.打印到終端其實是有返回值的,一般都忽略,但是其實是應該檢查其返回的錯誤值的
count , err := fmt.Println(x) //獲取打印的字節數和相應的error值 _ , err := fmt.Println(x) //丟棄打印的字節數,返回error值 count , _ := fmt.Println(x) //獲取打印的字節數,丟棄error值 fmt.Println(x) //忽略所有返回值
19.常量賦值的特性
1)使用iota
package main import "fmt" const ( a = iota b c ) func main(){ fmt.Println(a)//0 fmt.Println(b)//1 fmt.Println(c)//2 }
使用iota,則在const的第一個常量中被設為0,接下來增量為1
2)如果b設為字符串"test",那么c的值和b一樣,但是iota還是一樣遞增
package main import "fmt" const ( a = iota b = "test" c d = iota e ) func main(){ fmt.Println(a)//0 fmt.Println(b)//test fmt.Println(c)//test fmt.Println(d)//3 fmt.Println(e)//4 }
3)如果不使用iota,則第一個常量值一定要顯示定義值,后面的值和其前一個的值相同:
package main import "fmt" const ( a =1 b = "test" c d = 5 e ) func main(){ fmt.Println(a)//1 fmt.Println(b)//test fmt.Println(c)//test fmt.Println(d)//5 fmt.Println(e)//5 }
20.邏輯操作符
1)|| 和 &&
b1 || b2 :如果b1為true,則不會檢測b2,表達式結果為true
b1 && b2 : 如果b1為false,則不會檢測b2,表達結果為false
2)< 、<=、 == 、!= 、>= 、>
比較的兩個值必須是相同類型的
如果兩個是接口,則必須實現了相同的接口類型
如果有一個值是常量,那么它的類型必須和另一個類型相兼容。這意味着一個無類型的數值常量可以跟另一個任意數值類型的值進行比較
不同類型且非常量的數值不能直接比較,除非其中一個被顯示地轉換成與另一個相同類型的值,類型轉換即type(value)
21.整數
go語言允許使用byte來作為無符號uint8類型的同義詞
並且使用單個字符(即Unicode碼點)的時候提倡使用rune來替代uint32
大多數情況下,我們只需要一種整形,即int
從外部程序(如從文件或者網絡連接讀寫整數時,可能需要別的整數類型)。這種情況下需要確切地知道需要讀寫多少位,以便處理該整數時不會發生錯亂
常用做法是將一個整數以int類型存儲在內存中,然后在讀寫該整數的時候將該值顯示地轉換為有符號的固定尺寸的整數類型。byte(uint8)類型用於讀或寫原始的字節。例如,用於處理UTF-8編碼的文本
大整數:
有時我們需要使用甚至超過int64位和uint64位的數字進行完美的計算。這種情況下就不能夠使用浮點數,因為他們表示的是近似值。
因此Go的標准庫種提供了無限精度的整數類型:用於整數的big.Int型以及用於有理數的big.Rat型(即包括可以表示分數的2/3和1.1496.但不包括無理數e或者Π)
但是無論什么時候,最好只使用int類型,如果int型不能滿足則使用int64型,或者如果不是特別關心近似值的可以使用float32或者float64類型。
然而如果計算需要完美的精度,並願意付出使用內存和處理器的代價,那么就使用big.Int或者big.Rat類型
22.Unicode編碼——使用四個字節為每個字符編碼
它為每種語言中的每個字符設定了統一並且唯一的二進制編碼,以滿足跨語言、跨平台進行文本轉換、處理的要求
Unicode編碼的使用意味着go語言可以包含世界上任意語言的混合,代碼頁沒有任何的混亂和限制
每一個Unicode字符都有一個唯一的叫做“碼點”的標識數字,其值從0x0到0x10FFFF,后者在go中被定義成一個常量unicode.MaxRune
23.字符串
go語言的字符串類型在本質上就與其他語言的字符串類型不同。Java的string、C++的std:string和python3的str類型都是定寬字符序列,即字符串中的每個字符的寬度是相同的。但是go的字符串是一個用UTF-8編碼的變寬字符串。
UTF-8 使用一至四個字節為每個字符編碼,其中大部分漢字采用三個字節編碼,少量不常用漢字采用四個字節編碼。因為 UTF-8 是可變長度的編碼方式,相對於 Unicode 編碼可以減少存儲占用的空間,所以被廣泛使用。
一開始可能覺得其他語言的字符串類型比go的好用,因為他們定長,所以字符串中的單個字符可以通過索引得到,而go語言只有在字符串中的字符都是7位ASCII字符(7位ASCII字符剛好能用一個UTF-8字節來表示,即都用一個單一的UTF-8字節表示)時才能索引
但在實際情況下,其並不是個問題,因為:
- 直接索引用得不多
- go支持一個字符一個字符的迭代,for index,char := range string{},每次迭代都產生一個索引位置和一個碼點
- 標准庫提供了大量的字符串搜索和操作函數
- 隨時可以將字符串轉換成一個Unicode碼點切片(類型為[]rune),這個切片是可以直接索引的
所以:
[]rune(s)
能將一個字符串s轉換成一個Unicode碼點
如果想要轉回來即:
s := string(chars)//chars的類型即[]rune或[]int32
上面兩個時間代價為o(n)
len([]rune(s))
可以得到字符串s中字符的個數,當然你可以使用更快的:
utf8.RuneCountInString()
來替代
[]byte(s)
可以無副本地將字符串s轉換成一個原始字節的切片數組,時間代價為o(1)
string(bytes)
無副本地將[]byte或[]uint8轉換成一個字符串類型,不保證轉換的字節是合法的UTF-8編碼字節,時間代價為o(1)
舉例:
package main import "fmt" func main(){ s := "hello world,i am coming" uniS := []rune(s) retS := string(uniS) bytS := []byte(s) retBytS := string(bytS) fmt.Println(s)//hello world,i am coming fmt.Println(uniS)//[104 101 108 108 111 32 119 111 114 108 100 44 105 32 97 109 32 99 111 109 105 110 103] fmt.Println(retS)//hello world,i am coming fmt.Println(len([]rune(s)))//23 fmt.Println(bytS)//[104 101 108 108 111 32 119 111 114 108 100 44 105 32 97 109 32 99 111 109 105 110 103] fmt.Println(retBytS)//hello world,i am coming }
⚠️
go中字符串是不可變的,即不能如下這樣操作:
var s string = "hello" s[0] = 'c'
如果真的想要修改,可如下:
s := "hello" c := []byte(s)//將其轉換成[]byte類型 c[0] = 'c' //然后更改 s2 := string(c) //然后再將其轉換回來即可
雖然字符串不能修改,但是它可以進行切片操作
如果想要聲明一個多行的字符,可以使用``來聲明,如:
m := `hello
world`
使用``括起來的是Raw字符串,即字符串在代碼中的形式就是打印時的形式,沒有字符轉義,換行也原樣輸出
24.字符串索引和切片
之前有說過,我們完全不需要切片一個字符串,只需要使用for...range循環將其一個字符一個字符地迭代,但是有些時候我們可能還是需要使用切片來獲得一個子字符串,這時的解決辦法是:
go中有個能夠按字符邊界進行切片得到索引位置的方法:
即strings包中的函數:
- strings.Index()
- strings.LastIndex()
舉例:
package main import( "fmt" "strings" ) func main(){ //目的:得到該行文本的第一個和最后一個字 line := "i am the best" i := strings.Index(line, " ") //獲得第一個空格的索引位置 firstWord := line[:i] //切片得到第一個字 j := strings.LastIndex(line, " ") //獲得最后一個空格的索引 lastWord := line[j+1:] //切片得到最后一個字 fmt.Println(firstWord, lastWord) //輸出:i best }
上面的例子壞處在於不適合處理任意的Unicode空白字符,如U+2028(行分隔符)或U+2029(段分隔符),這時候就使用:
- strings.IndexFunc(s string, f func(rune)bool)int :s中第一個滿足函數f的位置i(該處的utf-8碼值r滿足f(r)==true),不存在則返回-1。
- strings.LastIndexFunc():s中最后一個滿足函數f的unicode碼值的位置i,不存在則返回-1。
- utf8.DecodeRuneInString():得到輸入的string中的碼點rune和其編碼的字節數
舉例:
package main import( "fmt" "strings" "unicode" "unicode/utf8" ) func main(){ line := "ra t@RT\u2028var" i := strings.IndexFunc(line,unicode.IsSpace)//unicode.IsSpace是一個函數 firstWord := line[:i] j := strings.LastIndexFunc(line, unicode.IsSpace) r, size := utf8.DecodeRuneInString(line[j:]) lastWord := line[j+size:] fmt.Println(i,j,size)//2 7 3 fmt.Printf("%U\n",r) //U+2028 fmt.Println(firstWord, lastWord)//ra var }
func IsSpace(r rune) bool,IsSpace報告一個字符是否是空白字符
上面這個就是Unicode字符\u2028的字符、碼點和字節,所以上面運行utf8.DecodeRuneInString()得到的r為U+2028,字節size為3
上面 s[0] == 'n',s[ len(s)-1 ] == 'e',但是問題在於第三個字符,其起始索引是2,但是如果我們使用s[2]只能得到該編碼字符的第一個UTF-8字節,這並不是我們想要的
我們可以使用:
- utf8.DecodeRuneInString() : 解碼輸入的string中的第一個rune,最后返回該碼點rune和其包含的編碼的字節數
- utf8.DecodeLastRuneInString() : 解碼輸入的string中最后一個rune,最后返回該碼點rune和其包含的編碼的字節數
當然,實在想要使用索引[]的最好辦法其實是將該字符串轉換成[]rune(這樣上面的例子將創建一個包含5個碼點的rune切片,而非上面圖中的6個字節),然后就能夠使用索引調用了,代價就是該一次性轉換耗費了CPU和內存o(n)。結束后可以使用string(char)來將其轉換成字符
25.fmt
fmt包中提供的掃描函數(fmt.Scan()\fmt.Scanf()\fmt.Scanln())的作用是用於從控制台、文件以及其他字符串類型中讀取數據
26.go語言提供了兩種創建變量的語法,同時獲得它們的指針:
- 使用內置的new()函數
- 使用地址操作符&
new(Type) === &Type{} (前提是該類型是可以使用大括號{}進行初始化的類型,如結構體、數組等。否則只能使用new()),&Type{}的好處是我們可以為其制定初始值
這兩種語法都分配了一個Type類型的空值,同時返回一個指向該值的指針
先定義一個結構體composer:
type composer struct{ name string birthYear int }
然后我們可以創建composer值或只想composer值的指針,即*composer類型的變量
結構體的初始化可以使用大括號來初始化數據,舉例:
package main import "fmt" type composer struct{ name string birthYear int } func main(){ a := composer{"user1",2001} //composer類型值 b := new(composer) //指向composer的指針 b.name, b.birthYear = "user2", 2002 //通過指針賦值 c := &composer{} //指向composer的指針 c.name, c.birthYear = "user3", 2003 d := &composer{"user4", 2004} //指向composer的指針 fmt.Println(a) //{user1 2001} fmt.Println(b,c,d) //&{user2 2002} &{user3 2003} &{user4 2004} }
當go語言打印指向結構體的指針時,它會打印解引用后的結構體內容,但會將取址操作符&作為前綴來表示它是一個指針
⚠️上面之所以b指針能夠通過b.name來得到結構體中的值是因為在go語言中.(點)操作符能夠自動地將指針解引用為它指向的結構體,都不會使用(*b).name這種格式
⚠️make、new操作
make用於內建類型(map、slice和channel)的內存分配。new用於各種類型的內存分配
1)new
new(T)分配了零值填充的T類型的內存空間,並且返回其地址,即*T類型的值(指針),用於指向新分配的類型T的零值
2)make
make(T, args)只能創建map、slice和channel,並且返回的是有初始值(非零)的T類型,而不是*T指針。這是因為指向數據結構的引用在使用前必須被初始化。
比如一個slice是一個包含指向數據(內部array)的指針、長度len、容量cap的三項描述符,因此make初始化其內部的數據結構,然后填充適當的值
27.一旦我們遇到需要在一個函數或方法中返回超過四五個值的情況時,如果這些值是同一類型的話最好使用一個切片來傳遞,如果其值類型各異則最好傳遞一個指向結構體的指針。
傳遞一個切片或一個指向結構體的指針的成本都比較低(在64位的機器上一個切片占16位,一個映射占8字節),同時也允許我們修改數據
28.引用類型-映射、切片、通道、函數、方法
這樣子當引用類型作為參數傳入函數時,在函數中對該引用類型的改變都會作用於它本身,即引用傳遞
這是因為一個引用類型的變量指向的是內存中某個隱藏的值,其保存着實際的數據
package main import "fmt" func main(){ grades := []int{2,3,4,5} //整形切片 inflate(grades,3) fmt.Println(grades) //返回[6 9 12 15] } func inflate(numbers []int, factor int){ for i := range numbers{ numbers[i] *= factor } }
在上面的例子中我們沒有使用for index,item := range numbers{}是因為這樣的操作得到的是切片的副本,它的改變不會改變原始切片的值。所以這里我們使用的是for index := range語法
如果我們定義一個變量來保存一個函數,該變量得到的實際上是該函數的引用
29.數組、切片
- 數組是定長的 a := [3]int{1,2,3}/a := [...]int{1,2,3} ,[...]會自動計算數組長度,聲明用 a:=[3]int
- 切片長度不定 b := []int{1,2,3}
a,b是不相同的。數組的cap()和len()函數返回的值是相同的,它也可以進行切片,即[i:j],但是返回的是切片,而非數組
數組是值傳遞,而切片是引用傳遞
在go的標准庫中的所有公開函數中使用的都是切片而非數組,建議使用切片
當創建一個切片的時候,他會創建一個隱藏的初始化為零值的數組,然后返回一個引用該隱藏數組的切片。所以切片其實就是一個隱藏數組的引用
切片的創建語法:
- make([]Type, length, capacity) ,切片長度小於或等於容量
- make([]Type, length) ,下面三個長度等於容量
- []Type{} == make([]Type,0),空切片
- []Type{value1,...,valueN}
make()內置函數用來創建切片、映射、通道
可以使用append()內置函數來增加切片的容量
除了使用上面的方法還有一種方法:
package main import "fmt" func main(){ s := new([3]string)[:] //new()創建一個只想數組的指針,然后通過[:]得到該數組的切片 s[0], s[1], s[2] = "A", "B", "C" //賦值 fmt.Println(s) //返回[A B C] }
對切片的切片其實也是引用了同一個隱藏數組,所以如果對切片中的某個值進行了更改,那么該切片的切片中如果也包含該值,則發現也被更改了,舉例:
package main import "fmt" func main(){ s := []string{"A", "B", "C"} t := s[1:] fmt.Println(s,t) //返回:[A B C] [B C] s[1] = "d" fmt.Println(s,t) //返回: [A d C] [d C] }
因為更改切片其實都是對底層的隱藏數據進行了修改
⚠️切片和字符串不同之處在於它不支持+或+=操作符,但是對切片進行添加、插入或刪除的操作是十分簡單的
- 遍歷切片:for index,item := range amounts[:5] ,可遍歷切片的前5個元素
- 修改切片中的項:for i:= range amounts{amounts[i] *= 3},不能使用上面的方法,那樣得到的只是切片的副本,但是也有特別的情況,如下:
當切片包含的是自定義類型的元素時,如結構體
package main import "fmt" type Product struct{ name string price float64 } func main(){ products := []*Product{{"user1",11},{"user2",12},{"user3",13}} //等價於products := []*Product{&Product{"user1",11},&Product{"user2",12},&Product{"user3",13}} fmt.Println(products) //會自動調用Product的String方法,返回[user1 (11.00) user2 (12.00) user3 (13.00)] for _, product := range products{ product.price += 50 } fmt.Println(products) //返回[user1 (61.00) user2 (62.00) user3 (63.00)] } func (product Product) String() string{ return fmt.Sprintf("%s (%.2f)", product.name, product.price) }
如果沒有定義Product.String()方法,那么%v格式符(該格式符在fmt.Println()以及類似的函數中被顯示調用),輸出的就是Product的內存地址,而非其內容,如:[0xc42000a080 0xc42000a0a0 0xc42000a0c0]
我們可以看見這里使用的是for index,item ...range迭代,這里能夠成功的原因是product被賦值的是*Product的副本,這是一個指向底層數據的指針,所以成功
- 修改切片:append(array, ...args)內置函數,返回一個切片。如果原始切片中沒有足夠的容量,那么append()函數會隱式地創建一個新的切片,並將其原始切片的項復制進去,再在末尾添加上新的項,然后將新的切片返回,因此需要將append()的返回值賦值給原始切片變量
如果是想要從任意位置插入值,那么需要自己寫一個函數,如:
package main import "fmt" func main(){ s := []string{"a","b","c","d"} x := InsertStringSlice(s,[]string{"e","f"},0) y := InsertStringSlice(s,[]string{"e","f"},2) z := InsertStringSlice(s,[]string{"e","f"},len(s)) fmt.Println(x) //[e f a b c d] fmt.Println(y) //[a b e f c d] fmt.Println(z) //[a b c d e f] } func InsertStringSlice(slice, insertion []string, index int)[]string{ return append(slice[:index],append(insertion,slice[index:]...)...) }
因為append()函數接受一個切片和一個或多個值作為參數,因此需要使用...(省略號語法)來將一個切片轉換成它的多個元素值,因此上面的例子中使用了兩個省略號語法
- 排序切片:sort.Sort(d)-排序類型為sort.interface的切片d、sort.Strings(s)-按升序排序[]string類型的切片s
sort.Sort(d)能夠對任意類型進行排序,只要其類型提供sort.Interface接口中定義的方法:
type Interface interface { // Len方法返回集合中的元素個數 Len() int // Less方法報告索引i的元素是否比索引j的元素小 Less(i, j int) bool // Swap方法交換索引i和j的兩個元素 Swap(i, j int) }
比如:
package main import( "fmt" "sort" "strings" ) func main(){ files := []string{"Test","uitl","Makefile","misc"} fmt.Printf("Unsorted: %q\n",files) //Unsorted: ["Test" "uitl" "Makefile" "misc"] sort.Strings(files)//區分大小寫 fmt.Printf("underlying bytes:%q\n",files) //underlying bytes:["Makefile" "Test" "misc" "uitl"] SortFoldedStrings(files)//不區分大小寫 fmt.Printf("Case insensitive:%q\n",files) //Case insensitive:["Makefile" "misc" "Test" "uitl"] } func SortFoldedStrings(slice []string){ sort.Sort(FoldedStrings(slice)) } type FoldedStrings []string func (slice FoldedStrings) Len() int { return len(slice) } func (slice FoldedStrings) Less(i,j int) bool{ return strings.ToLower(slice[i]) < strings.ToLower(slice[j]) } func (slice FoldedStrings) Swap(i,j int){ slice[i], slice[j] = slice[j], slice[i] }
- 搜索切片:sort.Search()
30.映射
map[string]float64:鍵為string類型,值為float64類型的映射
由於[]byte是一個切片,不能作為映射的鍵,但是我們可以先將[]byte轉換成字符串,如string([]byte),然后作為映射的鍵字段,等有需要的時候再轉換回來,這種轉換並不會改變原有切片的數據
如果值的類型是接口類型,我們就可以創建一個滿足這個接口定義的值作為映射的值,甚至我們可以創建一個值為空接口(interface{})的映射,這意味着任意類型的值都可以作為這個映射的值
不過當我們需要訪問這個值時,需要使用類型開關和類型斷言獲得這個接口類型的實際類型,或者也可以通過類型檢驗來獲得變量的實際類型
創建方式:
- make(map[keyType]ValueType,initialCapacity),隨着加入的項越多,映射會自動擴容
- make(map[keyType]ValueType)
- map[keyType]ValueType{}
- map[keyType]ValueType{key1 : value1,...,keyN : valueN}
第二和第三種的結果是相同的
對於比較大的映射,最好是為其指定恰當的容量來提高性能
映射可以使用索引操作符[],不同在於其里面的值不用是int型,應該是映射的keyType值
映射的鍵也可以是一個指針,比如:
package main import "fmt" func main(){ triangle := make(map[*Point]string,3) triangle[&Point{89, 47, 27}] = "a" //使用&獲得Point的指針 triangle[&Point{86, 65, 86}] = "b" triangle[&Point{7, 44, 45}] = "c" fmt.Println(triangle) //map[(89,47,27):a (86,65,86):b (7,44,45):c] } type Point struct{ x,y,z int} func (point Point) String() string{ return fmt.Sprintf("(%d,%d,%d)",point.x,point.y,point.z) }
如果想要按照某種方式來遍歷,如按鍵序,則可以:
package main import( "fmt" "sort" ) func main(){ populationForCity := map[string]int{"beijing":12330000,"shanghai":11002002,"guangzhou":14559400} cities := make([]string,0,len(populationForCity)) for city := range populationForCity{ cities = append(cities,city) } sort.Strings(cities) for _,city := range cities{ fmt.Printf("%-10s %8d\n", city, populationForCity[city]) } }
該方法就是創建一個足夠大的切片去保存映射里的所有鍵,然后對切片排序,遍歷切片得到鍵,再從映射里得到這個鍵的值,然后就可以實現鍵順序輸出了
另一種方法就是:使用一個有序的數據結構,這個之后講
上面是按鍵排序,按值排序當然也是可以的——映射反轉
前提:映射的值都是唯一的,如:
package main import( "fmt" ) func main(){ populationForCity := map[string]int{"beijing":12330000,"shanghai":11002002,"guangzhou":14559400} cityForPopulation := make(map[int]string,len(populationForCity)) //與上面的populationForCity鍵值類型相反 for city, population := range populationForCity{ cityForPopulation[population] = city } fmt.Println(cityForPopulation) //map[14559400:guangzhou 12330000:beijing 11002002:shanghai] }
即創建一個與populationForCity鍵值類型相反的cityForPopulation,然后遍歷populationForCity,並將得到的鍵值反轉,然后插到cityForPopulation中即可
返回:
userdeMBP:go-learning user$ go run test.go beijing 12330000 guangzhou 14559400 shanghai 11002002
31.++(遞增)\--(遞減)操作符
這兩個操作符都是后置操作符,並且沒有返回值。因此該操作符不能用於表達式和語意不明的上下文。所以i = i++這樣的代碼是錯誤的
32.類型斷言
一個interface{}的值可以用於表示任意Go類型的值。
我們可以使用類型開關、類型斷言或者Go語言的reflect包的類型檢查將一個interface{}類型的值轉換成實際數據的值
在處理從外部源接收到的數據、想創建一個通用函數及在進行面向對象編程時,我們會需要使用interface{}類型(或自定義接口類型),因為你不知道接收的是什么類型的數據
為了訪問底層值,我們需要進行類型斷言來確定得到的數據的類型,類型斷言的格式為:
resultOfType, boolean := expression.(Type) //安全斷言類型 resultOfType := expression.(Type) //非安全斷言類型,如果斷言失敗,則調用內置函數panic()
resultOfType返回的是expression的該Type類型的值,如下面的例子,i.(int)返回的j的值為99
舉例說明:
package main import "fmt" func main(){ var i interface{} = 99 var s interface{} = []string{"left","right"} j := i.(int) //j是int類型的數據,或者失敗發生了一個panic() // b := i.(bool) //b是bool類型的數據,或者失敗發生了一個panic() fmt.Printf("%T -> %d\n", j, j) // fmt.Println(b) if i, ok := i.(int); ok { fmt.Printf("%T -> %d\n", i, i) } if s, ok := s.([]string); ok { fmt.Printf("%T -> %q\n", s, s) } }
返回:
userdeMBP:go-learning user$ go run test.go int -> 99 int -> 99 []string -> ["left" "right"]
如果刪除上面的注釋:
// b := i.(bool) //b是bool類型的數據,或者失敗發生了一個panic() // fmt.Println(b)
將會調用panic()返回:
userdeMBP:go-learning user$ go run test.go panic: interface conversion: interface {} is int, not bool goroutine 1 [running]: main.main() /Users/user/go-learning/test.go:8 +0xe0 exit status 2
在例子中有i, ok := i.(int),在做類型斷言的時候將結果賦值給與原始變量同名的變量是很常見的事,這叫做使用影子變量
⚠️只有在我們希望表達式是某種特定類型的值時才使用類型斷言(如果目標類型可能是很多類型之一,我們可以使用類型開關)
如果我們輸出上面例子的i和s變量,它們可以以int和[]string類型的形式輸出。這是因為當fmt包打印函數遇到interface{}類型時,會足夠智能地打印實際類型的值
33.類型開關
舉例:
1)
package main import "fmt" func main() { classifier(5, -17.9, "ZIP", nil, true, complex(1,1)) } func classifier(items ...interface{}){//...聲明這是一個可變參數 for i, x := range items{ switch x.(type){//type為關鍵字而非一個實際類型,用於表示任意類型 case bool: fmt.Printf("param #%d is a bool\n", i) case float64: fmt.Printf("param #%d is a float64\n", i) case int, int8, int16, int32, int64: fmt.Printf("param #%d is a int\n", i) case uint, uint8, uint16, uint32, uint64: fmt.Printf("param #%d is a unsigned int\n", i) case nil: fmt.Printf("param #%d is a nil\n", i) case string: fmt.Printf("param #%d is a string\n", i) default : fmt.Printf("param #%d's type is unknow\n", i) } } }
返回:
userdeMBP:go-learning user$ go run test.go param #0 is a int param #1 is a float64 param #2 is a string param #3 is a nil param #4 is a bool param #5's type is unknow
2)處理外部數據
想要解析JSON格式的數據,將數據轉換成相對應的Go語言數據類型
使用go語言的json.Unmarshal()函數:
func Unmarshal(data []byte, v interface{}) error
解析json編碼的數據並將結果存入v指向的值,即將JSON格式的字符串轉成JSON
要將json數據解碼寫入一個接口類型值,函數會將數據解碼為如下類型寫入接口:
Bool 對應JSON布爾類型 float64 對應JSON數字類型 string 對應JSON字符串類型 []interface{} 對應JSON數組 map[string]interface{} 對應JSON對象 nil 對應JSON的null
如果我們向該函數傳入一個指向結構體的指針,該結構體又與該JSON數據相匹配(前提是你事先知道JSON的數據結構),那么該函數就會將JSON數據中對應的數據項填充到結構體的每一個字段,看下面的2.例子
但是如果我們事先不知道JSON數據的結構,那么就不能傳入一個結構體,而是應該傳入一個指向interface{}的指針,這樣json.Unmarshal()函數就會將其設置為引用一個map[string]interface{}類型值,其鍵為JSON字段的名字,而值則對應的保存為interface{}的值,下面舉例說明:
1.
package main import( "fmt" "encoding/json" "bytes" ) func main(){ MA := []byte("{\"name\":\"beijing\", \"area\":27000, \"water\":25.7, \"senators\":[\"John\", \"Scott\"]}") var object interface{} if err :=json.Unmarshal(MA, &object); err != nil{ fmt.Println(err) }else{ jsonObject := object.(map[string]interface{})
fmt.Println(jsonObject)//map[name:beijing area:27000 water:25.7 senators:[John Scott]] fmt.Println(jsonObjectAsString(jsonObject)) } } func jsonObjectAsString(jsonObject map[string]interface{})string{ var buffer bytes.Buffer buffer.WriteString("{") comma := "" for key, value := range jsonObject{ buffer.WriteString(comma) switch value := value.(type){ //影子變量 case nil: fmt.Fprintf(&buffer, "%q: null", key) case bool: fmt.Fprintf(&buffer, "%q: %t", key, value) case float64: fmt.Fprintf(&buffer, "%q: %f", key, value) case string: fmt.Fprintf(&buffer, "%q: %q", key, value) case []interface{}: fmt.Fprintf(&buffer, "%q: [", key) innerComma := "" for _, s := range value{ if s, ok := s.(string); ok{ //因為本例子中該類型的值為string類型 fmt.Fprintf(&buffer, "%s%q", innerComma, s) innerComma = ", " } } buffer.WriteString("]") } comma = ", " } buffer.WriteString("}") return buffer.String() }
如果反序列化失敗,即json.Unmarshal返回的err != nil,那么interface{}類型的object變量就會指向一個map[string]interface{}類型的變量,其鍵為JSON對象中字段的名字
jsonObjectAsString()函數接收一個該類型的映射,同時返回一個對應的JSON字符串
fmt.Fprintf()函數將數據寫入到其第一個io.Writer類型的參數。雖然bytes.Buffer不是io.Writer,但*bytes.Buffer卻是一個io.Writer,因此在上面的例子中傳入了buffer的地址,及&buffer
返回:
userdeMBP:go-learning user$ go run test.go {"name": "beijing", "area": 27000.000000, "water": 25.700000, "senators": ["John", "Scott"]}
中間遇見的問題:
userdeMBP:go-learning user$ go run test.go # command-line-arguments ./test.go:8:14: cannot convert '\u0000' (type rune) to type []byte ./test.go:8:15: invalid character literal (more than one character)
這是因為之前MA是這么寫的:
MA := []byte('{"name":"beijing", "area":27000, "water":25.7, "senators":["John", "Scott"]}')
但是單引用會被當作rune類型,所以要使用雙引號,而且也不能把里面的鍵值對的雙引號該成單引號,否則會報下面的錯誤:
invalid character '\'' looking for beginning of object key string
或者也可以寫成:
MA := []byte(`{"name":"beijing", "area":27000, "water":25.7, "senators":["John", "Scott"]}`)
如果輸入的字符串中只是一個float64的數字,效果如下:
package main import( "fmt" "encoding/json" "bytes" ) func main(){ MA := []byte("62") var object interface{} if err :=json.Unmarshal(MA, &object); err != nil{ fmt.Println(err) }else{ object := object.(float64) fmt.Println(object) //返回62 }
2.如果事先知道原始JSON對象的結構,那么我們就能夠很大程度地簡化代碼
可以使用一個結構體來保存數據,然后使用一個方法以字符串的形式將其輸出,舉例說明:
package main import( "fmt" "encoding/json" // "strings" ) func main(){ MA := []byte(`{"name":"beijing", "area":27000, "water":25.7, "senators":["John", "Scott"]}`) var state State if err := json.Unmarshal(MA, &state); err != nil{ fmt.Println(err) } fmt.Println(state) //返回{"name": "beijing", "area": 27000, "water": 25.700000,"senators": ["John", "Scott"]} } type State struct{ Name string Senators []string Water float64 Area int } func (state State) String() string { var senators []string for _, senator := range state.Senators{ senators = append(senators, fmt.Sprintf("%q", senator)) } return fmt.Sprintf( `{"name": %q, "area": %d, "water": %f,"senators": [%s]}`, state.Name, state.Area, state.Water, strings.Join(senators,", ")) }
如果沒有自定義String()的話,返回的結果為:
{beijing [John Scott] 25.7 27000}
34.for循環
使用break跳出雙層循環的兩種方法:
假設有一個二維切片(類型為[][]int),想從中搜索看看是否包含某個特定的值
1)
package main import( "fmt" ) func main() { table := [][]int{{1,2}, {3,4}, {5}} x := 4 found := false for row := range table { for column := range table[row] { if table[row][column] == x{ fmt.Println(row, column)//1 1 found = true break } } if found{ break } } }
2)帶標簽的中斷語句,嵌套越深,其效果越好
package main import( "fmt" ) func main() { table := [][]int{{1,2}, {3,4}, {5}} x := 4 FOUND: for row := range table { for column := range table[row] { if table[row][column] == x{ fmt.Println(row, column)//1 1 break FOUND } } } }
break、continue語句后都可以接標簽
除此之外還可以使用goto label語法,但是並不推薦使用goto語法,可能造成程序崩潰
35.通信和並發
並發goroutine的go語句創建:
go function(arguments) //之前已有函數 go func(parameters){ block }(arguments) //臨時創建的匿名函數
臨時調用函數中(parameters)是聲明該函數的參數類型,(arguments)是調用該匿名函數傳入的參數,即匿名函數聲明完后立即調用
被調用函數的執行會立即進行,但是是在另一個goroutine上實行,當前goroutine的執行會從下一條語句開始
大多數情況下,goroutine之間需要相互協作,這通過通信來完成
非阻塞的發送可以使用select語句,或使用帶緩沖的通道
通道可以使用內置的make()函數創建:
make(chan Type)
make(chan Type, capacity)
如果沒有聲明capacity,該通道就是同步的,因此會阻塞直到發送者准備好發送和接受者准備好接受
如果聲明了capacity,通道就是異步的.只要緩沖區有未使用空間用於發送數據,或還包含可以接收的數據,那么通信就會無阻塞地進行
通道默認是雙向的,如果有需要可以將其設置為單向的
舉例:
package main import( "fmt" ) func main() { counterA := createCounter(2) counterB := createCounter(102) //這兩個函數中的goroutine會阻塞並得到返回的next通道,等待接收通道中數據的請求 for i := 0; i<5 ; i++{ a := <-counterA b := <-counterB //這兩句就是對通道數據的請求 fmt.Printf("(A->%d, B->%d)", a, b) } fmt.Println() } func createCounter(start int) chan int{ next :=make(chan int) go func(i int){ //無限循環 for{ next <- i i++ } }(start) //會生成另一個goroutine,因為next沒有聲明capacity,所以此時會阻塞,直到收到接受請求 return next //上面的goroutine並發時,createCounter()函數會繼續往下執行,返回next }
返回:
userdeMBP:go-learning user$ go run test.go (A->2, B->102)(A->3, B->103)(A->4, B->104)(A->5, B->105)(A->6, B->106)
上面生成的兩個counterA、counterB通道是無限的,即它們可以無限地發送數據。當然,如果我們達到了int型數據的極限,下一個值就會從頭開始
一旦想要接收的五個值都從通道中接收完成,通道將繼續阻塞以備后續使用
如果有多個goroutine並發執行,每一個goroutine都有其自身通道。可以使用select語句來監控它們的通信
在一個select語句中,Go語言會按順序從頭到尾評估每一個發送和接收語句。如果其中任意一語句可以繼續執行(即沒有阻塞),那么就從那些可以執行的語句中任意選擇一條來使用。
如果沒有一條語句可以執行,那么就會執行default語句,同時程序的執行會從select語句后的語句中恢復
一個包含default語句的select語句是非阻塞的,並且會立即執行。但是如果沒有default語句,那么select語句將被阻塞,直到至少有一個通信可以繼續進行下去
舉例說明:
package main import( "fmt" "math/rand" ) func main() { channels := make([]chan bool, 6) //創建了6個用於發送和接收布爾數據的通道 for i := range channels { channels[i] = make(chan bool) } go func(){ for{ //無限循環,每次迭代都選擇從隨機生成的[0-6)的值中獲得的相應的通道,然后會阻塞,直至接收到數據 channels[rand.Intn(6)] <- true } }() for i:=0; i<36; i++{ var x int select { case <-channels[0]: x = 1 case <-channels[1]: x = 2 case <-channels[2]: x = 3 case <-channels[3]: x = 4 case <-channels[4]: x = 5 case <-channels[5]: x = 6 } fmt.Printf("%d", x) } fmt.Println() }
因為我們沒有提供default語句,所以該select語句會阻塞。一旦有一個或者更多個通道准備好了發送數據,那么程序會以偽隨機的形式選擇一個case語句來執行
由於該select語句在一個普通的for循環內部,它會執行固定數量的次數
返回:
userdeMBP:go-learning user$ go run test.go 646621235132165346636135422514216643
36.defer、panic、recover
1)defer
defer語句用於延遲一個函數或者方法(或者當前所創建的匿名函數)的執行,他會在外圍函數或方法返回之前,但是在該返回值(如果有的話)已經計算出來之后執行,這樣就有可能在延遲執行的函數或方法中改變返回值
如果一個函數或方法中有多個defer語句,它們會以LIFO(后進先出)的順序執行
defer最常用的用法是:保證使用完一個文件后將其成功關閉,或者將一個不再使用的通道關閉,或者捕獲異常
2)panic()\recover()
這是go提供的一套異常處理機制
go中將錯誤和異常區別對待:
- 錯誤是可能出錯的東西,例如文件不能打開
- 異常是指“不可能發生”的事情(如一個應該永遠為true的條件在實際環境下是false的)
錯誤的慣用方法是將錯誤以函數或者方法最后一個返回值的形式將其返回,並在調用它的地方檢查返回的錯誤值
result, err := function() if err != nil { ... }
對於異常則可以調用內置的panic()函數,該函數可以傳入任意想要傳入的值(例如:一個字符串用於解釋為什么那些不變的東西被破壞了)。當內置的panic()函數被調用時,其外圍的函數或方法的執行就會立即終止。
然后該外圍函數或方法的defer的函數或方法就會被調用。然后就會層層向上遞歸,就像該外圍函數的外圍函數也調用了panic()函數一樣,直至到達main()函數,不再有其他可以返回的外圍函數,然后此時程序就會終止,並將包含傳入原始panic()函數中的值的調用棧信息輸入到os.Stderr中
如果在上面的過程中的某個defer的函數和方法中包含一個對內置的recover()函數,該異常的展開就會終止。這種情況下,我們就能以任何我們想要的方式響應該異常。
解決方法是完成必要的清理工作,然后手動調用panic()函數來讓該異常繼續傳播
通用的解決辦法是創建一個error值,並將其設置成包含recover()調用函數的返回值(或返回值之一)這樣就可以將一個異常(即一個panic())轉換成錯誤(即一個error)
絕大多數情況下,go語言標准庫使用 error值而非異常。對於我們自己定義的包,最好別使用panic()。或者是如果要使用panic(),也要避免異常離開這個自定義包邊界(意思就是不要更上面說的一樣層層向上遞歸),可以通過使用recover()來捕捉異常並返回一個相應的錯誤值,就像標准庫中所做的那樣
對於那些只需要通過執行程序(比如你傳入的是一個非法的正則表達式)就能夠捕捉到的問題,我們應該使用panic(),因為一運行就崩潰的程序我們是不會部署的。要⚠️,只有在程序運行時一定會被調用到的函數中才這樣做,比如main包中的main()函數、init()函數等。
對於可能運行也可能不運行的函數或方法,如果調用了panic()函數或者調用了會發生異常的函數或方法,應該使用recover()以保證異常轉換成錯誤
一般recover()函數應該在盡可能接近於相應panic()的地方被調用,並在設置其外圍函數的error返回值之前盡可能合理地將程序恢復到健康狀態
對於main包的main()函數,我們可以放入一個可以捕獲一切異常的recover()函數,用於記錄任何捕獲的異常。但不幸的是,延遲執行的recover()函數被調用后程序會終止
⚠️recover僅在defer函數中有效,如果將其放在正常的執行過程中,調用recover()會返回nil,並沒有其他任何效果。如果當前的goroutine陷入Panic,調用recover可以捕獲Panic的輸入值,並恢復正常的執行
舉例:
1>演示如何將異常轉成錯誤
1)
package main import( "fmt" "math" ) func main() { var t int64 = 2147483648 fmt.Println(math.MinInt32) //-2147483648 fmt.Println(math.MaxInt32) //2147483647 result := CovertInt64ToInt(t) fmt.Println(result) } func CovertInt64ToInt(x int64) int { if math.MinInt32 <= x && x <= math.MaxInt32{ return int(x) } panic(fmt.Sprintf("%d is out of the int32 range", x)) }
返回:
bogon:~ user$ go run testGo.go -2147483648 2147483647 panic: 2147483648 is out of the int32 range goroutine 1 [running]: main.CovertInt64ToInt(0x80000000, 0x1) /Users/user/testGo.go:17 +0xf3 main.main() /Users/user/testGo.go:10 +0xaf exit status 2
為什么這樣的函數優先使用panic(),因為我們希望一旦有錯就強制崩潰,以便盡早弄清楚程序錯誤
上面顯示的就是沒有使用recover時,就會報錯,並且層層向上遞推指明錯誤所在
下面就是顯示當使用了recover后
2)
一種情況是:
有一個函數調用了一個或多個其他函數,一旦出錯我們希望盡快返回到原始調用函數,因此我們讓被調用的函數碰到問題時拋出異常,並在調用處使用recover()捕獲該異常(無論該異常來自哪里)
一般我們希望包報告錯誤而非拋出異常,因此常用的做法是在一個包內部使用panic(),同時使用recover()來保證產生的異常不會泄露出去,而只是報告錯誤
另一種情況是:
將類似panic("unreachable")這樣的調用放在一個我們從邏輯上判斷不可能到達的地方(例如函數的末尾,但是該函數總是會在到達末尾前通過return語句返回),或者在一個前置或者后置條件被破壞時才調用panic()函數。
這樣可以保證如果我們破壞了函數的邏輯,立馬就能夠知道
3)如果上面的情況都不滿足,那么在問題發生時應該避免崩潰,而只是返回一個非空的error值
因此本例子希望如果轉換成功就返回一個int型和一個nil;如果失敗則返回一個int型和一個非空的錯誤值
package main import( "fmt" "math" ) func main() { var t int64 = 2147483648 fmt.Println(math.MinInt32) //-2147483648 fmt.Println(math.MaxInt32) //2147483647 result, err := IntFromInt64(t) fmt.Println(result) //0 fmt.Println(err) //2147483648 is out of the int32 range } func IntFromInt64(x int64) (i int, err error){ defer func(){//終止panic,使得函數能夠繼續執行 if e := recover();e != nil{//獲取panic中輸入的值e,覆蓋err的nil值 err = fmt.Errorf("%v", e) } }() i = CovertInt64ToInt(x) //如果拋出異常,會停止函數接下去執行,然后計算得到return的返回值后,直接調用上面的defer函數,得到error,覆蓋之前計算的return的nil值,然后才運行return return i, nil //i為初始值0 } func CovertInt64ToInt(x int64) int { if math.MinInt32 <= x && x <= math.MaxInt32{ return int(x) } panic(fmt.Sprintf("%d is out of the int32 range", x)) }
返回:
userdeMBP:go-learning user$ go run test.go -2147483648 2147483647 0 2147483648 is out of the int32 range
2>展示如何讓程序變得健壯
在運行網站時我們希望即使出現異常服務器也能繼續運行,同時將任何異常都以日志的形式記錄下來,以便我們進行跟蹤並在有時間的時間將其修復
代碼之后回公司在弄188頁
必須保證每一個頁面相應函數都有一個調用recover()的匿名函數,以免異常傳播到main()函數,導致服務器的終止。
當然,對於一個含有大量頁面處理函數的網站,添加一個延遲執行的函數來捕獲和記錄異常回產生大量重復的代碼,並容易遺漏。因此我們應該將每個頁面處理函數都需要的代碼包裝成一個函數來解決這個問題
37.可變參數
其實就是在參數的類型前面寫上省略號...
38.自定義函數
如果函數有返回值,則函數必須至少有一個return語句或者最后執行panic()調用
如果返回值是命名的,則return語句可以向沒有命名的返回值一樣的寫法,或者是寫一個空的return語句,但是一般不建議寫空的return,這種寫法太拙劣
如果函數是以拋出異常結束的,go編譯器會認為這個函數不需要正常返回,所以不需要return
如果是以if或switch語句結束,且if語句的else分支以return語句結尾或switch的default以return語句結尾,那么無法確定后面需不需要return語句。解決方法是要么不給其添加else語句和default分支,要么將return語句放到if或者switch后面,或者在最后簡單地加上一句panic("unreachable")語句
39.函數參數
1)
如果想要傳遞任何類型的數據,可以將參數的類型定義為interface{}
2)將函數調用作為函數的參數
其實就是將這個調用的函數的返回值作為這個函數的參數,只要兩者匹配即可
3)可變參數
其實就是函數的最后一個參數的類型前面添加...
4)可選參數——即增加一個額外結構體
比如說我們有一個函數用來處理一些自定義的數據,默認就是簡單地處理所有的數據,但有些時候我們希望可以指定處理第一個first或者最后一個項last,還有是否記錄函數的行為,或者對於非法的項做錯誤處理等
1>一個辦法是創建一個簽名如下的函數:
ProcessItems(items Items, first, last int, audit bool, errorHandler func(item Item))
如果last的值為0意味着需要取到最后一個item
errorHandler函數只有在不為nil的時候才會被調用
所以說,如果調用該函數時希望是默認行為,只需要寫ProcessItems(items, 0, 0, false, nil)即可
2>更好的辦法是定義函數為:
ProcessItems(items Items, options Options)
其中Options結構體保存了所有其他參數的值,初始化均為零值,即默認行為
Options結構體的定義:
type Options struct{ First int //要處理的第一項 Last int //要處理的最后一項,0則默認處理從指定的第一項到最后一項 Audit bool //true則左右動作都要被記錄 ErrorHandler func(item Item) //如果不是nil,對每一個壞項都用一次 }
使用:
//默認行為,處理所有項,不記錄任何動作,對於非法的記錄也不調用錯誤處理函數來處理 processItems(items, Options{}) errorHandler := func(item Item){ log.Println("Invalid: ", item)} //要記錄行為,並且在發現非法的項時要做一些相應的處理 processItems(items, Options{Audit: true, ErrorHandler: errorHandler})
40.init()函數、 main()函數
init()函數可以出現在任何包中,一個包中可以有多個init,但是推薦只使用一個,可選
main()函數只能出現在main包中,必有
這兩個函數即不可接收任何參數,可不返回任何結果,會自動執行,不需要顯示調用
go程序的初始化和執行都從main包開始,init()函數就在main()函數前執行,所以init()中不應該依賴main()中創建的東西
41.運行時動態選擇函數
1)使用映射和函數引用來制造分支
比如如果我們想要根據不同的輸入去實現不同的操作,以前的方法是使用if或switch,但是這種方法十分死板,如果情況比較復雜是代碼會過長
這個時候的一種解決辦法是使用映射,比如想要根據輸入文件的后綴名來決定調用的函數:
var FunctionForSuffix = map[string]func(string) ([]string, error){ ".gz": GzipFileList, ".tar": TarFileList, ".tar.gz": TarFileList, ".tgz": TarFileList, ".zip": ZipFileList} func ArchiveFileListMap(file string) ([]string, error){ if function, ok := FunctionForSuffix[Suffix(file)]; ok { return function(file) } return nil, errors.New("Unrecognized archive") }
2)動態函數的創建
當我們有兩個或者更多的函數實現了相同功能時,比如使用了不同的算法,我們不希望在程序編譯時靜態綁定到其中任一個函數(例如允許我們動態地選擇他們來做性能測試或回歸測試)
舉個例子,如果我們使用一個7位的ASCII字符,我們可以寫一個更加簡單的IsPalicdrome()函數,而在運行時動態地創建一個我們所需要的版本
一種做法就是聲明一個和這個函數簽名相同的包級別的變量IsPalindrome,然后創建一個appropriate()函數和一個init()函數:
var IsPalindrome func(string) bool //是否是回文字符串的聲明 func init() { if len(os.Args) > 1 && (os.Args[1] == "-a" || os.Args[1] == "--ascii"){ os.Args = append(os.Args[:1], os.Args[2:]...) //去掉參數os.Args[1] IsPalindrome = func(s string) bool { //簡單的ASCII版本 if len(s) <= 1 { return true } if s[0] != s[len(s) - 1] { return false } return IsPalindrome(s[1 : len(s) - 1]) }else { IsPalindrome = func(s string) bool {//UTF-8版本 //... } }
什么代碼會被執行完全取決於我們創建的事哪個版本的函數
42.泛型函數
根據傳入的參數來確定參數的類型,而不是一開始就指定參數類型,這樣一個函數就可以支持所有類型
方法其實就是將參數聲明為interface{}類型,比如:
func Minimum(first interface{}, rest ...interface{}) interface{} { //... }
但有一個問題:上面的泛型函數處理不了實際類型為切片的interface{}參數
舉例說明:
1)
比如,傳入一個切片和與切片的項類型相同的值,返回這個值在切片里第一次出現的索引,如果不存在就返回-1:
package main import( "fmt" ) func main() { xs := []int{2, 4, 6, 8} fmt.Println(" 5 @", Index(xs, 5), " 6 @", Index(xs, 6)) ys := []string{"C", "B", "K", "A"} fmt.Println(" Z @", Index(ys, "z"), " A @", Index(ys, "A")) } func Index(xs interface{}, x interface{}) int { switch slice := xs.(type) { case []int: for i,y := range slice{ if y == x.(int){ return i } } case []string: for i, y := range slice{ if y == x.(string) { return i } } } return -1 }
返回:
users-MacBook-Pro:~ user$ go run testGo.go 5 @ -1 6 @ 2 Z @ -1 A @ 3
我們真正想做的是希望能夠有通用的方式對待切片,可以僅用一個循環,然后在里面用特定類型測試:
package main import( "fmt" "reflect" ) func main() { xs := []int{2, 4, 6, 8} fmt.Println(" 5 @", IndexReflectX(xs, 5), " 6 @", IndexReflectX(xs, 6)) ys := []string{"C", "B", "K", "A"} fmt.Println(" Z @", IndexReflectX(ys, "z"), " A @", IndexReflectX(ys, "A")) } func IndexReflectX(xs interface{}, x interface{}) int { if slice := reflect.ValueOf(xs); slice.Kind() == reflect.Slice { for i := 0; i < slice.Len(); i++ { switch y := slice.Index(i).Interface().(type){ case int: if y == x.(int){ return i } case string: if y == x.(string){ return i } } } } return -1 }
跟上面的Index()的返回時一樣的
ValueOf()返回一個初始化為xs接口保管的具體值的Value,ValueOf(nil)返回Value零值
reflect.ValueOf(xs)的作用是將傳入的任意類型的參數xs轉換成一個切片類型reflect.Value
type Value struct { // 內含隱藏或非導出字段 }
然后使用reflect.Value.Interface()函數將其值以interface{}類型提取出來,然后賦值給y,用以保證y和切片里的項有着相同的類型
Kind()返回reflect.Value持有的值的分類,查看是不是切片類型reflect.Slice
Index()返回v持有值的第i個元素。如果v的Kind不是Array、Chan、Slice、String,或者i出界,會panic
Interface()返回reflect.Value當前持有的值(表示為/保管在interface{}類型),因為傳進來的值也是賦值給interface{}類型的參數x
簡化版本:
package main import( "fmt" "reflect" ) func main() { xs := []int{2, 4, 6, 8} fmt.Println(" 5 @", IndexReflectX(xs, 5), " 6 @", IndexReflectX(xs, 6)) ys := []string{"C", "B", "K", "A"} fmt.Println(" Z @", IndexReflectX(ys, "z"), " A @", IndexReflectX(ys, "A")) } func IndexReflectX(xs interface{}, x interface{}) int { if slice := reflect.ValueOf(xs); slice.Kind() == reflect.Slice { for i := 0; i < slice.Len(); i++ { if reflect.DeepEqual(x, slice.Index(i)) { return i } } } return -1 }
func DeepEqual(a1, a2 interface{}) bool:用來判斷兩個值是否深度一致
然后我們從返回中可以看出,使用深度對比,則找不到相應的值:
users-MacBook-Pro:~ user$ go run testGo.go 5 @ -1 6 @ -1 Z @ -1 A @ -1
2)
目的:在一個切片中查找某一項的索引
非泛型函數寫法:
func IntSliceIndex(xs []int, x int) int { for i, y := range xs { if x ==y { return i } } return -1 }
使用自定義函數將泛型函數的好處(即僅需實現一次算法)和類型特定函數的簡便性和高效率結合
如果想要改成string類型,則更改:
type StringSlice []string func (slice StringSlice) EqualTo(i int, x interface{}) bool{ return slice[i] == x.(string) } func (slice StringSlice) Len() int {return len(slice)}
因此當我們使用切片或映射時,通常可以創建泛型函數,這樣就不用使用類型測試和類型斷言
或者將我們的泛型函數寫成高階函數,對所有特定的類型相關邏輯進行抽象
43.高階函數
即將一個或者多個函數作為自己的參數,並在函數體中調用它們
44.純記憶函數
純函數即對於同一組輸入總是產生相同的結果,無副作用
如果一個純函數執行時開銷很大而且頻繁地使用相同的參數進行調用,我們可以使用記憶功能來降低處理的開銷
記憶技術就是保存計算的結果,當執行下一個相同的計算時,我們能夠返回保存的結果而不是重復執行一次計算過程
比如使用遞歸來計算斐波那契數列的開銷是很大的,而且重復計算相同的過程
解決辦法就是使用一個非遞歸的算法
1)首先先創建一個使用遞歸的具有記憶功能的斐波那契函數
package main import( "fmt" ) func main() { fmt.Println("Fibonacci(45) = ", Fibonacci(45).(int)) //1 } type memorizeFunction func(int, ...int) interface{} var Fibonacci memorizeFunction //聲明一個Fibonacci變量來保存這個類型的函數 func init(){ Fibonacci = Memorize(func(x int, xs ...int) interface{} {//Memorize可以記憶任何傳入至少一個int參數並返回一個interface{}的函數 if x < 2 { return x } return Fibonacci(x - 1).(int) + Fibonacci(x - 2).(int) //4... }) } func Memorize(function memorizeFunction) memorizeFunction{ cache := make(map[string]interface{}) return func(x int, xs ...int) interface{} { //2 key := fmt.Sprint(x) for _, i := range xs { key += fmt.Sprintf(",%d", i) }
if value, found := cache[key]; found { return value } value := function(x, xs...) //3 cache[key] = value return value } }
使用映射結構cache來保存預先計算的結果,映射的鍵是將所有的整數參數組合並用逗號分隔的字符串(⚠️go語言的映射要求鍵必須完全支持==和!=操作,字符串符合,切片不可以)
鍵key准備好后去查看映射里是否有對應的“鍵-值對”cache[key],如果有就直接返回緩存結果value;否則我們就執行傳入的參數function函數,再將結果緩存到映射中
45.面向對象編程
go的面向對象與c++、Java、python最大的不同就在於其不支持繼承,只支持聚合(也叫組合)和嵌入
聚合和嵌入的區別:
type ColoredPoint struct{//其字段可以通過point.Color、point.x、point.y來訪問 color.Color //匿名字段,因為其沒有變量名(嵌入),來自image/color包類型 x, y int //具名字段(聚合) } //創建point := ColoredPoint{} //其字段可以通過point.Color、point.x、point.y來訪問 //注意當訪問來自其他包中的類型的字段時,只用到了其名字的最后一部分,如上面是Color,而不是color.Color
go語言與眾不同之處在於它的接口、值和方法之間都保持獨立。接口用於聲明方法簽名,結構體用於聲明聚合或嵌入的值,方法用於聲明在自定義類型(通常為結構體)上的操作
自定義類型的方法和任何特殊接口之間沒有顯示的聯系;但是如果該類型的方法滿足一個或者多個接口,那么該類型的值可以用於任何接受該接口的值的地方
其實每一種類型都滿足空接口(interface{}),因此任何值都可以用於聲明了空接口的地方
聲明為匿名字段的好處是:
比如:
package main import( "fmt" ) type Human struct{ name string age int weight int } type Student struct{ Human speciality string } func main() { mark := Student{ Human{"Mark", 25, 120}, "computer"} //獲取相應的信息 fmt.Println("his name is: ", mark.name) fmt.Println("His age is: ", mark.age) fmt.Println("His weight is: ", mark.weight) fmt.Println("His speciality is: ", mark.speciality) //修改專業 mark.speciality = "AI" fmt.Println("His changed speciality is: ", mark.speciality) //修改年齡 mark.age = 46 fmt.Println("His changesd age is: ", mark.age) //修改體重 mark.weight += 60 fmt.Println("His changed weight is: ", mark.weight) }
可見,定義為匿名函數,當你想要訪問Human中的值的時候,可以簡單地使用mark.age來訪問
當然,如果參數有重名的,默認會先訪問外面的重名值。
如果你想要訪問里面的重名值,你可以使用Human作為字段名,使用mark.Human.age來指明你訪問的是重名中的那個值
46.自定義類型
語法:
type typeName typeSpecification
typeName可以是一個包或者函數內唯一的任何合法的go標識符
typeSpecification可以是任何內置的類型(如string、int、切片、通道)、一個接口、一個結構體或者一個函數簽名,舉例:
type Count int type StringMap map[string]string type FloatChan chan float64
這樣的自定義類型提升了程序的可讀性,也可以在后面改變其底層類型。但是其實並沒有什么用,所以一般都只把其當作基本的抽象機制
package main import( "fmt" ) func main() { var i Count = 7 i++ fmt.Println(i) sm := make(StringMap) sm["key1"] = "value1" sm["key2"] = "value2" fmt.Println(sm) fc := make(FloatChan, 1) fc <- 2.299999 fmt.Println(<-fc) } type Count int type StringMap map[string]string type FloatChan chan float64
返回:
userdeMBP:go-learning user$ go run test.go 8 map[key1:value1 key2:value2] 2.299999
向上面的Count、StringMap和FloatChan都是直接基於內置類型創建的,因此可以當作內置類型來使用
因此StringMap也可以調用內置函數append()
但是如果要將其傳遞給一個接受其底層類型的函數,就必須先將其轉換成底層類型(無需成本,因為這是在編譯是完成的)
有時我們可能需要將一個內置類型的值升級成一個自定義類型的值,以使用其自定義類型的方法
47.自定義方法
定義方法的語法幾乎等同於定義函數,除了需要在func關鍵字和方法名之間需要寫上接受者,接受者的類型總是一個該類型的值,或者該類型值的指針
1)重寫方法
package main import( "fmt" ) func main() { special := SpecialItem{Item{"Green", 3, 5}, 207} fmt.Println(special.id, special.price, special.quantity, special.catalogId) fmt.Println(special.Cost()) } type Item struct{ id string //具名字段(聚合) price float64 quantity int } func (item *Item) Cost() float64{ return item.price * float64(item.quantity) } type SpecialItem struct{ Item //匿名字段(嵌入) catalogId int }
返回:
userdeMBP:go-learning user$ go run test.go Green 3 5 207 15
因此我們可以在SpecialItem上調用Item的Cost()方法,即SpecialItem.Cost(),傳入的是嵌入的Item值,而非整個調用該方法的SpecialItem
當然,如果Item中有字段和SpecialItem字段同名,比如都有price,那么要調用Item的就使用special.Item.price
當然,我們也可以聲明一個Cost()函數來覆蓋Item的Cost()函數,有三種寫法:
func (item *LuxuryItem) Cost() float64{ //太冗長 return item.Item.Price * float64(item.Item.quantity) * item.makeup } func (item *LuxuryItem) Cost() float64{ //沒必要重復 return item.price * float64(item.quantity) * item.makeup } func (item *LuxuryItem) Cost() float64{ //完美 return item.Item.Cost() * item.makeup }
整體代碼:
package main import( "fmt" ) func main() { luxury := LuxuryItem{Item{"Green", 3, 5}, 20} fmt.Println(luxury.id, luxury.price, luxury.quantity, luxury.makeup) fmt.Println(luxury.Cost()) } type Item struct{ id string //具名字段(聚合) price float64 quantity int } func (item *Item) Cost() float64{ return item.price * float64(item.quantity) } type LuxuryItem struct{ Item //匿名字段(嵌入) makeup float64 } func (item *LuxuryItem) Cost() float64{ //完美 return item.Item.Cost() * item.makeup }
返回:
userdeMBP:go-learning user$ go run test.go Green 3 5 20 300
2)方法表達式
就像對函數進行賦值和傳遞一樣,也可以對方法進行賦值和傳遞
⚠️方法表達式是一個必須將方法類型作為第一個參數的函數
var part Part asStringV := Part.String //聲明asStringV的有效簽名為func(Part) string sv := asStringV(part) //將類型為Part的part作為第一個參數 hasPrefix := Part.HasPrefix //聲明hasPrefix的有效簽名為func (Part, string) bool asStringP := (* Part).String //聲明asStringP的有效簽名為func (*Part) string sp := asStringP(&part) lower := (*Part).LowerCase //聲明lower的有效簽名為func(* Part) lower(&part) fmt.Println(sv, sp, hasPrefix(part, "w"), part)
以上的自定義類型都有一個潛在的致命錯誤,都沒有對自定義類型進行限制,Part.Id等字段可以被設置為任何想要設置的值,所以要進行下面的驗證
3)驗證類型
package main import( "fmt" ) type Place struct{ latitude, longitude float64 //需要驗證 Name string } //saneAngle()函數:接受一個舊的角度值和一個新的角度值,如果新值在其范圍內則返回新值 func New(latitude, longitude float64, name string) *Place{//保證總是能夠創建一個合法的*place.Place,go慣例調用New()構造函數 return &Place{saneAngle(0, latitude), saneAngle{0, longitude}, name} } func (place *Place) Latitude() float64 { return place.latitude} func (place *Place) SetLatitude(latitude float64){ place.latitude = saneAngle(place.latitude, latitude) } func (place *Place) Longitude() float64 { return place.longitude} func (place *Place) SetLongitude(longitude float64){ place.longitude = saneAngle(place.longitude, longitude) } func (place *Place) String() string { return fmt.Sprintf("(%.3f, %.3f)%q", place.latitude, place.longitude, place.name) }
48.接口嵌入
即在某個接口中嵌入其他接口
type LowerCaser interface{ LowerCase() } type UpperCaser interface{ UpperCase() } type LowerUpperCaser interface{ LowerCaser UpperCaser } //實際上就等價於 type LowerUpperCaser interface{ LowerCase() UpperCase() }
但是上面比下面的寫法好的一點在於,如果LowerCaser和UpperCaser接口添加或刪減了方法,LowerUpperCaser接口也會相應地進行變化,無需改變其代碼
1)interface值
那么interface 里面到底能夠存什么值呢?
如果我們定義了一個interface的變量,那么該變量就能夠存儲實現了這個interface的任意類型的對象,如下面的例子:
package main import "fmt" type Human struct{ name string age int phone string } type Student struct{ Human//匿名字段 school string loan float32 } type Employee struct { Human//匿名字段 company string money float32 } //該interface被Human、Employee、Student都實現了 type Men interface{ SayHi() Sing(lyrics string) } //Human實現SayHi方法 func (h Human) SayHi(){ fmt.Printf("hi i am %s ,you can call me on %s\n", h.name, h.phone) } //Human實現SayHi方法 func (h Human) Sing(lyrics string){ fmt.Println("la la la la ...", lyrics) } //Employee重載Human的SayHi方法 func (e Employee) SayHi(){ fmt.Printf("hi i am %s, i work at %s. Call me on %s\n", e.name, e.company, e.phone) } func main() { mike := Student{Human{"Mike", 25, "222-222-xxx"}, "MIT", 0.00} paul := Student{Human{"Paul", 26, "111-222-xxx"}, "Harvard", 100} sam := Employee{Human{"Sam", 36, "444-222-xxx"}, "Golang Inc.", 1000} tom := Employee{Human{"Tom", 36, "333-444-xxx"}, "Tings Ltd.", 5000} //定義一個interface Men的變量 var i Men //i能存儲Human、Student和Employee的值 i = mike fmt.Println("this is mike, a student") i.SayHi() i.Sing("rain") i = tom fmt.Println("this is tom, a Employee") i.SayHi() i.Sing("wild") //也可以定義一個切片Men來分別存儲三種Human、Student和Employee的值 x := make([]Men, 3) x[0], x[1], x[2] = paul, sam, mike for _, value := range x{ value.SayHi() } }
返回:
userdeMBP:go-learning user$ go run test.go his name is: Mark His age is: 25 His weight is: 120 His speciality is: computer His changed speciality is: AI His changesd age is: 46 His changed weight is: 180 userdeMBP:go-learning user$ go run test.go this is mike, a student hi i am Mike ,you can call me on 222-222-xxx la la la la ... rain this is tom, a Employee hi i am Tom, i work at Tings Ltd.. Call me on 333-444-xxx la la la la ... wild hi i am Paul ,you can call me on 111-222-xxx hi i am Sam, i work at Golang Inc.. Call me on 444-222-xxx hi i am Mike ,you can call me on 222-222-xxx
2)interface函數參數
從上面的可知,interface的變量可以持有任意實現該interface類型的對象。
這樣我們就想是否可以通過定義interface參數,讓函數接受各種類型的參數。在這里舉例的是fmt包中的Println函數,我們發現它能夠接受任意類型的數據。
首先在fmt的源碼中定義了一個interface:
type Stringer interface { String() string }
定義任何實現了String()方法,即Stringer interface的類型都能夠作為參數被fmt.Println調用
所以,如果你想要某個類型能被fmt包義特殊格式輸出,那么你就要實現該interface;如果你沒有實現,那么就會義默認的方式輸出,如下:
package main import( "fmt" ) type Human struct{ name string age int phone string } func main() { Bob := Human{"Bob", 39, "000-777-xxx"} fmt.Println("this human is : ", Bob) }
上面將以默認的格式輸出:
userdeMBP:go-learning user$ go run test.go this human is : {Bob 39 000-777-xxx}
如果實現了interface,如下:
package main import( "fmt" "strconv" ) type Human struct{ name string age int phone string } func (h Human) String() string{ return " " + h.name + " - " + strconv.Itoa(h.age) + " years - phone : " + h.phone } func main() { Bob := Human{"Bob", 39, "000-777-xxx"} fmt.Println("this human is : ", Bob) }
以特殊格式返回:
userdeMBP:go-learning user$ go run test.go this human is : Bob - 39 years - phone : 000-777-xxx
3)那么我門如何反向知道目前的interface變量中存儲的是什么類型呢?使用:
value, ok = element.(T)//T為類型,即int,[]string,Student value = element.(type) //返回element的類型
49.反射
反射就是動態運行時的狀態。一般是使用reflect包
使用reflect包一般分為下面三步:
1)如果要去反射一個類型的值(這些值都實現了空interface),首先需要將它轉化成reflect對象(使用reflect.Type或reflect.Value,根據不同的情況去調用不同的函數):
t := reflect.TypeOf(i) //得到類型的元數據,通過t我們能獲取類型定義里面的所有元素 v := reflect.ValueOf(i) //得到實際的值,通過v我們獲取存儲在里面的值,還可以去改變值
2)轉化成reflect對象后就能夠進行一些操作,即將reflect對象轉化成相應的值,如:
tag := t.Elem().Field(0).Tag //獲取定義在struct里面的標簽 name := v.Elem().Field(0).String() //獲取存儲在第一個字段里面的值
獲取反射值能返回相應的類型和數值:
package main import( "fmt" "reflect" ) func main() { var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("type : ", v.Type()) //type : float64 fmt.Println("kind is float64 :", v.Kind() == reflect.Float64) //kind is float64 : true fmt.Println("value : ", v.Float()) //value : 3.4 }
3)反射的字段必須是可修改的,即可讀寫的,那么就要引用傳遞
如果按下面這樣寫,就會發生錯誤:
var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1)
應該寫成:
var x float64 = 3.4 p := reflect.ValueOf(&x) v := p.Elem() v.SetFloat(7.1)
這只是簡單的介紹,更詳細的介紹看go標准庫的學習-reflect
50.結構體
當值(在結構體中叫字段)來自不同類型時,它不能存儲在一個切片中(除非我們使用[]interface{}),比如:
package main import( "fmt" ) func main() { xs := make([]interface{}, 4) map1 := make(map[int]int) map1[0] = 2 map2 := make(map[int]string) map2[1] = "A" xs[0] = map1 xs[1] = map2 fmt.Println(xs) //返回:[map[0:2] map[1:A] <nil> <nil>] }
結構體字段的調用,切片使用[]索引,結構體則是.
package main import( "fmt" ) func main() { points := []struct{x, y int} {{4,6}, {}, {-7, 11}} for _, point := range points{ fmt.Printf("(%d, %d)", point.x, point.y) } fmt.Println() }
返回:
userdeMBP:go-learning user$ go run test.go (4, 6)(0, 0)(-7, 11)
結構體除了可以聚合和嵌入一個具體的類型外,也可以聚合和嵌入接口。但是反之在接口中聚合或嵌入結構體是不行的,因為接口是完全抽象的概念,這樣的聚合和嵌入毫無意義
當一個結構體包含聚合(具名的)或嵌入(匿名的)接口類型的字段時,這意味着該結構體可以將任意滿足該接口規格的值存儲在該字段中
51.並發編程goroutine
main()函數就是由一個單獨的goroutine來執行的
Go 程序中使用 go 關鍵字為一個函數創建一個 goroutine。一個函數可以被創建多個 goroutine,一個 goroutine 必定對應一個函數。
使用格式:
go 函數名( 參數列表 )
可見上面並不需要返回值,在goroutine 中返回數據使用的是通道chan
⚠️所有goroutine在main函數結束時會一同結束
陷阱:
1)主goroutine在其他工作goroutine還沒有完成時就提前退出:所以必須保證所有工作goroutine都完成后才讓主goroutine退出
2)死鎖
1》即使所有工作都已經完成,但是主goroutine和工作goroutine還存活。一般是由於工作完成了,但是主goroutine無法獲得工作goroutine的完成狀態
2》當兩個不同的goroutine都鎖定了受保護的資源,而且同時嘗試去獲得對方資源的時候,即使用鎖的時候會出現。但是在go中並不多見,因為go中使用通道來避免使用鎖
1)為了避免程序提前退出或不能正常退出,常見用法是讓主goroutine在一個done通道上等待,根據接收到的消息來判斷工作是否完成
2)另一種避免這些陷阱的方法就是使用sync.WaitGroup來讓每個工作goroutine報告自己的完成狀態。但是使用sync.WaitGroup本身也會產生死鎖,特別是當所有的工作goroutine都處於鎖定狀態的時候(等待接受通道的數據)調用sync.WaitGroup.Wait()
就算只使用通道,仍然可能發生死鎖。假如有若干個goroutine可以互相通知對方去執行某個函數(向對方發一個請求),現在,如果這些被請求執行的函數中有一個函數向執行它的goroutine發送了一些東西,例如數據,死鎖就發生了
通道為並發運行的goroutine之間提供了一種無鎖通信方式
本質上說在通道中傳輸布爾類型、整形或float64類型的值都是安全的,因為它們都是通過復制的方法來傳送的
1)但是go不保證在通道中發送指針或者引用類型(如切片或映射)的安全性,因為指針指向的內容或者所引用的值可能在對方接收到時就已經被發送方修改。因此對這些值的訪問必須要串行進行
2)除了使用互斥量實現串行訪問,另一種辦法就是設定一個規則,一旦指針或者引用發送之后發送方就不會再訪問它,然后讓接受者來訪問和釋放指針或者引用指向的值。如果雙方都發送指針或者引用的話,就雙方都要接受這種機制
3)第三種方法就是讓所有導出的方法不能修改其值,所有可修改其值的方法都不引出。這樣外部可以通過引出的這些方法進行並發訪問,但是內部實現只允許一個goroutine去訪問它的非導出方法
使用並發的最簡單的一種方式就是用一個goroutine來准備工作,然后讓另一個goroutine來執行處理,讓主goroutine和一些通道來安排一切事情
舉例說明:
func main() { jobs := make(chan Job) done := make(chan bool, len(jobList)) go func(){ for _, job := range jobList{ jobs <- job //阻塞,等待接收方接收 } close(jobs) }() go func(){ for job := range jobs{ //等待發送方發送數據 fmt.Println(job) done <- true //只要接收到一個數據就傳送一個true } }() for i := 0; i < len(jobList); i++{//該代碼目的是確保主goroutine等到所有的工作完成后才退出,即接收到len(jobList)個長度的true <- done } }
可以在聲明時將通道設置為單向的
chan <- Type :聲明一個只允許別人朝該通道中發送數據的通道,即只寫
<-chan Type :聲明一個只允許將通道中的數據發送出去的通道,即只讀
52.並發的grep(cgrep)
並發編程更常見的一種方式就是我們有很多工作需要處理,且每個工作都可以獨立完成。比如go語言標准庫中的net/http包的HTTP服務器利用這種模式來處理並發,每個請求都在一個獨立的goroutine里處理,和其他的goroutine之間沒有任何通信
這里我們實現一個cgrep程序來實現這一種模式。
目的:從命令行中讀取一個正則表達式和一個文件列表,然后輸出文件名、行號,和每個文件中所有匹配這個表達式的行。沒匹配的話就什么也不輸出
這里有很多例子都沒有看,之后再轉回來看!!!!!!!
參考http://c.biancheng.net/golang/concurrent/
53.調整並發的運行性能(使用runtime標准庫)
在 Go 程序運行時(runtime)實現了一個小型的任務調度器。這套調度器的工作原理類似於操作系統調度線程,Go 程序調度器可以高效地將 CPU 資源分配給每一個任務。傳統邏輯中,開發者需要維護線程池中線程與 CPU 核心數量的對應關系。同樣的,Go 地中也可以通過 runtime.GOMAXPROCS() 函數做到,格式為:
runtime.GOMAXPROCS(邏輯CPU數量)
func GOMAXPROCS
func GOMAXPROCS(n int) int
GOMAXPROCS設置可同時執行的最大CPU數,並返回先前的設置。 若 n < 1,它就不會更改當前設置;n = 1,單核運行;n > 1,多核並發運行
本地機器的邏輯CPU數可通過 NumCPU 查詢。
本函數在調度程序優化后會去掉。
func NumCPU
func NumCPU() int
NumCPU返回本地機器的邏輯CPU個數。
所以最終的設置版本為:
runtime.GOMAXPROCS(runtime.NumCPU())
GO 語言在 GOMAXPROCS 數量與任務數量相等時,可以做到並行執行,但一般情況下都是並發執行。
goroutine 屬於搶占式任務處理,已經和現有的多線程和多進程任務處理非常類似。應用程序對 CPU 的控制最終還需要由操作系統來管理,操作系統如果發現一個應用程序長時間大量地占用 CPU,那么用戶有權終止這個任務。
54.通道(chan)
函數和函數間需要交換數據才能體現並發執行函數的意義
在go語言中提倡使用通道(chan)的方法代替內存
在任何時候,同時只能有一個 goroutine 訪問通道進行發送和獲取數據。
1)通道聲明:
var 通道變量 chan 通道類型
chan 類型的空值是 nil,聲明后需要配合 make 后才能使用。
可以在聲明時將通道設置為單向的:
- chan <- Type :聲明一個只允許別人朝該通道中發送數據的通道,即只寫
- <-chan Type :聲明一個只允許將通道中的數據發送出去的通道,即只讀
舉例:
ch := make(chan int) // 聲明一個只能發送的通道類型, 並賦值為ch,即只寫入 var chSendOnly chan<- int = ch //聲明一個只能接收的通道類型, 並賦值為ch,即只讀出 var chRecvOnly <-chan int = ch
但是,一個不能填充數據(發送)只能讀取的通道是毫無意義的。即<-chan Type
使用的方式有將其作為一個計時器,在標准庫time中可見
type Ticker
type Ticker struct { C <-chan Time // 周期性傳遞時間信息的通道 r runtimeTimer }
Ticker保管一個通道,並每隔一段時間向其傳遞"tick"。
func Tick
func Tick(d Duration) <-chan Time
Tick是NewTicker的封裝,只提供對Ticker的通道的訪問。如果不需要關閉Ticker,本函數就很方便。
實現代碼:
func Tick(d Duration) <-chan Time { if d <= 0 { return nil } return NewTicker(d).C }
func NewTicker
func NewTicker(d Duration) *Ticker
NewTicker返回一個新的Ticker,該Ticker包含一個通道字段,並會每隔時間段d就向該通道發送當時的時間。它會調整時間間隔或者丟棄tick信息以適應反應慢的接收者。如果d<=0會panic。關閉該Ticker可以釋放相關資源。
實現代碼;
func NewTicker(d Duration) *Ticker { if d <= 0 { panic(errors.New("non-positive interval for NewTicker")) } // Give the channel a 1-element time buffer. // If the client falls behind while reading, we drop ticks // on the floor until the client catches up. c := make(chan Time, 1) t := &Ticker{ C: c, r: runtimeTimer{ when: when(d), period: int64(d), f: sendTime, arg: c, }, } startTimer(&t.r) return t }
單向通道有利於代碼接口的嚴謹性。
2)通道創建:
因為是引用類型,需要使用make來創建:
通道實例 := make(chan 數據類型)
3)通道接收有如下特性:
① 通道的收發操作在不同的兩個 goroutine 間進行。
② 接收將持續阻塞直到發送方發送數據。
③ 每次接收一個元素。
4)通道數據接受
通道的數據接收一共有以下 4 種寫法。
1) 阻塞接收數據
阻塞模式接收數據時,將接收變量作為<-
操作符的左值,格式如下:
data := <-ch
執行該語句時將會阻塞,直到接收到數據並賦值給 data 變量。
2) 非阻塞接收數據
使用非阻塞方式從通道接收數據時,語句不會發生阻塞,格式如下:
data, ok := <-ch
- data:表示接收到的數據。未接收到數據時,data 為通道類型的零值。
- ok:表示是否接收到數據。
非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。
如果需要實現接收超時檢測,可以配合 select 和計時器 channel 進行,可以參見后面的內容。
3) 接收任意數據,忽略接收的數據
阻塞接收數據后,忽略從通道返回的數據,格式如下:
<-ch
執行該語句時將會發生阻塞,直到接收到數據,但接收到的數據會被忽略。這個方式實際上只是通過通道在 goroutine 間阻塞收發實現並發同步。
4)循環接受——使用for range
for data := range ch { ... }
5)通道的多路復用——同時處理接收和發送多個通道的數據
辦法就是使用select。select 的每個 case 都會對應一個通道的收發過程。多個操作在每次 select 中挑選一個進行響應。
格式為:
select{ case 操作1: 響應操作1 case 操作2: 響應操作2 … default://有default則說明是非阻塞的,當沒有任何操作時,則默認執行default中的語句 沒有操作情況 }
case中的操作語句的類型分為下面的三種:
- 等待接收任意數據:case <- ch
- 等待接收數據並賦值到變量中: case d := <- ch
- 等待發送數據: case ch <- 100
6)關閉通道后(使用close())如何繼續使用通道
通道是一個引用對象,和 map 類似。map 在沒有任何外部引用時,Go 程序在運行時(runtime)會自動對內存進行垃圾回收(Garbage Collection, GC)。類似的,通道也可以被垃圾回收,但是通道也可以被主動關閉。
格式:
使用 close() 來關閉一個通道:
close(ch)
關閉的通道依然可以被訪問,訪問被關閉的通道將會發生一些問題。
給被關閉通道發送數據將會觸發panic
被關閉的通道不會被置為 nil。如果嘗試對已經關閉的通道進行發送,將會觸發宕機,代碼如下:
package main import "fmt" func main() { // 創建一個整型的通道 ch := make(chan int) // 關閉通道 close(ch) // 雖然通道已經被關閉了,但是還是能夠打印通道的指針, 容量和長度 fmt.Printf("ptr:%p cap:%d len:%d\n", ch, cap(ch), len(ch)) // 但是如果給關閉的通道發送數據 ch <- 1 }
返回:
userdeMBP:go-learning user$ go run test.go ptr:0xc000062060 cap:0 len:0 panic: send on closed channel goroutine 1 [running]: main.main() /Users/user/go-learning/test.go:16 +0x144 exit status 2
從已關閉的通道接收數據時將不會發生阻塞
package main import "fmt" func main() { // 創建一個整型帶兩個緩沖的通道 ch := make(chan int, 2) // 給通道放入兩個數據 ch <- 0 ch <- 1 // 關閉緩沖 close(ch) // 遍歷緩沖所有數據, 且多遍歷1個,故意造成這個通道的超界訪問 for i := 0; i < cap(ch)+1; i++ { // 從通道中取出數據 v, ok := <-ch // 打印取出數據的狀態 fmt.Println(v, ok) } }
返回:
userdeMBP:go-learning user$ go run test.go 0 true 1 true 0 false
運行結果前兩行正確輸出帶緩沖通道的數據,表明緩沖通道在關閉后依然可以訪問內部的數據。
運行結果第三行的“0 false”表示通道在關閉狀態下取出的值。0 表示這個通道的默認值,false 表示沒有獲取成功,因為此時通道已經空了。我們發現,在通道關閉后,即便通道沒有數據,在獲取時也不會發生阻塞,但此時取出數據會失敗。
7)競態檢測——檢測代碼在並發環境下可能出現的問題
通道內部的實現依然使用了各種鎖,因此優雅代碼的代價是性能。在某些輕量級的場合,原子訪問(atomic包)、互斥鎖(sync.Mutex)以及等待組(sync.WaitGroup)能最大程度滿足需求。
1》原子訪問
當多線程並發運行的程序競爭訪問和修改同一塊資源時,會發生競態問題。
下面的代碼中有一個 ID 生成器,每次調用生成器將會生成一個不會重復的順序序號,使用 10 個並發生成序號,觀察 10 個並發后的結果。
package main import ( "fmt" "sync/atomic" ) var ( // 序列號 seq int64 ) // 序列號生成器 func GenID() int64 { // 嘗試原子的增加序列號 atomic.AddInt64(&seq, 1) //這里故意沒有使用 atomic.AddInt64() 的返回值作為 GenID() 函數的返回值,因此會造成一個競態問題。 return seq } func main() { //生成10個並發序列號 for i := 0; i < 10; i++ { go GenID() } fmt.Println(GenID()) }
如果正常運行:
userdeMBP:go-learning user$ go run test.go 9 //並不是期待的結果
在運行程序時,為運行參數加入-race
參數,開啟運行時(runtime)對競態問題的分析:
userdeMBP:go-learning user$ go run -race test.go ================== WARNING: DATA RACE Write at 0x000001212840 by goroutine 7: sync/atomic.AddInt64() /usr/local/Cellar/go/1.11.4/libexec/src/runtime/race_amd64.s:276 +0xb main.GenID() /Users/user/go-learning/test.go:17 +0x43 Previous read at 0x000001212840 by goroutine 6: main.GenID() /Users/user/go-learning/test.go:19 +0x53 Goroutine 7 (running) created at: main.main() /Users/user/go-learning/test.go:26 +0x4f Goroutine 6 (finished) created at: main.main() /Users/user/go-learning/test.go:26 +0x4f ================== 10 Found 1 data race(s) exit status 66
可見第6個goroutine和第7個goroutine之間發生了競態問題
但是如果我們將GetID()的返回更改成:
func GenID() int64 { // 嘗試原子的增加序列號 return atomic.AddInt64(&seq, 1) }
競態問題就解決了,然后返回:
userdeMBP:go-learning user$ go run -race test.go 10
本例中只是對變量進行增減操作,雖然可以使用互斥鎖(sync.Mutex)解決競態問題,但是對性能消耗較大。在這種情況下,推薦使用原子操作(atomic)進行變量操作。
2》互斥鎖(sync.Mutex)
互斥鎖是一種常用的控制共享資源訪問的方法,它能夠保證同時只有一個 goroutine 可以訪問共享資源
package main import ( "fmt" "sync" ) var ( // 邏輯中使用的某個變量 count int // 與變量對應的使用互斥鎖,一般情況下,建議將互斥鎖的粒度設置得越小越好,降低因為共享訪問時等待的時間 countGuard sync.Mutex //保證修改 count 值的過程是一個原子過程,不會發生並發訪問沖突 ) func GetCount() int { // 鎖定,此時如果另外一個 goroutine 嘗試繼續加鎖時將會發生阻塞,直到這個 countGuard 被解鎖 countGuard.Lock() // 在函數退出時解除鎖定 defer countGuard.Unlock() return count } func SetCount(c int) { countGuard.Lock() count = c countGuard.Unlock() } func main() { // 可以進行並發安全的設置 SetCount(1) // 可以進行並發安全的獲取 fmt.Println(GetCount()) }
返回:
userdeMBP:go-learning user$ go run -race test.go 1 userdeMBP:go-learning user$ go run test.go 1
3》讀寫互斥鎖(sync.RWMutex)
在讀多寫少的環境中,可以優先使用讀寫互斥鎖(sync.RWMutex),它比互斥鎖更加高效。sync 包中的 RWMutex 提供了讀寫互斥鎖的封裝。
將上面互斥鎖例子中的一部分代碼修改為讀寫互斥鎖:
var ( // 邏輯中使用的某個變量 count int // 與變量對應的使用讀寫互斥鎖,差別就是當另一個goroutine也要讀取改數據時,不會發生阻塞 countGuard sync.RWMutex ) func GetCount() int { // 鎖定 countGuard.RLock() // 在函數退出時解除鎖定 defer countGuard.RUnlock() return count }
4》等待組(sync.WaitGroup)
除了可以使用通道(channel)和互斥鎖進行兩個並發程序間的同步外,還可以使用等待組進行多個任務的同步,等待組可以保證在並發環境中完成指定數量的任務
WaitGroup用於等待一組線程的結束。父線程調用Add方法來設定應等待的線程的數量。每個被等待的線程在結束時應調用Done方法。同時,主線程里可以調用Wait方法阻塞至所有線程結束。
三個方法:
func (*WaitGroup) Add
func (wg *WaitGroup) Add(delta int)
Add方法向內部計數加上delta,delta可以是負數;如果內部計數器變為0,Wait方法阻塞等待的所有線程都會釋放,如果計數器小於0,方法panic。注意Add加上正數的調用應在Wait之前,否則Wait可能只會等待很少的線程。一般來說本方法應在創建新的線程或者其他應等待的事件之前調用。
func (*WaitGroup) Done
func (wg *WaitGroup) Done()
Done方法減少WaitGroup計數器的值,即-1,應在線程的最后執行。
func (*WaitGroup) Wait
func (wg *WaitGroup) Wait()
Wait方法阻塞直到WaitGroup計數器減為0。
舉例說明,當我們添加了 N 個並發任務進行工作時,就將等待組的計數器值增加 N。每個任務完成時,這個值減 1。同時,在另外一個 goroutine 中等待這個等待組的計數器值為 0 時,表示所有任務已經完成:
package main import ( "fmt" "net/http" "sync" ) func main() { // 聲明一個等待組 var wg sync.WaitGroup // 准備一系列的網站地址 var urls = []string{ "http://www.github.com/", "https://www.qiniu.com/", "https://www.golangtc.com/", } // 遍歷這些地址 for _, url := range urls { // 每一個任務開始時, 將等待組增加1 wg.Add(1) // 開啟一個並發 go func(url string) { // 使用defer, 表示函數完成時將等待組值減1 // wg.Done() 方法等效於執行 wg.Add(-1) defer wg.Done() // 使用http訪問提供的地址 // Get() 函數會一直阻塞直到網站響應或者超時 _, err := http.Get(url) //在網站響應和超時后,打印這個網站的地址和可能發生的錯誤 fmt.Println(url, err) // 通過參數傳遞url地址 }(url) } // 等待所有的網站都響應或者超時后,任務完成,Wait 就會停止阻塞。 wg.Wait() fmt.Println("over") }
返回:
userdeMBP:go-learning user$ go run test.go https://www.golangtc.com/ <nil> http://www.github.com/ <nil> https://www.qiniu.com/ Get https://www.qiniu.com/: dial tcp 124.200.113.148:443: i/o timeout over
55.實現遠程過程調用(RPC)
使用通道chan代替socket實現RPC的例子:
package main import( "fmt" "time" "errors" ) //模擬RPC客戶端的請求和接收消息封裝 func RPCClient(ch chan string, req string) (string, error){ //向服務端發送數據 ch <- req //等待服務端返回數據 select{ case ack := <- ch: return ack, nil case <- time.After(time.Second): return "", errors.New("time out") } } func RPCServer(ch chan string){ for{ //接收客戶的請求 data := <-ch //打印接收到的數據 fmt.Println("server received: ", data) //並給客戶反饋結果 ch <- "received" } } func main() { //創建一個無緩沖字符串通道 ch := make(chan string) //並發執行服務器 go RPCServer(ch) //客戶端請求數據並接收數據 recv, err := RPCClient(ch, "hello server") if err != nil{ fmt.Println(err) }else{ //打印接收到的數據 fmt.Println("client received : ", recv) } }
⚠️time.After(time.Second):用於實現超時操作
返回:
userdeMBP:go-learning user$ go run test.go
server received: hello server
client received : received
模擬超時:
package main import( "fmt" "time" "errors" ) //模擬RPC客戶端的請求和接收消息封裝 func RPCClient(ch chan string, req string) (string, error){ //向服務端發送數據 ch <- req //等待服務端返回數據 select{ case ack := <- ch: return ack, nil
//time.After即一段時間后,這里是一秒后 case <- time.After(time.Second): //因為這里如果客戶端處理超過1秒就會返回超時的錯誤 return "", errors.New("time out") } } //模擬超時主要改的是客戶端 func RPCServer(ch chan string){ for{ //接收客戶的請求 data := <-ch //打印接收到的數據 fmt.Println("server received: ", data) // 通過睡眠函數讓程序執行阻塞2秒的任務 time.Sleep(time.Second * 2) //然后再給客戶反饋結果 ch <- "received" } } func main() { //創建一個無緩沖字符串通道 ch := make(chan string) //並發執行服務器 go RPCServer(ch) //客戶端請求數據並接收數據 recv, err := RPCClient(ch, "hello server") if err != nil{ fmt.Println(err) }else{ //打印接收到的數據 fmt.Println("client received : ", recv) } }
返回:
userdeMBP:go-learning user$ go run test.go server received: hello server time out
56.使用通道響應計時器——使用標准庫time
1)使用time.AfterFunc()實現等待一段時間后調用函數,並直到該函數生成的另一goroutine結束后才結束main()函數的goroutine
package main import( "fmt" "time" ) func main() { //聲明一個用於退出的通道 exit := make(chan int) fmt.Println("start") //過1秒后,就會開一個新goroutine來運行匿名函數 time.AfterFunc(time.Second, func(){ //該匿名函數的作用就是在1秒后打印結果,並通知main()函數可以結束主goroutine fmt.Println("one second after") exit <- 0 }) //main()正在等待從exit通道中接受數據來結束主goroutine <- exit }
返回:
userdeMBP:go-learning user$ go run test.go
start
one second after
如果不使用通道來控制,主goroutine一定會在一秒內先結束,這樣永遠不會運行到匿名函數中的內容:
package main import( "fmt" "time" ) func main() { //聲明一個用於退出的通道 exit := make(chan int) fmt.Println("start") //過1秒后,就會開一個新goroutine來運行匿名函數 time.AfterFunc(time.Second, func(){ //該匿名函數的作用就是在1秒后打印結果,並通知main()函數可以結束主goroutine fmt.Println("one second after") exit <- 0 }) //main()正在等待從exit通道中接受數據來結束主goroutine // <- exit //如果沒有這個,我們會發現主goroutine會在1秒前結束,同時結束上面的goroutine,這樣根本就不會輸出"one second after" fmt.Println("if there is not <- exit, won't print one second after") }
返回:
userdeMBP:go-learning user$ go run test.go start if there is not <- exit, won't print one second after
2)定點計時
計時器(Timer)的原理和倒計時鬧鍾類似,都是給定多少時間后觸發。
打點器(Ticker)的原理和鍾表類似,鍾表每到整點就會觸發。
這兩種方法創建后會返回 time.Ticker 對象和 time.Timer 對象,里面通過一個 C 成員,類型是只能接收的時間通道(<-chan Time),使用這個通道就可以獲得時間觸發的通知。
詳細內容可見go標准庫的學習-time
下面代碼創建一個打點器Ticker,每 500 毫秒觸發一起;創建一個計時器Timer,2 秒后觸發,只觸發一次。
package main import( "fmt" "time" ) func main() { //創建一個打點器,每500毫秒觸發一次 ticker := time.NewTicker(time.Millisecond * 500) //創建一個計時器,2秒后觸發 timer := time.NewTimer(time.Second * 2) //聲明計數變量 var count int //不斷檢查通道情況 for{ //多路復用通道 select{ case <- timer.C://計時器到時了,即2秒已到 fmt.Println("time is over,stop!!") goto StopLoop case <- ticker.C://打點器觸發了,說明已隔500毫秒 count++ fmt.Println("tick : ", count) } } //停止循環所到的標簽 StopLoop: fmt.Println("ending") }
返回:
userdeMBP:go-learning user$ go run test.go tick : 1 tick : 2 tick : 3 tick : 4 time is over,stop!! ending
53.文件處理
沒認真看,之后再補
54.包
我們的程序和包最好是放在GOPATH源碼目錄下的一個src的目錄中。如果這個包只屬於某個應用程序,那么可以直接放在應用程序的子目錄下。但是如果希望這個包可以被其他的應用程序共享,那就應該放在GOPATH的src目錄下.
每個包單獨放在一個目錄下,如果兩個不同的包放在同一個目錄下,會出現命名沖突的編譯錯誤
包的源碼應該放在一個同名的文件夾下,同一個包里可以有多個文件,文件后綴為.go。如果一個包中有多個文件,其中必定有一個文件的名字和包名相同