1. 程式人生 > 實用技巧 >Go 語言聖經筆記

Go 語言聖經筆記

Go聖經

  • i++是語句,而不是表示式,所以類似j=i++非法,並且++只能在後邊
  • 常量目前只能是數值、字串或者一個固定的布林值

變數

基本型別:數值、字串、布林值

引用型別:指標、介面、切片、map、函式、chan

聚合型別:陣列、結構體

  • Go中宣告變數不初始化的情況下都會預設零值,不會出現undefined的情況

引用型別的零值為nil

基本型別為其對應的零值

聚合型別的元素或欄位為其對應的零值

  • 短變數宣告對已經宣告過的變數只有賦值作用,但是多次變數宣告必須要有一個是新的變數
res,err:=test()
res1,err:=test()//ok
res:=test()
res:=test()//error
  • 不是每個值都有記憶體地址,但是每個變數一定有記憶體地址,換句話說變數是可定址的(也不是一定,reflect.Value型別變數事不可取地址的)
  • p:=new(T)宣告一個T型別的零值變數,返回其地址,每次返回都是新的變數地址,但是可能會相同,如果兩個型別為空時,即型別的大小為0,如struct{}[0]int,返回地址可能是同一個,並且當大小為0時,垃圾回收器有不同行為,應該避免這種用法。

整形

  • Go中取模結果符號與被取模運算子的符號是一致的(同Javascript),除法結果依賴於運算元是否為整數,只要有一個為浮點數那麼就是正常除法
  • 列印資料時%[1]為繼續取第一個操作符,如fmt.Println("%d %[1]c %[1]q\n",ascii)

字串

字串的第ige索引值代表的是第i個位元組,第i個位元組不一定是第i個字元,因為UTF-8是一種變長的編碼方式,對於非ASCII編碼可能會使用兩個或者更多位元組進行編碼,可以使用[]rune(string)將字串轉化為unicode碼點後進行索引,一個unicode碼點為4個位元組

func main() {
s := "國hello, world"
fmt.Println(string(s[0]), string([]rune(s)[0])) // å 國
}


常量

golang也可以進行類似列舉的操作(只能是常量),使用iota,從0開始遞增

var (
A int = 1 << iota
B
C
D
)


func main() {
// var a Vex
fmt.Println(A, B, C)
}


golang許多常量可能並沒有一個明確的基礎型別,在賦值時會進行嘗試轉換

import "fmt"


func main() {
var a float64 = 3 + 0i
fmt.Printf("型別:%T,值:%[1]v\n", a) // 型別:float64,值:3
a = 20
fmt.Printf("型別:%T,值:%[1]v\n", a) // 型別:float64,值:20
a = 'a'
fmt.Printf("型別:%T,值:%[1]v\n", a) // 型別:float64,值:97
}

複合資料型別

陣列

  • 陣列可以使用索引的形式進行宣告:[...]int{99:1}宣告長度為100,最後一個元素值為1的陣列。
  • 陣列區別與JavaScript不是引用型別,陣列的比較相等判斷對應元素是否相等
  • append像切片新增內容時,如果底層陣列容量足夠,那麼直接是在底層陣列上新增,然後返回切片值,如果容量我夠則擴容生成一個新的底層陣列,所以在使用append向切片新增內容時我們不確定返回的是之前的底層陣列的切片值還是一個新的底層陣列
func test(arr []int) {
arr = append(arr, 3)
arr[0] = 3
fmt.Println(len(arr), cap(arr)) // 3 4
}
func main() {
arr := make([]int, 2, 4)
test(arr)
fmt.Println(arr[:3])            //[3 0 3] 共享同一底層陣列,擴容後可以看見新增的值
fmt.Println(len(arr), cap(arr)) // 2 4
}

所以平常可以這麼使用,避免一些不必要的麻煩(覆蓋原始資料之類的)

func test(arr []int) []int {
arr = append(arr, 3)
arr[0] = 3
return arr
}


func main() {
arr := make([]int, 2, 4)
arr = test(arr)
fmt.Println(arr) // 2 4
}

map

  • map的值是不可取地址的,原因在於map隨著元素增加會分配更大的記憶體空間,之前分配的空間可能失效

函式

可變引數

golang支援可變引數,型別必須匹配

func test(v ...int) {
for _, value := range v {
fmt.Println(value)
}
}
func main() {
test(12, 3, 3)
arr := []int{5, 6, 7}
test(arr...)
}

匿名函式

golang中也要注意迴圈閉包帶來的引用同一變數的問題,類似JavaScript

Recover

golang異常可以使用recover()恢復,但是不要濫用,在該恢復的地方進行恢復

func main() {
defer func() {
if p := recover(); p != nil {
fmt.Println("呼叫recover可以恢復異常,注意應該只恢復應該恢復的panic")
}
}()
log.Panic("異常")
log.Println("啊啊啊啊啊")
}

介面

  • 空介面型別不能對值進行操作,因為他沒有實現任意方法
  • 只有當兩個或兩個以上具體型別需要使用同樣的方法處理時才應該使用介面

包和工具

包的重新命名

import (
"crypto/rand"
"fmt"
mrand "math/rand"
)
  • golang中出現迴圈依賴會報錯

測試

建立test目錄,為file包建立file_test.go檔案,test檔案測試函式必須以Test開始。呼叫go test -v進行測試。demo:

func TestSum(t *testing.T) {
if Sum(2, 3) != 5 {
t.Error("錯誤:2+3!=5")
}
if Sum(4, 6) == 8 {
t.Error("錯誤:4+6=8")
}
}


go test ./...可以進行遞迴測試
測試覆蓋率檢視

  1. go test -v -coverprofile=cover.out
  2. go tool cover -html=c.out

第二部是轉化為html檢視

反射

反射可以幫助我們在執行時更新變數或者檢查他們的值,呼叫方法和內在操作等,interface{}需要在執行時確定型別,但是如果不是確定其動態型別並且呼叫斷言的話,是不能訪問其內在的值和方法的,但是反射提供了這種機制。

package main


import (
"fmt"
"reflect"
)


func test(data interface{}) {
t := reflect.TypeOf(data) // 返回reflect.Type
fmt.Printf("%q\n", t)     // 列印時會呼叫String方法


v := reflect.ValueOf(data) // 返回reflect.Value型別
fmt.Printf("%q\n", v)
fmt.Println("---------", v.Kind() == reflect.String) // 判斷型別,不能與string比較,因為其是無型別string


t = v.Type() // 呼叫Type 方法返回對應具體型別的reflect.Type
fmt.Printf("%q\n", t)


//  reflect.ValueOf(data)的逆操作,返回interface{}型別的具體值
// 與reflect.Value不同的是空介面隱藏了值的內部表現和方法(什麼都
// 做不了),只有知道具體的動態型別時才能使用斷言訪問內部值。相反
// reflect。Value有很多方法檢查其內容
i := v.Interface()
fmt.Printf("%q\n", i.(string))
}


func main() {
test("字串")
}


通過反射修改值

package main


import (
"fmt"
"reflect"
)


func main() {
x := 3
a := reflect.ValueOf(&x).Elem() // 獲取可取地址的Value,reflect.ValueOf()返回的是值的拷貝
fmt.Println(a.CanAddr())        // 可以使用該方法判斷是否可取地址
/*
Addr() 返回reflect.Value 儲存指向變數的指標
隨後取Interface + 斷言即可通過普通指標更新變數
*/
// a.Addr().Interface().(*int)


// 或者呼叫可取地址的reflect.Value的Set方法更新值
a.Set(reflect.ValueOf(4))
// 簡便寫法
a.SetInt(6)


y := struct{ field int }{10}
// 可以獲取未匯出欄位,但是不能更改
field := reflect.ValueOf(&y).Elem().FieldByName("field")
// 判斷是否可更改
fmt.Println(fd.CanAddr(), fd.CanSet()) // "true false"


}


獲取結構體欄位標記

type resp struct {
Code int    `http:"responseNo"`
Msg  string `http:"responseMsg"`
}




func main() {
res := resp{10000, "成功"}


v := reflect.ValueOf(&res).Elem()


fmt.Printf("欄位有:%d個\n", v.NumField())          // 獲取欄位數
fmt.Println(v.Type().Field(1).Tag.Get("http")) // responseMsg
}

呼叫方法

reflect.Typereflect.Value 都實現了reflect.Mehtod,通過Call呼叫方法,傳入引數為[]reflect.Value型別,返回值同樣為[]reflect.Value型別

// 必須要是匯出方法才能獲取
func (r resp) Fn() int {
return 123
}
func (r resp) fn() int {
return 123
}
func main() {
res := resp{10000, "成功"}
v := reflect.ValueOf(res)             // 值型別的reflect.Value 則獲取方法的時候獲取接受者為值的方法
fmt.Printf("有%d個方法\n", v.NumMethod()) // 有1個方法


// reflect.Type 和reflect.Value 均實現了reflect.Method
t := v.Type()
fmt.Printf("有%d個方法\n", t.NumMethod()) // 有一方犯法


// Call 呼叫方法,呼叫方法傳入的引數必須為[]reflect.Value 返回也是[]reflect.Value
// 呼叫Interface 返回interface{},值是具體值
fmt.Println(v.Method(0).Call(nil)[0].Interface().(int))
}

goroutines和channels

  • 帶快取的通道與不帶快取的區別在於,帶快取的通道維護一個佇列,好比蛋糕店做蛋糕的兩個師傅,如果不帶快取其中一個做完必須等待另外一個師傅進行交接,此時是阻塞狀態。帶快取通道好比有個專門存放蛋糕的地方,其中一個師傅做完後可以放進存放的地方,繼續做下一個工作

基於共享變數的併發

  • 當事件x與事件y不知道是事件x先於事件y呼叫還是後呼叫時,那麼可以說事件x與事件y是併發的

goroutine與執行緒的區別

  1. 具有動態棧
    執行緒執行時會分配一個大約2MB左右(一般語言中)的記憶體棧用於儲存執行緒執行期間,函式內部的變數等,但是如果每個goroutine都分配這麼大的記憶體的話有些浪費,goroutine一般分配記憶體大小為2kb左右,並且可以動態改變,最大可達1GB
  2. 排程開銷小
    執行緒的排程會通過核心函式完成,golang具有自己排程器,使用內部實現的結構進行排程
  3. 沒有特定標識(遮蔽了)
    避免濫用