Go語言學習筆記

Go 編程查看標準包函數方法: ctrl + . + h 或者: ctrl + . + g

基礎知識

  1. 運行方式()
    Golang提供了go run“解釋”執行和go build編譯執行兩種運行方式,所謂的“解釋”執行其實也是編譯出了可執行文件後才執行的。

  2. Package管理()
    Golang約定:我們可以用./或../相對路徑來引自己的package;如果不是相對路徑,那麼go會去$GOPATH/src下查找。

  3. 格式化輸出()
    類似C、Java等語言,Golang的fmt包提供了格式化輸出功能,而且像%d、%s等佔位符和\t、\r、\n轉義也幾乎完全一致。但Golang的Println不支持格式化,只有Printf支持,所以我們經常會在後面加入\n換行。此外,Golang加入了%T打印值的類型,%v打印數組等集合的所有元素。

  4. Go語言基本類型

    • 值類型(基礎類型)
      • 0.rune // int32 的別名
        1.bool,一個字節,值是true或者false,不可以用0或者1表示(java中boolean佔用4個字節,而boolean作爲數組出現時,每個boolean佔用1個字節)
        2.int/uint(帶符號爲與不帶符號位的int類型):根據平臺不同是32位或者64位
        3.intx/uintx:x代表任意位數,例如:int3,代表佔3bit的int類型
        4.byte佔用8位,一個字節,相當於uint8,不帶符號位
        5.floatx:由於沒有double類型,所以float64就是double。float32小數精確到7位,float64小數精確到15位。
        6.complex64/complex128:複數類型
        7.uintptr:保存指針用的類型,也是隨着平臺改變而改變,因爲指針的長度就是隨平臺而變。
        8.其他類型值:array,struct,string
    • 引用類型
      • slice,map,chan
    • 接口類型
      • interface
    • 函數類型
      • func
  5. 變量和常量()
    1.雖然Golang是靜態類型語言,卻用類似JavaScript中的var關鍵字聲明變量。而且像同樣是靜態語言的Scala一樣,支持類型自動推斷。有一點很重要的不同是:如果明確指明變量類型的話,類型要放在變量名後面。這有點彆扭吧?!後面會看到函數的入參和返回值的類型也要這樣聲明。
    2.短變量聲明
    在函數中,簡潔賦值語句 := 可在類型明確的地方代替 var 聲明。
    注意: 函數外的每個語句都必須以關鍵字開始( var 、 func 等等), 因此 := 結構不能在函數外使用。

  6. 類型轉換
    表達式 T(v) 將值 v 轉換爲類型 T.
var i int = 42 
var f float64 = float64(i)
var u uint = uint(f) 
//或者這樣寫 
i := 42 
f := float64(i) 
u := uint(f)


7. 常量
常量的聲明與變量類似,只不過是使用 const 關鍵字。
常量可以是字符、字符串、布爾值或數值。
常量不能用 := 語法聲明。

const ( f = 12i ) 
func main(){
 const ( a = 2 b = 3.12 c = true d = "sssss" ) 
 fmt.Println(a, b, c, d, f) 
}


8. 控制語句:
作爲最基本的語法要素,Golang的各種控制語句也是特點鮮明。在對C繼承發揚的同時,也有自己的想法融入其中;
* if/switch/for 的條件部分都沒有圓括號(可以寫),但必須有花括號。
* switch的case中不需要break(默認break);
* 如果case語句後,想繼續下一個case語句執行,需加入fallthrogh
* 沒有條件的 switch 同 switch true 一樣。
* switch的case條件可以是多個值。
* Golang中沒有while。

9. 分號和花括號()
注意: “分號和花括號 “
分號由詞法分析器在掃描源代碼過程自動插入的,分析器使用簡單的規則:如果在一個新行前方的最後一個標記是一個標識符(包括像int和float64這樣的單詞)、一個基本的如數值這樣的文字、或break continue fallthrough return ++ – ) }中的一個時,它就會自動插入分號。 分號的自動插入規則產生了“蝴蝶效應”:所有控制結構的左花括號不都能放在下一行。因爲按照上面的規則,這樣做會導致分析器在左花括號的前方插入一個分號,從而引起難以預料的結果。所以Golang中是不能隨便換行的。

10. 函數;
* 1. func關鍵字。
* 2. 最大的不同就是“倒序”的類型聲明。
* 3. 不需要函數原型,引用的函數可以後定義。這一點很好,不類似C語言裏要麼將“最底層抽象”的函數放在最前面定義,要麼寫一堆函數原型聲明在最前面。
* 4. 函數的定義:
func 關鍵字 函數名(參數1..)(返回值1, 返回值2 ){
函數體
}

如:
func add(a int, b int)(ret int, err int){ return a+b, b }
* 5. 函數也是值,它們可以像其它值一樣傳遞。函數值可以用作函數的參數或返回值。

func main() {
     toSqrt := func(x, y float64)float64 { return math.Sqrt(x*x + y*y) } 
     fmt.Println(toSqrt(12, 5)) 
     fmt.Println(autoSqrt(toSqrt)) 
     fmt.Println(autoSqrt(math.Pow)) 
 } 

 func autoSqrt(fn func(x, y float64) float64) float64 {
     return fn(4, 3)
 }


* 6. 函數的閉包
Go 函數可以是一個閉包。閉包是一個函數值,它引用了其函數體之外的變量。 該函數可以訪問並賦予其引用的變量的值,換句話說,該函數被“綁定”在了這些變量上。

11. 集合()基本數據結構 slice, struct
* 1. Golang提供了數組和Map作爲基本數據結構:
* 2. 數組中的元素會自動初始化,例如int數組元素初始化爲0
* 3. 切片(借鑑Python)的區間跟主流語言一樣,都是 “左閉右開”, 用 range()遍歷數組和Map
例如:

func test02() {
     source := []int{1, 2, 3, 4, 5, 6}
     var sliceTmp []int = source[2:5] //注意 [2:n] 它爲左閉右開, 例子即使: 從 下標2 開始至 下標4 
     fmt.Printf("%v\n", sliceTmp)
}


* 4. 切片就像數組的引用
切片並不存儲任何數據, 它只是描述了底層數組中的一段。
更改切片的元素會修改其底層數組中對應的元素。
與它共享底層數組的切片都會觀測到這些修改。
例如

func main() { 
    source := [6]int{1, 2, 3, 5, 4} 
    var s []int = source[2:6] 
    fmt.Println(s) 
    source[5] = 7 
    fmt.Println(s) 
    s[0] = 88 fmt.Println(source) 
} 
輸出: [3 5 4 0] [3 5 4 7] [1 2 88 5 4 7]


* 5. 切片的初始化
變量名 := []類型{…}
例如
a := []int{1, 2, 3 }
s := []struct{ age int name string }{ {1, "xiaoming"}, {21. "xiaohua"}, {23, "nhao"}, } //注意, 最後一個逗號不能省
* 6. 切片的長度與容量
切片擁有 長度 和 容量 。
切片的長度就是它所包含的元素個數。
切片的容量是從它的第一個元素開始數,到其底層數組元素末尾的個數。
切片 s 的長度和容量可通過表達式 len(s) 和 cap(s) 來獲取。

func main() { 
    // a 是切片 
    a := []int{12, 5, 3, 6, 8, 6} 
    // 讓切片的長度爲 0 
    a = a[:0] 
    printSlice(a) 
    // 擴充切片的長度 
    a = a[:3] 
    printSlice(a) 
    // 丟掉開始的兩個元素 
    a = a[2:] 
    printSlice(a) 
} 
func printSlice(s []int) { 
    fmt.Printf("len = %d, cap = %d, value = %v\n", len(s), cap(s), s) 
} 
輸出: len = 0, cap = 6, value = [] 
    len = 3, cap = 6, value = [12 5 3] 
    len = 1, cap = 4, value = [3]


* 7. nil 切片
切片的零值是 nil 。
nil 切片的長度和容量爲 0 且沒有底層數組。
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
}
輸出: [] 0 0 nil!
* 8. 用 make 創建切片
切片可以用內建函數 make 來創建,這也是你創建動態數組的方式。
make 函數會分配一個元素爲零值的數組並返回一個引用了它的切片.

func main() {
    a := make([]int, 6)
    printSlice("a", a)
    b := make([]int, 0, 5)
    printSlice("b", b)
    c := make([]int, 3, 5)
    printSlice("c", c)
    d := b[:2]
    printSlice("d", d)
    e := d[2:5]
    printSlice("e", e)
} 
func printSlice(flag string, s []int) {
    fmt.Printf("%s, len = %d, cap = %d, value = %v\n", flag, len(s), cap(s), s)
} 
輸出: a, len = 6, cap = 6, value = [0 0 0 0 0 0] 
    b, len = 0, cap = 5, value = [] 
    c, len = 3, cap = 5, value = [0 0 0]
    d, len = 2, cap = 5, value = [0 0]
    e, len = 3, cap = 3, value = [0 0 0]


* 9. Go的數組 與 C語言數組的區別
Go的數組是值語義。一個數組變量表示整個數組,它不是指向第一個元素的指針(不像 C 語言的數組)。
當一個數組變量被賦值或者被傳遞的時候,實際上會複製整個數組。
(爲了避免複製數組,你可以傳遞一個指向數組的指針,但是數組指針並不是數組。)
可以將數組看作一個特殊的struct,結構的字段名對應數組的索引,同時成員的數目固定.

12. map結構的使用
例如:

type People struct { 
    age int
    name string
} 
var m map[string]People 
func test04() {
     m = make(map[string]People)
     fmt.Println(m)
     m["Afra55"] = People{ 22, "Victor", }
     m["xiaohuo"] = People{ 24, "nihao", } //注意:這種寫法,最後一個','一定不能少,否則爲語法錯誤 
     fmt.Println(m)
     fmt.Println(m["Afra55"])
} 
輸出: map[] 
    map[Afra55:{22 Victor} xiaohuo:{24 nihao}]
    {22 Victor}


* 修改 map 映射
在映射 m 中插入或修改元素: m[key] = elem
獲取元素: elem = m[key]
刪除元素: delete(m, key)
通過雙賦值檢測某個鍵是否存在:elem, ok = m[key]
若 key 在 m 中, ok 爲 true ;否則, ok 爲 false
若 key 不在映射中,那麼 elem 是該映射元素類型的零值
同樣的,當從 映射 中讀取某個不存在的鍵時,結果是 映射 的元素類型的零值

13. 指針和內存分配()
Golang中可以使用指針, 並提供了兩種內存分配機制:
* new:分配長度爲0的空白內存,返回類型T*。new 返回的是一個函數指針。
* make:僅用於 切片、map、chan消息管道,返回類型T而不是指針.

14. 面向對象編程
Golang的結構體跟C有幾點不同:
 * 1. 結構體可以有方法,其實也就相當於OOP中的類了。
 * 2. 支持帶名稱的初始化。
 * 3. 用指針訪問結構中的屬性也用”.”而不是”->”,指針就像Java中的引用一樣。
* 4. 沒有public,protected,private等訪問權限控制。C也沒有protected,C中默認是public的,private需要加static關鍵字限定。Golang中方法名大寫就是public的,小寫就是private的。
 * 5. 同時,Golang支持接口和多態,而且接口有別於Java中繼承和實現的方式,而是採取了類似Ruby中更爲新潮的DuckType。只要struct與interface有相同的方法,就認爲struct實現了這個接口。就好比只要能像鴨子那樣叫,我們就認爲它是一隻鴨子一樣。
* 6. Go 沒有類。然而,可以在結構體類型上定義方法。
* 7. 可以對包中的任意類型(以type定義的類)定義任意方法,而不僅僅是針對結構體。但是,不能對來自其他包的類型或基礎類型定義方法。
* 8. 有時候我們需要將接受方法的對象定義爲指針,這樣可以有兩個效果:
1) 可以提高參數傳遞的效率,不用拷貝。
2) 修改接收者指向的值。

15. 異常處理
* 1. Golang中異常的使用比較簡單,可以用errors.New創建錯誤返回值,也可以實現Error接口的方法來自定義異常類型,同時利用函數的多返回值特性可以返回異常類。
* 2. 比較複雜的是defer和recover關鍵字的使用。Golang沒有采取try-catch“包住”可能出錯代碼的這種方式,而是用 延遲處理的方式.
* 3. 用defer調用的函數會以後進先出(LIFO)的方式,在當前函數結束後依次順行執行。defer的這一特點正好可以用來處理panic。當panic被調用時,它將立即停止當前函數的執行並開始逐級解開函數堆棧,同時運行所有被defer的函數。如果這種解開達到堆棧的頂端,程序就死亡了。
* 4. 但是,也可以使用內建的recover函數來重新獲得Go程的控制權並恢復正常的執行。由於僅在解開期間運行的代碼處在被defer的函數之內,recover僅在被延期的函數內部纔是有用的。
* 5. defer語句會將函數推遲到外層函數返回之後執行。
推遲調用的函數其參數會立即求值,但直到外層函數返回前該函數都不會被調用。

func main() { 
    defer sqrt(9) 
    fmt.Println("9 * 9: ")
} 
func sqrt(x float64) {
     fmt.Println(x * x)
} 
 打印的值: 9 * 9: 81


16. goroutine(協程)
* 1. goroutine使用Go關鍵字來調用函數,也可以使用匿名函數。可以簡單的把go關鍵字調用的函數想像成pthread_create。也就是說goroutine阻塞時,Golang會切換到其他goroutine執行,這是非常好的特性!Java對類似
goroutine這種的協程沒有原生支持,像Akka最害怕的就是阻塞。
* 2. 因爲協程不等同於線程,操作系統不會幫我們完成“現場”保存和恢復,所以要實現goroutine這種特性,就要模擬操作系統的行爲,保存方法或函數在協程“上下文切換”時的Context,當阻塞結束時才能正確地切換回來。
* 3. 像Kilim等協程庫利用字節碼生成,能夠勝任,而Akka完全是運行時的。
* 4. 注意:”如果你要真正的併發,需要調runtime.GOMAXPROCS(CPU_NUM)設置;
* 5. 自己的觀察: Go程類似分割的線程,執行完後, 從自己的函數中就直接退出, 不會回到主進程空間,同時不需要回收資源”

17. 原子操作
像Java一樣,Golang支持很多CAS操作。運行結果是unsaftCnt可能小於200,因爲unsafeCnt++在機器指令層面上不是一條指令,而可能是從內存加載數據到寄存器、執行自增運算、保存寄存器中計算結果到內存這三部分,所以不進行保護的話有些更新是會丟失的。

18. Channel管道()
* 1. 通過前面可以看到,儘管goroutine很方便很高效,但如果濫用的話很可能會導致併發安全問題。而Channel就是用來解決這個問題的,它是goroutine之間通信的橋樑,類似Actor模型中每個Actor的mailbox。多個goroutine要修改一個狀態時,可以將請求都發送到一個Channel裏,然後由一個goroutine負責順序地修改狀態。
* 2. Channel默認是阻塞的,也就是說select時如果沒有事件,那麼當前goroutine會發生讀阻塞。同理,Channel是有大小的,當Channel滿了時,
發送方會發生寫阻塞。Channel這種阻塞的特性加上goroutine可以很容易就能實現生產者-消費者模式。
* 3. 用case可以給Channel設置阻塞的超時時間,避免一直阻塞。而default則使select進入無阻塞模式。
* 5. 有緩存管道與無緩存管道的區別:
* 對於無緩衝的channel,放入操作和取出操作不能再同一個routine中,而且應該是先確保有某個routine對它執行取出操作,然後才能在另一個routine中執行放入操作
* 在使用帶緩衝的channel時一定要注意放入與取出的速率問題
* 使用channel控制goroutine數量

19. 緩衝流()
* 1. Golang的bufio包提供了方便的緩衝流操作,通過strings或網絡IO得到流後,用bufio.NewReader/Writer()包裝:
* 2. 緩衝區:Peek()或Read時,數據會從底層進入到緩衝區。緩衝區默認大小爲4096字節。
* 3. 切片和拷貝:Peek()和ReadSlice()得到的都是切片(緩衝區數據的引用)而不是拷貝,所以更加節約空間。但是當緩衝區數據變化時,切片也會隨之變化。而ReadBytes/String()得到的都是數據的拷貝,可以放心使用。
* 4. Unicode支持:ReadRune()可以直接讀取Unicode字符。有意思的是Golang中Unicode字符也要用單引號,這點與Java不同。
* 5. 分隔符:ReadSlice/Bytes/String()得到的包含分隔符,bufio不會自動去掉。
* 6. Writer:對應地,Writer提供了WriteBytes/String/Rune。
* 7. undo方法:可以將讀出的字節再放回到緩衝區,就像什麼都沒發生一樣。

20. 數則和切片的異同:
* 聲明數組和切片時的區別:
* 聲明數組時, 方括號內包含了數組的長度或者 以動態計算數組的長度; var Myarray = [3]int{1, 2, 3}
* 聲明切片時, 方括號內沒有任何字符; var Myslice []int
* 切片的信息:
* 切片的長度: 爲 兩個序列號的減值;
* 切片的容量: 如果該切片直接基於數組創建, 它的容量爲 起始位置至數組最後一位的元素個數; 即相減
* 如果該切片基於切片創建, 它的容量爲 起始位置至母切片的容量的元素個數; 即相減

func main(){
    var Myarray = [10]int{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    aslice := Myarray[2:7]
    fmt.Println("aslice lenth : ", len(aslice), " cap : ", cap(aslice))   //lenth : 5, cap : 8
    bslice := Myarray[2:4]
    fmt.Println("bslice lenth : ", len(bslice), " cap : ", cap(bslice))   //lenth : 2, cap : 6
}


* 基於數組創建的切片, 切片的修改在不變化本身容量的情況下會影響到底層數組; 如果本身容量產生變化, 語言底層將會另外分配一塊內存去存儲該切片, 此時就不會影響到該數組了;

func main(){
    var Myarray = [10]int{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    aslice := Myarray[2:7]
    aslice[0] = 99   //切片的容量爲變化
    fmt.Println(aslice, Myarray )  //[99 4 5 6 7] [1 2 99 4 5 6 7 8 9 10]

    aslice = append(aslice, 3, 3, 3, 3, 3, 3, 3, 3) //切片的容量增大, 底層重新分配了一塊內存;
    fmt.Println(aslice, Myarray ) //[3 4 5 6 7 3 3 3 3 3 3 3 3] [1 2 3 4 5 6 7 8 9 10]

}


21. make 與 new 的區別:
* make用於內建類型(slice, map, channel)的內存分配.
* new用於各種類型的內存分配;
* new(T) 分配了零值填充的T 類型的內存空間, 並且返回其地址(指針), *T 類型的值; new返回的是指針
* make(T, args), make 只能創建slice, map, channel, 並且返回一個有初始值(Go語言中, 每種類型的初始值並不相同)的的T類型, 注意: 非*T 類型. 本質上講: 導致這三個類型有所不同的原因是: 指向數據結構的引用在使用前必須被初始化.
*


Go 語言開發中的坑

  1. slice(切片) 的坑:
    因爲:當原始切片的容量不夠,增加後,新的切片指向了一個新的地址,開發者此時極容易使用 slice 特性,導致返回的結果不是所期望的。
 //1.錯誤寫法
 func test(s []int){     
    s.append(s, 3);      //此時slice將與傳入的slice指向不同內存地址,所以想得到理想的結果,就需要將新的slice地址傳出。
}
func main(){
    s := make([]int, 0)     //創建一個容量爲 0 的slice;
    fmt.Println(s)
    test(s)                 //對這個創建的slice進行擴容
    fmt.Println(s)
}
打印結果爲: [0] [0]

//2.正確寫法
 func test(s []int) []int {
     s.append(s, 3)
     return s
 }
 func main(){
      s := make([]int, 0)
      fmt.Println(s)
      s = test(s)
      fmt.Println(s)
  }
 打印結果爲: [0] [3]

所以如果在操作slice時, 可能會使容量增大, 此時就一定要把新的slice返回出來。

2. time時間的坑
* 因爲:Go語言的設計時,提供了一組常量layout, 來格式化它的時間輸出。但是,但是:要麼使用layout中提供的常量,要麼直接拷貝它的常量字符串,千萬不要對它的字符串進行修改,否則或造成輸出時間不正確。

3. for range 與閉包函數的坑

//1.錯誤寫法
func closures() {
    s := make([]int, 3)
    for i := 0; i < 3; i++ {
        s[i] = i + 1
    }
    for _, v := range s {   //輪詢切片s,將取出的值,從地址中取出該值進行打印 ,因爲range主線程先運行完,等打印時所有的v都已變爲最後一個元素的地址,所以打印全是 3
        go func() {
            fmt.Println(v)
        }()
    }
}
打印結果爲: 3 3 3

//2.正確的寫法
func closures() {
    s := make([]int, 3)
    for i := 0; i < 3; i++ {
        s[i] = i + 1
    }
    for _, v := range s {   //輪詢切片s,將取出的值以值傳遞的方式傳入閉包函數中;
        go func(v int) {
            fmt.Println(v)
        }(v)
    }
}
打印結果爲: 1 2 3

Go 博客

  1. golang編程經驗總結 http://studygolang.com/articles/2331

Go書籍

  1. Go web編程;
  2. Go語言編程
發佈了76 篇原創文章 · 獲贊 23 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章