https://www.jb51.net/article/126998.htmhtml
go標準庫文檔https://studygolang.com/pkgdocpython
1.c++
若是想要再本地直接查看go官方文檔,能夠再終端中運行:git
userdeMacBook-Pro:~ user$ godoc -http=:8000
而後在瀏覽器中運行http://localhost:8000就可以查看文檔了,以下圖所示:github
2.os.Args : Args保管了命令行參數,第一個是程序名golang
3.全部的go語言代碼都只能放置在包中,每個go程序都必須包含一個main包以及一個main函數。main()函數做爲整個程序的入口,在程序運行時最早被執行。實際上go語言中的包還可能包含init()函數,它先於main()函數被執行正則表達式
4.包名和函數名之間不會發生命名衝突算法
5.go語言針對處理的單元是包而非文件,若是全部文件的包聲明都是同樣的,那麼他們屬於同一個包express
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字節表示)時才能索引
但在實際狀況下,其並非個問題,由於:
因此:
[]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包中的函數:
舉例:
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(段分隔符),這時候就使用:
舉例:
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字節,這並非咱們想要的
咱們可使用:
固然,實在想要使用索引[]的最好辦法實際上是將該字符串轉換成[]rune(這樣上面的例子將建立一個包含5個碼點的rune切片,而非上面圖中的6個字節),而後就可以使用索引調用了,代價就是該一次性轉換耗費了CPU和內存o(n)。結束後可使用string(char)來將其轉換成字符
25.fmt
fmt包中提供的掃描函數(fmt.Scan()\fmt.Scanf()\fmt.Scanln())的做用是用於從控制檯、文件以及其餘字符串類型中讀取數據
26.go語言提供了兩種建立變量的語法,同時得到它們的指針:
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,b是不相同的。數組的cap()和len()函數返回的值是相同的,它也能夠進行切片,即[i:j],可是返回的是切片,而非數組
數組是值傳遞,而切片是引用傳遞
在go的標準庫中的全部公開函數中使用的都是切片而非數組,建議使用切片
當建立一個切片的時候,他會建立一個隱藏的初始化爲零值的數組,而後返回一個引用該隱藏數組的切片。因此切片其實就是一個隱藏數組的引用
切片的建立語法:
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] }
由於更改切片其實都是對底層的隱藏數據進行了修改
⚠️切片和字符串不一樣之處在於它不支持+或+=操做符,可是對切片進行添加、插入或刪除的操做是十分簡單的
當切片包含的是自定義類型的元素時,如結構體
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的副本,這是一個指向底層數據的指針,因此成功
若是是想要從任意位置插入值,那麼須要本身寫一個函數,如:
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接口中定義的方法:
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] }
30.映射
map[string]float64:鍵爲string類型,值爲float64類型的映射
因爲[]byte是一個切片,不能做爲映射的鍵,可是咱們能夠先將[]byte轉換成字符串,如string([]byte),而後做爲映射的鍵字段,等有須要的時候再轉換回來,這種轉換並不會改變原有切片的數據
若是值的類型是接口類型,咱們就能夠建立一個知足這個接口定義的值做爲映射的值,甚至咱們能夠建立一個值爲空接口(interface{})的映射,這意味着任意類型的值均可以做爲這個映射的值
不過當咱們須要訪問這個值時,須要使用類型開關和類型斷言得到這個接口類型的實際類型,或者也能夠經過類型檢驗來得到變量的實際類型
建立方式:
第二和第三種的結果是相同的
對於比較大的映射,最好是爲其指定恰當的容量來提升性能
映射可使用索引操做符[],不一樣在於其裏面的值不用是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中將錯誤和異常區別對待:
錯誤的慣用方法是將錯誤以函數或者方法最後一個返回值的形式將其返回,並在調用它的地方檢查返回的錯誤值
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(n int) int
GOMAXPROCS設置可同時執行的最大CPU數,並返回先前的設置。 若 n < 1,它就不會更改當前設置;n = 1,單核運行;n > 1,多核併發運行
本地機器的邏輯CPU數可經過 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 後才能使用。
能夠在聲明時將通道設置爲單向的:
舉例:
ch := make(chan int) // 聲明一個只能發送的通道類型, 並賦值爲ch,即只寫入 var chSendOnly chan<- int = ch //聲明一個只能接收的通道類型, 並賦值爲ch,即只讀出 var chRecvOnly <-chan int = ch
可是,一個不能填充數據(發送)只能讀取的通道是毫無心義的。即<-chan Type
使用的方式有將其做爲一個計時器,在標準庫time中可見
type Ticker struct { C <-chan Time // 週期性傳遞時間信息的通道 r runtimeTimer }
Ticker保管一個通道,並每隔一段時間向其傳遞"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(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 種寫法。
阻塞模式接收數據時,將接收變量做爲<-
操做符的左值,格式以下:
data := <-ch
執行該語句時將會阻塞,直到接收到數據並賦值給 data 變量。
使用非阻塞方式從通道接收數據時,語句不會發生阻塞,格式以下:
data, ok := <-ch
非阻塞的通道接收方法可能形成高的 CPU 佔用,所以使用很是少。
若是須要實現接收超時檢測,能夠配合 select 和計時器 channel 進行,能夠參見後面的內容。
阻塞接收數據後,忽略從通道返回的數據,格式以下:
<-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中的操做語句的類型分爲下面的三種:
6)關閉通道後(使用close())如何繼續使用通道
通道是一個引用對象,和 map 相似。map 在沒有任何外部引用時,Go 程序在運行時(runtime)會自動對內存進行垃圾回收(Garbage Collection, GC)。相似的,通道也能夠被垃圾回收,可是通道也能夠被主動關閉。
格式:
使用 close() 來關閉一個通道:
close(ch)
關閉的通道依然能夠被訪問,訪問被關閉的通道將會發生一些問題。
被關閉的通道不會被置爲 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 (wg *WaitGroup) Add(delta int)
Add方法向內部計數加上delta,delta能夠是負數;若是內部計數器變爲0,Wait方法阻塞等待的全部線程都會釋放,若是計數器小於0,方法panic。注意Add加上正數的調用應在Wait以前,不然Wait可能只會等待不多的線程。通常來講本方法應在建立新的線程或者其餘應等待的事件以前調用。
func (wg *WaitGroup) Done()
Done方法減小WaitGroup計數器的值,即-1,應在線程的最後執行。
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。若是一個包中有多個文件,其中一定有一個文件的名字和包名相同