函數作爲值 作爲類型
在go 中函數也是一種變量, 我們可以通過type 來定義它, 他的類型就是所有擁有相同的參數, 相同的返回值的一種類型。
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
函數作爲類型的好處:可以把這個函數當作類型來傳遞
函數 當作值和類型在我們寫的一些通用接口的時候非常有用。
通過下面的例子可以看到testInt 這個類型是一個函數類型 , 然後兩個filter函數的參數和返回值與testInt 類型是一樣的, 我們可以實現很多這樣的邏輯 可以讓我們得程序非常的靈活。
package main
import "fmt"
type testInt func(int) bool // 聲明瞭一個函數類型
func isOdd(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}
func isEven(integer int) bool {
if integer%2 == 0 {
return true
}
return false
}
// 聲明的函數類型在這個地方當做了一個參數
func filter(slice []int, f testInt) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
}
}
return result
}
func main(){
slice := []int {1, 2, 3, 4, 5, 7}
fmt.Println("slice = ", slice)
odd := filter(slice, isOdd) // 函數當做值來傳遞了
fmt.Println("Odd elements of slice are: ", odd)
even := filter(slice, isEven) // 函數當做值來傳遞了
fmt.Println("Even elements of slice are: ", even)
}
switch
邏輯判斷比較多 用if else 比較長夜不太好維護
switch 可以很好的解決這個問題 代碼:
switch sExpr {
case expr1:
some instructions
case expr2:
some other instructions
case expr3:
some other instructions
default:
other code
}
注意:
sExpr和expr1、expr2、expr3的類型必須一致。Go的switch非常靈活,表達式不必是常量或整數,執行的過程從上至下,直到找到匹配項;而如果switch沒有表達式,它會匹配true
go 中switch 默認相當於每一個case 最後帶有break 匹配和成功後不會向下執行其他的case, 而是跳出整個switch 但是可以使用 fallthrough 強制執行case 後面的代碼
example :
integer := 6
switch integer {
case 4:
fmt.Println("The integer was <= 4")
fallthrough
case 5:
fmt.Println("The integer was <= 5")
fallthrough
case 6:
fmt.Println("The integer was <= 6")
fallthrough
case 7:
fmt.Println("The integer was <= 7")
fallthrough
case 8:
fmt.Println("The integer was <= 8")
fallthrough
default:
fmt.Println("default case")
}
變參
Go 函數支持變參。接受變參的函數有着不定數量的參數。
func myfunc(arg ...int) {}
使用參數
for _, n := range arg {
fmt.Printf("And the number is: %d\n", n)
}
傳參 傳值與傳指針
當我們傳遞一個參數值到被調用函數裏面時候, 實際傳遞的是一這個值的copy, 當在被調用函數中修改參數的值的時候, 調用函數中相應實參不會發生 任何變化, 因爲數值變化只作用在copy 上面。
要想 在多個函數中修改 一個變量 使用 指針。變量在內存中是存放在一定地址上的, 修改變量實際是修改變量內存的地址,下面是 傳遞指針方式
package main
import "fmt"
//簡單的一個函數,實現了參數+1的操作
func add1(a *int) int { // 請注意,
*a = *a+1 // 修改了a的值
return *a // 返回新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 應該輸出 "x = 3"
x1 := add1(&x) // 調用 add1(&x) 傳x的地址
fmt.Println("x+1 = ", x1) // 應該輸出 "x+1 = 4"
fmt.Println("x = ", x) // 應該輸出 "x = 4"
}
傳遞指針的 好處:
- 指針使得多個多個函數能夠操作 同一個對象
- 傳遞指針比較輕量級 (8bytes),只是傳遞內存地址 我們可以用指針傳遞 體積比較大的結構體。 如果用參數傳遞的話, 在每次copy 上面就會花費相對較多的系統開銷 (內存和時間)。 所以當你傳遞大的結構體的時候, 用指針是一個明智的選擇。
- Go 語言中channel slice map 這三種類型實現機制類似於指針, 所以可以直接傳遞, 而不用取地址後然後傳遞指針。(注意:若函數需要改變 slice 的長度, 則仍需要取地址傳遞指針)
defer
延遲語句, 可以在函數中添加多個defer 語句。 當函數執行到最後這些defer 語句會按照逆序執行, 最後該函數返回。 特別是當你在打開資源的操作時候, 遇到錯誤需要提前返回, 在返回前你需要關閉相應的資源, 不然很容易造成資源泄露等問題。
如果 有很多調用defer 那麼defer 是採用先進後出的模式。
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
會輸出 4 3 2 1 0
golang 中defer 的使用規則
規則一 當defer 被聲明時, 其參數就會被實時解析
package main
import "fmt"
func a() {
i := 0
defer fmt.Println(i)
i++
return
}
func main() {
a()
}
根據上面的運行結果進行分析
輸出 : 0
分析:
defer 函數 是在return 調用之後調用, 但是這裏輸出0 而不是1 這是一個我們在defer 後面定義的是一個帶變量的函數:fmt.Println(i), 但是這個變量 (i),在defer 被聲明的時候, 就已經確定了他的值。 上面這段代碼等同於:
func a() {
i := 0
defer fmt.Println(0) //因爲i=0,所以此時就明確告訴golang在程序退出時,執行輸出0的操作
i++
return
}
爲了更加明確的說明這個問題, 我們繼續定義一個defer
package main
import "fmt"
func b() {
i := 0
defer fmt.Println(i) //輸出0,因爲i此時就是0
i++
defer fmt.Println(i) //輸出1,因爲i此時就是1
return
}
func main() {
b()
}
運行結果 1 0
可以看到defer 輸出的值, 就是定義時候的值。 而不是defer 真正執行的時候變量的值
爲什麼是 1 0 而不是 01 呢 是因爲 規則二 defer 執行順序爲先進後出
規則二 defer 執行順序爲先進後出
可以運行
func b() {
for i := 0; i < 4; i++ {
defer fmt.Print(i)
}
}
輸出3 2 1 0
規則三 defer 可以讀取有名返回值
package main
import "fmt"
func c() (i int) {
defer func() { i++ }()
return 1
}
func main() {
aa := c()
fmt.Println(aa)
}
en 你猜猜打印 多少 0, 1, 2?
恭喜你猜對了 2
在開頭說過 defer 是在return 調用之後才執行。這裏需要明確的是defer 代碼塊的作用域仍然在函數之內,結合上面的函數, defer 仍然可以讀取c 函數內的變量(如果無法讀取函數內的變量, 那有如何進行清理呢。。)
當執行 return 1 的時候 i 的值就爲1 , 此刻defer 代碼塊開始執行, 對i 進行自增操作, 因此輸出2
注意:
在遇到defer 代碼的時候 根據這三個規則 明確得知 期望結果
- defer 代碼塊 在 調用reture 之後
- defer 被聲明 參數就會被實時解析
- 執行順序先進後出
- defer
defer return 返回值之間總結
其中有規則三 defer 可以讀取 有名返回值
package main
func main() {
fmt.Println(test())
fmt.Println(test1())
fmt.Println(test2())
}
func test() (result int) { // 打印 2
defer func() {
result++
}()
return 1
}
// 無名返回值
func test1() (result int) { // 打印 5
t := 5
defer func() {
t = t + 5
}()
return t
}
// 有名返回值
func test2() (result int) { // 打印 10
result = 5
defer func() {
result = result + 5
}()
return result
}
打印:
2
5
10
在看期望 打印值的時候 要看是有名還是無名的返回值 。
如果函數的返回值是無名的, 則go 語言會在執行return 的時候會執行一個類似於創建一個臨時變量作爲保存return 值的動作, defer 執行 變量 不會影響這個return 要返回的值。 而有名返回值的函數, 由於返回值是在函數定義的時候已經將該變量進行定義, 在執行return 的時候會先執行返回值保存操作, 而後續的defer 函數 會改變這個返回值 (雖然defer是在return之後執行的,但是由於使用的函數定義的變量,所以執行defer操作後對該變量的修改會影響到return的值)
證明 defer 是在return 函數之後執行
package main
import "fmt"
func main() {
fmt.Println("return a ", a())
fmt.Println("---------------------------------------")
b1,b2 := b()
fmt.Println("return b ", b1, b2)
fmt.Println("---------------------------------------")
c1,c2 := c()
fmt.Println("return c ", c1, c2)
fmt.Println("---------------------------------------")
}
//有名返回值
func a() (i int) {
defer func() {
i++
fmt.Println("defer a 2:", i)
}()
defer func() {
i++
fmt.Println("defer a 1", i)
}()
return i
}
//無名返回值
func b() (t *int, m int) {
var i int
defer func() {
i++
fmt.Println("defer b 2:", &i, i)
}()
defer func() {
i++
fmt.Println("defer b 1", &i, i)
}()
return &i,i
}
func c() (*int,int) {
var i int
defer func() {
i++
fmt.Println("defer c 2:", &i, i)
}()
defer func() {
i++
fmt.Println("defer c 1", &i, i)
}()
return &i,i
}
打印值
defer a 1 1
defer a 2: 2
return a 2
---------------------------------------
defer b 1 0x41602c 1
defer b 2: 0x41602c 2
return b 0x41602c 0
---------------------------------------
defer c 1 0x416048 1
defer c 2: 0x416048 2
return c 0x416048 0
---------------------------------------
defer 打印的值大於 返回值打印的值
測試返回值
func testPr() {
for i := 0; i < 5; i++ {
defer func() {
println(i)
}()
}
}
全部打印5
func testPr2() {
for i := 0; i < 5; i++ {
defer func(i int) {
println(i)
}(i)
}
}
打印 4 3 2 1 0
Panic 和 Recover
-
panic
是一個內建函數,可以中斷原有的控制流程,進入一個panic狀態中。當函數F調用panic,函數F的執行被中斷,但是F中的延遲函數會正常執行,然後F返回到調用它的地方。在調用的地方,F的行爲就像調用了panic。這一過程繼續向上,直到發生panic的goroutine中所有調用的函數返回,此時程序退出。panic可以直接調用panic產生。也可以由運行時錯誤產生,例如訪問越界的數組。 -
recover
是一個內建的函數,可以讓進入panic狀態的goroutine恢復過來。recover僅在延遲函數中有效。在正常的執行過程中,調用recover會返回nil,並且沒有其它任何效果。如果當前的goroutine陷入panic狀態,調用recover可以捕獲到panic的輸入值,並且恢復正常的執行。
main init 函數
這兩個是保留函數 init() 可以應用於所有的package main 函數只能應用於package main
這兩個函數在定義時候不能有任何的參數和返回值
從可讀性和維護上來說 建議在一個package 中每個文件只寫一個init 函數
go 程序會自動調用init() main() 函數 所以不需要再任何地方調用這兩個 函數
每個package 中的init 都是可選的 但是package main 就必須包含一個main 函數
程序的初始化和執行都起始於main包。如果main包還導入了其它的包,那麼就會在編譯時將它們依次導入。有時一個包會被多個包同時導入,那麼它只會被導入一次(例如很多包可能都會用到fmt包,但它只會被導入一次,因爲沒有必要導入多次)。當一個包被導入時,如果該包還導入了其它的包,那麼會先將其它包導入進來,然後再對這些包中的包級常量和變量進行初始化,接着執行init函數(如果有的話),依次類推。等所有被導入的包都加載完畢了,就會開始對main包中的包級常量和變量進行初始化,然後執行main包中的init函數(如果存在的話),最後執行main函數。下圖詳細地解釋了整個執行過程:
main 函數 引入包初始化流程圖
import
- 相對路徑
import “./model” //當前文件同一目錄的model目錄,但是不建議這種方式來import - 絕對路徑
import “shorturl/model” //加載gopath/src/shorturl/model模塊
特殊的引入
- 點操作
import(
. "fmt"
)
這個點操作的含義就是這個包導入之後在你調用這個包的函數時,你可以省略前綴的包名,也就是前面你調用的fmt.Println(“hello world”)可以省略的寫成Println(“hello world”)
- 別名操作
別名操作顧名思義我們可以把包命名成另一個我們用起來容易記憶的名字
import(
f "fmt"
)
別名操作的話調用包函數時前綴變成了我們的前綴,即f.Println(“hello world”)
- _操作
import (
"database/sql"
_ "github.com/ziutek/mymysql/godrv"
)
_操作其實是引入該包,而不直接使用包裏面的函數,而是調用了該包裏面的init函數。
struct
使用struct 匿名字段 簡單省事
不寫字段名的方式 也就是匿名字段 也稱爲嵌入字段
當匿名字段是struct 的時候, 那麼這個struct 所擁有的全部字段都被隱式的引入 了當前定義的這個struct
package main
import "fmt"
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,那麼默認Student就包含了Human的所有字段
speciality string
}
func main() {
// 我們初始化一個學生
mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
// 我們訪問相應的字段
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("Mark changed his speciality")
fmt.Println("His speciality is ", mark.speciality)
// 修改他的年齡信息
fmt.Println("Mark become old")
mark.age = 46
fmt.Println("His age is", mark.age)
// 修改他的體重信息
fmt.Println("Mark is not an athlet anymore")
mark.weight += 60
fmt.Println("His weight is", mark.weight)
}
struct 不僅能夠將 struct 作爲匿名字段, 自定義,類型 內置類型都可以作爲匿名字段
而且可以在相應的字段上面進行函數操作。
面向對象 method
method : 帶有接收者的函數
method 是附屬在一個給定的類型上的, 它的語法和函數的聲明語法幾乎一樣, 只是在func 後面增加一個receiver (也就是method 所依從的主體)
method 語法
func (r ReceiverType) funcName(parameters) (results)
package main
import (
"fmt"
"math"
)
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
func (r Rectangle) area() float64 {
return r.width*r.height
}
func (c Circle) area() float64 {
return c.radius * c.radius * math.Pi
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
c1 := Circle{10}
c2 := Circle{25}
fmt.Println("Area of r1 is: ", r1.area())
fmt.Println("Area of r2 is: ", r2.area())
fmt.Println("Area of c1 is: ", c1.area())
fmt.Println("Area of c2 is: ", c2.area())
}
注意
- 雖然method 的名字一樣但是如果接受這不一樣 那麼method 就不一樣
- method 裏面可以訪問接受者的字段
- 調用method 通過 . 訪問就像struct 裏面訪問字段一樣
以上方法的receiver 是以值傳遞, 而非 引用傳遞 receiver 還可以是指針, 兩者的差別在於 指針作爲 receiver 會對實例對象的內容發生操作, 而普通類型作爲Receiver 僅僅是以副本作爲操作對象, 並不對原實例對象發生操作。
package main
import "fmt"
const(
WHITE = iota
BLACK
BLUE
RED
YELLOW
)
type Color byte
type Box struct {
width, height, depth float64
color Color
}
type BoxList []Box //a slice of boxes
func (b Box) Volume() float64 {
return b.width * b.height * b.depth
}
func (b *Box) SetColor(c Color) {
b.color = c
}
func (bl BoxList) BiggestColor() Color {
v := 0.00
k := Color(WHITE)
for _, b := range bl {
if bv := b.Volume(); bv > v {
v = bv
k = b.color
}
}
return k
}
func (bl BoxList) PaintItBlack() {
for i := range bl {
bl[i].SetColor(BLACK)
}
}
func (c Color) String() string {
strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
return strings[c]
}
func main() {
boxes := BoxList {
Box{4, 4, 4, RED},
Box{10, 10, 1, YELLOW},
Box{1, 1, 20, BLACK},
Box{10, 10, 1, BLUE},
Box{10, 30, 1, WHITE},
Box{20, 20, 20, YELLOW},
}
fmt.Printf("We have %d boxes in our set\n", len(boxes))
fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String())
fmt.Println("The biggest one is", boxes.BiggestColor().String())
fmt.Println("Let's paint them all black")
boxes.PaintItBlack()
fmt.Println("The color of the second one is", boxes[1].color.String())
fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String())
}
指針作爲receiver
現在讓我們回過頭來看看SetColor這個method,它的receiver是一個指向Box的指針,是的,你可以使用*Box。想想爲啥要使用指針而不是Box本身呢?
我們定義SetColor的真正目的是想改變這個Box的顏色,如果不傳Box的指針,那麼SetColor接受的其實是Box的一個copy,也就是說method內對於顏色值的修改,其實只作用於Box的copy,而不是真正的Box。所以我們需要傳入指針。
這裏可以把receiver當作method的第一個參數來看,然後結合前面函數講解的傳值和傳引用就不難理解
這裏你也許會問了那SetColor函數裏面應該這樣定義*b.Color=c,而不是b.Color=c,因爲我們需要讀取到指針相應的值。
你是對的,其實Go裏面這兩種方式都是正確的,當你用指針去訪問相應的字段時(雖然指針沒有任何的字段),Go知道你要通過指針去獲取這個值,看到了吧,Go的設計是不是越來越吸引你了。
也許細心的讀者會問這樣的問題,PaintItBlack裏面調用SetColor的時候是不是應該寫成(&bl[i]).SetColor(BLACK),因爲SetColor的receiver是*Box,而不是Box。
你又說對了,這兩種方式都可以,因爲Go知道receiver是指針,他自動幫你轉了。
說明:
如果 一個method 的receiver 是 *T, 你可以在一個T 類型的實例變量V 上調用這個method, 而不需要&V 去調用這個method
類似的
如果一個method 的receiver 是T, 你可以在一個 *T 類型的變量P 上面調用這個method, 而不需要 *p 去調用這個method
你不用擔心你是調用的指針的method還是不是指針的method,Go知道你要做的一切
接口interface
什麼是 interface :
簡單的說, interface 就是一組method 簽名的組合, 我們通過interface 來定義對象的一組行爲。
一組抽象方法的集合
interface 類型
interface 定義了一組方法, 如果某個對象實現了某個接口的所有方法, 則此對象實現了此接口
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段Human
school string
loan float32
}
type Employee struct {
Human //匿名字段Human
company string
money float32
}
//Human對象實現Sayhi方法
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
// Human對象實現Sing方法
func (h *Human) Sing(lyrics string) {
fmt.Println("La la, la la la, la la la la la...", lyrics)
}
//Human對象實現Guzzle方法
func (h *Human) Guzzle(beerStein string) {
fmt.Println("Guzzle Guzzle Guzzle...", beerStein)
}
// 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) //此句可以分成多行
}
//Student實現BorrowMoney方法
func (s *Student) BorrowMoney(amount float32) {
s.loan += amount // (again and again and...)
}
//Employee實現SpendSalary方法
func (e *Employee) SpendSalary(amount float32) {
e.money -= amount // More vodka please!!! Get me through the day!
}
// 定義interface
type Men interface {
SayHi()
Sing(lyrics string)
Guzzle(beerStein string)
}
type YoungChap interface {
SayHi()
Sing(song string)
BorrowMoney(amount float32)
}
type ElderlyGent interface {
SayHi()
Sing(song string)
SpendSalary(amount float32)
}
interface 可以被任意的對象實現
任意的類型都實現了空 interface
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
}
//Human實現SayHi方法
func (h Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Human實現Sing方法
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)
}
// Interface Men被Human,Student和Employee實現
// 因爲這三個類型都實現了這兩個方法
type Men interface {
SayHi()
Sing(lyrics string)
}
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", 37, "222-444-XXX"}, "Things Ltd.", 5000}
//定義Men類型的變量i
var i Men
//i能存儲Student
i = mike
fmt.Println("This is Mike, a Student:")
i.SayHi()
i.Sing("November rain")
//i也能存儲Employee
i = tom
fmt.Println("This is tom, an Employee:")
i.SayHi()
i.Sing("Born to be wild")
//定義了slice Men
fmt.Println("Let's use a slice of Men and see what happens")
x := make([]Men, 3)
//這三個都是不同類型的元素,但是他們實現了interface同一個接口
x[0], x[1], x[2] = paul, sam, mike
for _, value := range x{
value.SayHi()
}
}
通過上面 我們知道:
interface 是一組抽象 方法的集合, 但是踏必須由其他非interface 類型實現, 而不能是 自己實現。
一個函數把interface{} 作爲參數, 那麼他可以接受任意類型的值作爲參數, 如果一個函數返回interface{}, 那麼也就可以返回任意類型的值。
interface 變量存儲的類型
怎麼知道interface 存儲的什麼類型呢
Go 裏面有一個語法, 可以直接判斷是否是該類型的變量: value, ok = element.(T)
這裏value 就是變量的值 , ok 就是一個bool類型, element是interface變量, T 是斷言的類型
如果interface 確實存儲了T 類型的數值, 那麼ok 返回true 否則返回false
package main
import (
"fmt"
"strconv"
)
type Element interface{}
type List [] Element
type Person struct {
name string
age int
}
//定義了String方法,實現了fmt.Stringer
func (p Person) String() string {
return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}
func main() {
list := make(List, 3)
list[0] = 1 // an int
list[1] = "Hello" // a string
list[2] = Person{"Dennis", 70}
for index, element := range list {
if value, ok := element.(int); ok {
fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
} else if value, ok := element.(string); ok {
fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
} else if value, ok := element.(Person); ok {
fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
} else {
fmt.Printf("list[%d] is of a different type\n", index)
}
}
}
另外一種實現方式 上面都是if else 不好維護比較累贅 改用switch
package main
import (
"fmt"
"strconv"
)
type Element interface{}
type List [] Element
type Person struct {
name string
age int
}
//打印
func (p Person) String() string {
return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)"
}
func main() {
list := make(List, 3)
list[0] = 1 //an int
list[1] = "Hello" //a string
list[2] = Person{"Dennis", 70}
for index, element := range list{
switch value := element.(type) {
case int:
fmt.Printf("list[%d] is an int and its value is %d\n", index, value)
case string:
fmt.Printf("list[%d] is a string and its value is %s\n", index, value)
case Person:
fmt.Printf("list[%d] is a Person and its value is %s\n", index, value)
default:
fmt.Println("list[%d] is of a different type", index)
}
}
}
注意:
這裏需要強調的是: `element.(type)` 語法不能在 switch 外的任何邏輯使用。如果要在switch 外面判斷一個類型就是用 if else 裏面的代碼 使用 `comma-ok` 斷言
反射
反射: 能檢查程序 在運行時的狀態
要去反射一個類型的值(這些值都實現了空的interface),首先需要把它轉化成reflect對象(reflect.Type 或者reflect.Value 根據不同情況調用不同的函數)
兩種獲取方式:
t :=reflect.TypeOf(i) // 得到類型的元數據, 通過t 我們能過獲得類型定義裏面所有的元素
v := reflect.ValueOf(i) // 得到實際的值, 通過v 我們獲取存儲在裏面的值, 還可以去改變值
獲取反射值能夠返回 響應的類型和數值
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
修改相應的值必須這樣寫
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)
反射字段必須是可讀寫的意思
這樣寫會報錯的
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)
array 、slice、 map
array
var arr [n]type
聲明方式
a := [3]int{1, 2, 3} // 聲明瞭一個長度爲3的int數組
b := [10]int{1, 2, 3} // 聲明瞭一個長度爲10的int數組,其中前三個元素初始化爲1、2、3,其它默認爲0
c := [...]int{4, 5, 6} // 可以省略長度而採用`...`的方式,Go會自動根據元素個數來計算長度
slice
slice 不是真正 意義上面的動態數組, 而是一個引用類型。 slice 總是指向一個底層array, slice 的聲明也可以像array 一樣 只是不需要長度
// 和聲明array一樣,只是少了長度
var fslice []int
注意:
slice 是引用類型索引當引用 改變其中元素值的時候, 其他所有的引用都會改變其值
slice 像一個結構體 這個結構體包含3個元素:
- 一個指針, 指向數組中slice 指定的開始位置
- 長度 即slice 的長度
- 最大長度, 也就是slice開始位置到數組的最後位置的長度
slice 內置函數
- len
- cap 獲取slice 的 最大容量
- append
- copy 數copy從源slice的src中複製元素到目標dst,並且返回複製的元素的個數
map
numbers = make(map[string]int)
map 需要注意的點:
- map 是無序的
- map 長度不固定
- len() 表示擁有key 的數量
- map 的值方便修改
- map 和其他基本型別不一樣, 它不是thread-safem 在多個go-routine 存取的時候必須使用mutex lock 機制
csharpRating, ok := rating[“C#”] // 第一個是值 第二個是 判斷在不在
delete(ranting , “a1”) 刪除key 爲a1 的元素
map 也是一種引用
如果 兩個map 同時指向一個底層, 那麼一個改變 另一個也相應改變
new make 區別
make 用於內建類型 的內存分配 (map slice channel)
new 用於所有類型 的內存分配
new :
內建函數new : new(T) 分配了零值 填充的T 類型的內存空間, 並且返回其地址, 即一個 *T。 返回了一個指針 指向新的分配內存的類型T 的 零值。 劃重點 了啊: new 返回指針
make
內建函數make(T, args) 和new(T) 有着不同的功能, make 只能創建slice map channel 並且返回 一個有初始值(非0 )的T 類型, 而不是 *T 。 本質上來講, 導致這3個類型不同的原因是指向數據結構的引用在在使用前必須被初始化。
例如 一個slice 是一個包含指向數據(內部array) 的指針, 長度 、容量的三項描述符; 在這些項目被初始化之前, slice 爲nil.
對於slice map channel 來說make 初始化了內部結構, 填充適當的值
make 返回初始化後的非0 值
零值理解
所指的是不是空值 , 而是一種變量未填充之前 的默認值, 通常爲0 此處羅列部分 0值
int 0
int8 0
int32 0
int64 0
uint 0x0
rune 0 //rune的實際類型是 int32
byte 0x0 // byte的實際類型是 uint8
float32 0 //長度爲 4 byte
float64 0 //長度爲 8 byte
bool false
string ""
總結:
new 返回一個指針, 指向新分配的類型T 的零值
make 返回有初始值(非零)的T 類型 而不是 *T
併發go
goroutine
協程 比線程更小, 十幾個線程體現在底層可能就五六個線程, go 語言內部實現了這些goroutine之間內存的共享。執行 goroutine 只需要極少的棧內存(4-5k)可同時運行成千上萬個併發任務。 goroutine 比thread 更易用 更高效 更輕便
goroution 是通過go 的runtime管理的一個線程管理器。 goroutine 通過go 關鍵字實現
go hello(a, b, c)
channel
goroutione 運行在相同的地址空間, 因此訪問共享內存必須做好同步。 go 的同步機制 channel.
可以通過 channel 發送或者接受值。
這些值只能是特定的類型:channel 類型
定義一個channel 時候也需要定義發送到channel 值的類型。
注意 必須使用channel 創建channel:
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
channel 通過 操作符 <- 來接收和發送數據
ch <- v // 發送v 到channel ch
v := <- ch // 從ch 中 接收數據並 賦值給v
看具體的例子:
package main
import "fmt"
func sum(a []int, c chan int) {
total := 0
for _, v := range a {
total += v
}
c <- total
}
func main() {
a := []int{7, 2, 6, 35, 8, -1}
c := make(chan int)
go sum(a[:len(a)/2], c)
go sum(a[len(a)/2:], c)
x, y := <-c, <-c
fmt.Println(x, y, x+y)
}
思考:
在生產環境 goroutine 就是這樣使用的?
默認情況下 channel 接收和發送都是阻塞的,除非另一端已經準備好了, 這樣使 groutines 同步變得簡單, 而不需要顯示的lock,
所謂 阻塞 , 也就是如果讀取 (value := < ch) 它將被阻塞, 直到有數據接受。 其次任何發送 (ch <- 5) 將會被阻塞, 直到有數據被讀出。 無緩衝channel 是在多個goroutine 之間同步很棒的工具。
buffered channels
上面是非緩存類型的channel go 也允許指定channel 的緩衝大小。 就是channel 可以存儲 多少元素
ch :=make(chan bool 4) 創建 可以存儲 4個元素的bool 型channel. 在這個channel 中, 前4個元素可以無阻塞的寫入。 當寫到第5個的時候, 代碼會阻塞 直到 其他goroutine 從channel 中讀取一些元素 騰出空間。
ch := make(chan type value)
當 value = 0 時,channel 是無緩衝阻塞讀寫的,當value > 0 時,channel 有緩衝、是非阻塞的,直到寫滿 value 個元素才阻塞寫入
package main
import "fmt"
func main() {
c := make(chan int, 2)//修改2爲1就報錯,修改2爲3可以正常運行
c <- 1
c <- 2
fmt.Println(<-c)
fmt.Println(<-c)
}
//修改爲1報如下的錯誤:
//fatal error: all goroutines are asleep - deadlock!
思考: 在生產環境如何確定這個值的大小呢
range 和close
上面這個例子讀取了兩次c 不方便 可以使用range 讀取
package main
import "fmt"
func fib(n int, c chan int) {
x, y := 1, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fib(cap(c), c)
//for h,i := range c { // 通道只有一個接受
// fmt.Println(h,i)
//}
for i := range c {
fmt.Println(i)
}
}
for i :=range c 能夠不斷讀取channel 裏面的數據, 直到channel 被顯示關閉。 上面我們顯示的關閉通道, 生產者通過內置函數close 關閉channel 關閉channel 之後就無法再發送任何數據了, 在消費方面可以通過語法
v , ok := <- ch 測試 channel 是否關閉。 如果ok 返回false 那麼說明channel 已經沒有任何數據並且已經被關閉
記住應該在生成者的地方關閉channel , 而不是消費的地方去關閉它這樣容易引起 panic
另外就是 記住channel 不像 文件之類 不需要經常去關閉。 只有當你完全沒有任何數據發送, 或者你想顯示的結束range 循環之類
select
上面都是隻有一個 channel 的情況
如果有多個channel 就得使用關鍵字select
select 用來監聽和channel 有關的IO 操作 。 IO 操作發生時候, 觸發相應的動作
select 監聽 channel 數據的流動
默認select 是阻塞的, 只有當監聽的channel 中有發送或者接受可以進行時纔會運行。當多個channel 都準備好的時候 select 是隨機選擇的一個。
分析代碼
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x + y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
for 在不斷的循環 當監聽到chan c 有數據讀出的時候 她就寫入 當監聽到 quit chan 有寫入的時候 就讀出 看case 後面的 行爲
當把 fibonacci(c, quit) 調用放在 go func() 上面的時候就會報錯 程序會阻塞走不到下面。
在select 還有default 方法 select 類似於switch 功能 。 default 就是當監聽的channel 都沒有準備好的時候 默認執行的(select 不在阻塞 等待 channel )
select {
case i := <-c:
// use i
default:
// 當c阻塞的時候執行這裏
}
注意 :
一般default 不會寫在裏面, select 中的default 子句總是可以 運行的, 因爲會消耗CPU 資源
select { //不停的在這裏檢測
case <-chanl : //檢測有沒有數據可以讀 如果有可以 讀的 就執行這個case 處理語句
case chan2 <- 1 : //檢測有沒有可以寫 如果可以 寫就 執行這個case 處理 句子
default:
//如果以上都沒有符合條件,那麼則進行default處理流程
}
超時
有時候會出現 goroutine 阻塞的情況, 那麼如果我們如何避免程序進入阻塞的情況呢。 可以利用select 來設置超時
package main
import "time"
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <-c:
println(v)
case <-time.After(5 * time.Second):
println("timeout")
o <- true
break
}
}
}()
<-o
}
runtime goroutine
runtime 中處理goroutine 的函數
-
Goexit
退出當前執行的goroutine, 但是defer 函數還會繼續調用 -
Gosched
讓出當前goroutine 的執行權限, 調度器安排其他等待的任務運行, 並在下次某個時候從該位置恢復執行。 -
NumCPU
返回cpu 的核數 -
NumGorutine
返回正在執行的和排隊的任務總數 -
GOMAXPROCS
用來設置可以並行計算的CPU 核數的最大值, 並返回之前的值