Go語言(golang)常見問題總結(二)

1.main包的唯一性

傳統語言中對主入口的要求是main函數,如c++/java等,只需要保證這點即可,但是在go中還需要保證main包的唯一性。

如下,在一個main包定義如下函數

package main

import "fmt"

func func1(){
    fmt.Println("test func1")
}

然後在另一個main包的main函數中如下調用

package main

import "fmt"

func main(){
    //嘗試1-嘗試調用同目錄下另一個main包中的函數
    //func1()
}

報錯如下:

common_point\test_main1.go:63:2: undefined: func1

可以看到,兩個main包實際上是相互不可見的,對於自己來說都是唯一的。需要提下的是,實際項目中,可以同一目錄下包含多個main包,只要不相互調用,go build/run xxx指明對應main包文件編譯運行即可,這樣處理的目的在於使項目結構都清晰,同時兼容一個項目存在多個入口的情況。

2.如何跳出for select 循環

通常在for循環中,使用break可以跳出循環,但是注意在go語言中,for select配合時,break並不能跳出循環。

如下代碼:

func testSelectFor(chExit chan bool){
    for  {
        select {
        case v, ok := <-chExit:
            if !ok {
                fmt.Println("close channel 1", v)
                break
            }

            fmt.Println("ch1 val =", v)
        }

    }

    fmt.Println("exit testSelectFor")
}

如下調用:

//嘗試2 select for 跳出循環
c := make(chan bool)
go testSelectFor(c)

c <- true
c <- false
close(c)

time.Sleep(time.Duration(2) * time.Second)

運行結果如下,可以看到break無法跳出循環:

...
close channel 1 false
close channel 1 false
close channel 1 false
close channel 1 false
...

爲了解決這個問題,需要設置標籤,break 標籤或goto 便籤即可跳出循環,如下兩種方法均可。

func testSelectFor2(chExit chan bool){
    EXIT:
    for  {
        select {
        case v, ok := <-chExit:
            if !ok {
                fmt.Println("close channel 2", v)
                break EXIT//goto EXIT2
            }

            fmt.Println("ch2 val =", v)
        }
    }

    //EXIT2:
    fmt.Println("exit testSelectFor2")
}

同樣調用,輸出結果如下:

ch2 val = true
ch2 val = false
close channel 2 false
exit testSelectFor2

3.如何在切片中查找

go中使用sort.searchXXX方法在排序好的切片中查找指定的方法,但是其返回結果很奇怪,返回是對應的查找元素不存在時待插入的位置下標(元素插入在返回下標前)。如下調用:

    //嘗試3 search查找返回值
    s := []string{"ab", "ac", "ac", "bb", "bb", "ee"}
    fmt.Println("s=", s)

    fmt.Println(sort.SearchStrings(s, "aa"))
    fmt.Println(sort.SearchStrings(s, "ac"))
    fmt.Println(sort.SearchStrings(s, "ad"))
    fmt.Println(sort.SearchStrings(s, "ff"))

返回結果如下:

s= [ab ac ac bb bb ee]
0
1
3
6

可以看到,單獨根據返回值沒法判斷對應元素是否存在,封裝如下函數:

func IsExist(s []string, t string) (int, bool) {
    iIndex := sort.SearchStrings(s, t)
    bExist := iIndex!=len(s) && s[iIndex]==t

    return iIndex, bExist
}

這裏用返回的下標取值再和原來值對比下,即可判斷對應元素是否存在。

fmt.Println(IsExist(s, "aa"))
    fmt.Println(IsExist(s, "ac"))
    fmt.Println(IsExist(s, "ad"))
    fmt.Println(IsExist(s, "ff"))
    
    /*out
    0 false
    1 true
    3 false
    6 false*/

4.如何初始化帶嵌套結構的結構體

go的哲學是組合優於繼承,使用struct嵌套即可完成組合,內嵌的結構體屬性就像外層結構的屬性即可,可以直接調用;但是注意初始化外層結構體時必須指定內嵌結構體名稱的結構體初始化,如下看到s1方式報錯,s2方式正確。

type stPeople struct {
    Gender bool
    Name string
}

type stStudent struct {
    stPeople
    Class int
}

//嘗試4 嵌套結構的初始化表達式
//var s1 = stStudent{false, "JimWen", 3}
var s2 = stStudent{stPeople{false, "JimWen"}, 3}
fmt.Println(s2.Gender, s2.Name, s2.Class)

5.切片和數組

go中沒有特別複雜的數據結構,核心就是數組(array)和map,切片(slice)是基於數組的。很多人容易混淆array和slice的關係,這裏着重說下兩者的聯繫和區別以及應用場合。

先看二者的初始化方法,如下:

    //slice和數組初始化方法
    var a0 [5]int
    a1 := [5]int{}
    a2 := [5]int{1,2,3}
    a3 := [...]int{1,2,3}
    a4 := [5]int{1,2,3,4,5}
    fmt.Println(a0, a1, a2, a3, a4)

    var s0 []int
    s1 := a4[0:3]
    s2 := []int{1,2,3}
    s3 := make([]int, 2, 3)
    fmt.Println(s0==nil, s1, s2, s3)

輸出如下

[0 0 0 0 0] [0 0 0 0 0] [1 2 3 0 0] [1 2 3] [1 2 3 4 5]
true [1 2 3] [1 2 3] [0 0]

可以看到,array可以如下初始化:

1.單獨聲明長度,會自動填充0值,如 var a0 [5]int

2.也可以初始化時賦值,末尾沒有填滿的填充0值,如a2 := [5]int{1,2,3}

3.如果不指定長度還可以自動計算,如a3 := [...]int{1,2,3}

而slice不關聯數組是即爲nil(如var s0 []int),稱爲nil切片。slice要想不爲空,必須和數組關聯,有如下幾種方法:

1.引用已有數組,如s1 := a4[0:3]

2.初始化表達式賦值,會默認生成底層數組,然後引用它,如s2 := []int{1,2,3}

3.使用make生成底層數組,數組填充0值,然後引用它,如s3 := make([]int, 2, 3)

除了引用數組的情況,slice和array初始化很容易區別,array必須指定長度(具體數字或...),而slice不指定長度。

前面說了slice必須關聯數組,實際上看下二者的內存結構就都明白了,如下:

array在內存中是連續的塊,而slice底層也是數組,只不過加了一個頭,分別包含數組起始地址、slice長度、slice容量,數組起始地址和slice長度決定了slice引用的數組範圍,slice容量決定了append操作時是否開闢新的底層數組。

如下操作:

    //slice以數組爲基準
    v0 := [4]int{-10, 20, 30, 40}
    s := v0[1:3]
    fmt.Println(v0, s, &v0[1], &s[0])

    //s[3] = 10 panic

    s = append(s, 80)
    fmt.Println(v0, s, &v0[1], &s[0])

    s = append(s, 90)
    fmt.Println(v0, s, &v0[1], &s[0])

結果:

[-10 20 30 40] [20 30] 0xc04200a488 0xc04200a488
[-10 20 30 80] [20 30 80] 0xc04200a488 0xc04200a488
[-10 20 30 80] [20 30 80 90] 0xc04200a488 0xc042008330

一開始s指向v0的第一個元素,包含兩個元素,slice長度爲2,slice容量剩餘數組元素長度3。打印結果可以看到,此時s-0和v0-1是同一個元素,即slice是指向已有array的。

然後append一個80,此時可以看到底層數組也發生了改變,這是因爲slice的容量爲3,當前數組還夠用,所以直接用了當前數組,此時s-0和v0-1是同一個元素,即slice還是指向已有array的。

然後再append一個90,此時可以看到此時s-0和v0-1不再是同一個元素,即slice指向了新的底層數組,因爲原有數組已經不夠用了,所以新生成數組。

一定要注意此處的邏輯,否則很容易不小心修改了底層數組或想修改底層數組而生成了新數組。

那麼爲什麼要同時又數組和切片呢,在我看來,數組是爲了提供一種底層C數組的能力,而切片相當於一種容器迭代器,可以很方便的實現動態語言的易操作特性,同時結合二者可以實現高性能和易用性兼得。

明白了這些,看下如下問題,如下給函數changeValue1傳遞一個數組,修改數組元素值,然後打印,發現並沒有變化。實際上go中變量賦值都是拷貝,要想實現改變可以使用數組指針changeValue2或切片changeValue3,他們依然是拷貝,只不過拷貝的是地址,但是指向的依然是底層數組,所以能夠成功改變數組元素值,這樣就保證了go中邏輯的一致性。

//數組
func changeValue1(v [5]int)  {
    v[0] = 100
}

//數組指針
func changeValue2(v *[5]int)  {
    (*v)[0] = 100
}

//切片
func changeValue3(v []int)  {
    v[0] = 100
}

// 傳遞數組參數
v1 := [5]int{-10, 20, 30, 40, 50}
changeValue1(v1)

v2 := [5]int{-10, 20, 30, 40, 50}
changeValue2(&v2)

v3 := [5]int{-10, 20, 30, 40, 50}
changeValue3(v3[0:])

fmt.Println(v1, v2, v3)

//輸出
[-10 20 30 40 50] [100 20 30 40 50] [100 20 30 40 50]

同樣需要注意的是for range遍歷切片的時候,返回的是拷貝,要想改變對應的元素值必須使用索引來改變原來的值,如下:

    //for range 對數組/切片爲拷貝
    v4 := [5]int{-10, 20, 30, 40, 50}
    for _,v := range v4{
        v = v*2 //不會改變原來值
    }
    for k,v := range v4{
        fmt.Println(k,v)
    }

    for k,v := range v4{
        v4[k] = v*2 //改變原來值
    }
    for k,v := range v4{
        fmt.Println(k,v)
    }

6.map結構查找是否存在鍵值

在其他語言中,鍵值不存在直接應用會報異常,但是在go語言中會返回一個0值,因此可以如下兩種方法判斷鍵值是否存在:

package main

import "fmt"

func main(){
    m := map[int]string{1:"aaa", 2:"bbb", 3:"ccc", 4:"ddd", 5:"eee"}
    fmt.Println(m)

    v0, exist0 := m[5]
    if exist0{
        fmt.Println("exist key 5", v0)
    } else{
        fmt.Println("not exist key 5")
    }

    v1 := m[5]
    if v1!=""{
        fmt.Println("exist key 5", v0)
    } else{
        fmt.Println("not exist key 5")
    }
}

7、數組是值傳遞

在函數調用參數中, 數組是值傳遞, 無法通過修改數組類型的參數返回結果.

func main() {  
    x := [3]int{1, 2, 3}

    func(arr [3]int) {
        arr[0] = 7
        fmt.Println(arr)
    }(x)

    fmt.Println(x)
}

必要時需要使用切片(地址傳遞).

8、map遍歷是順序不固定

map是一種hash表實現, 每次遍歷的順序都可能不一樣.

func main() {
    m := map[string]string{
        "1": "1",
        "2": "2",
        "3": "3",
    }

    for k, v := range m {
        println(k, v)
    }
}

9、recover必須在defer函數中運行

recover捕獲的是祖父級調用時的異常, 直接調用時無效:

func main() {  
    recover()
    panic(1)
}

直接defer調用也是無效:

func main() {  
    defer recover()
    panic(1)
}

defer調用時多層嵌套依然無效:

func main() {  
    defer func() {
        func() { recover() }()
    }()
    panic(1)
}

必須在defer函數中直接調用纔有效:

func main() {  
    defer func() {
        recover()
    }()
    panic(1)
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章