golang新手容易犯的3個錯誤

來源:互聯網
上載者:User

從golang小白到成為golang工程師快兩個月了,我要分享一下新手在開發中常犯的錯誤,都是我親自踩過的坑。這些錯誤中有些會導致無法通過編譯,這種錯容易發現,而有些錯誤在編譯時間不會拋出,甚至在運行時也不會panic,如果缺少相關的知識,撓破頭皮都搞不清楚bug出在哪。

1.對nil map、nil slice 添加資料

請考慮一下這段代碼是否有錯,然後運行一遍:

package mainfunc main() {    var m map[string]string    m["name"] = "zzy"}

不出意外的話,這段代碼將導致一個panic:

panic: assignment to entry in nil map

這是因為代碼中只是聲明了map的類型,卻沒有為map建立底層數組,此時的map實際上在記憶體中還不存在,即nil map,可以運行下面的代碼進行驗證:

package mainimport "fmt"func main() {    var m map[string]string    if m == nil {        fmt.Println("this map is a nil map")    }}

所以想要順利的使用map,一定要使用內建函數make函數進行建立:

m := make[string]string

使用字面量的方式也是可以的,效果同make:

m := map[string]string{}

同樣的,直接對nil slice添加資料也是不允許的,因為slice的底層也是數組,沒有經過make函數初始化時,只是聲明了slice類型,而底層數組是不存在的:

package mainfunc main() {    var s []int    s[0] = 1}

上面的代碼將產生一個panicruntime error:index out of range,正確做法應該是使用make函數或者字面量:

package mainfunc main() {    //第二個參數是slice的len,make slice時必須提供,還可以傳入第三個參數作為cap      s := make([]int, 1)     s[0] = 1}

可能有人發現對nil slice使用append函數而不經過make也是有效:

package mainimport "fmt"func main() {    var s []int    s = append(s, 1)    fmt.Println(s) // s => [1]}

那是因為slice本身其實類似一個struct,它有一個len屬性,是當前長度,還有個cap屬性,是底層數組的長度,append函數會判斷傳入的slice的len和cap,如果len即將大於cap,會調用make函數產生一個更大的新數組並將原底層數組的資料複製過來(以上均為本人猜測,未經查證,有興趣的同學可以去挑戰一下源碼),過程類似:

package mainimport "fmt"func main() {    var s []int //len(s)和cap(s)都是0    s = append(s, 1)    fmt.Println(s) // s => [1]}func append(s []int, arg int) []int {    newLen := len(s) + 1    var newS []int    if newLen > cap(s) {        //建立新的slice,其底層數組擴容為原先的兩倍多        newS = make([]int, newLen, newLen*2)        copy(newS, s)    } else {        newS = s[:newLen] //直接在原數組上切一下就行    }    newS[len(s)] = arg    return newS}

對nil map、nil slice的錯誤使用並不是很可怕,畢竟編譯的時候就能發覺,下面要說的一個錯誤則非常坑爹,一不小心中招的話,很難排查。

2.誤用:=賦值導致變數覆蓋

先看下這段代碼,猜猜會列印出什麼:

package mainimport (    "errors"    "fmt")var i = 2func main() {    if i > 1 {        i, err := doDivision(i, 2)        if err != nil {            panic(err)        }        fmt.Println(i)    }    fmt.Println(i)}func doDivision(x, y int) (int, error) {    if y == 0 {        return 0, errors.New("input is invalid")    }    return x / y, nil}

我估計有人會認為是:

11

實際執行一遍,結果是:

12

為什麼會這樣呢!?
這是因為golang中變數的範圍範圍小到每個詞法塊(不理解的同學可以簡單的當成{} 包裹的部分)都是一個單獨的範圍,大家都知道每個範圍的內部聲明會屏蔽外部同名的聲明,而每個if語句都是一個詞法塊,也就是說,如果在某個if語句中,不小心用:=而不是=對某個if語句外的變數進行賦值,那麼將產生一個新的局部變數,並僅僅在if語句中的這個指派陳述式後有效,同名的外部變數會被屏蔽,將不會因為這個指派陳述式之後的邏輯產生任何變化!
在語言層面這也許並不是個錯誤,但是實際工作中如果誤用,那麼產生的bug會很隱秘。比如例子中的代碼,因為err是之前未聲明的,所以使用了:=賦值(圖省事,少寫了var err error),然後既不會在編譯時間報錯,也不會在運行時報錯,它會讓你百思不得其解,覺得自己的邏輯明明走對了,為什麼最後的結果卻總是不對,直到你一點一點調試,才發現自己不小心多寫了一個
我因為這個被坑過好幾回了,每次都查了好久,以為是自己邏輯有漏洞,最後發現是把=寫成了:=,唉,說起來都是淚。

3.將值傳遞當成引用傳遞

實值型別資料和參考型別資料的區別我相信在座的各位都能分得清,否則不用往下看了,因為看不懂。
在golang中,arraystruct都是實值型別的,而slicemapchan是參考型別,所以我們寫代碼的時候,基本不使用array,而是用slice代替它,對於struct則盡量使用指標,這樣避免傳遞變數時複製資料的時間和空間消耗,也避免了無法修改原資料的情況。
如果對這點認識不清,導致的後果可能是代碼有瑕疵,更嚴重的是產生bug。
考慮這段代碼並運行一下:

package mainimport "fmt"type person struct {    name   string    age    byte    isDead bool}func main() {    p1 := person{name: "zzy", age: 100}    p2 := person{name: "dj", age: 99}    p3 := person{name: "px", age: 20}    people := []person{p1, p2, p3}    whoIsDead(people)    for _, p := range people {        if p.isDead {            fmt.Println("who is dead?", p.name)        }    }}func whoIsDead(people []person) {    for _, p := range people {        if p.age < 50 {            p.isDead = true        }    }}

我相信很多人一看就看出問題在哪了,但肯定還有人不清楚for range文法的機制,我絮叨一下:golang中for range文法非常方便,可以輕鬆的遍曆arrayslicemap等結構,但是它有一個特點,就是會在遍曆時把當前遍曆到的元素,複製給內部變數,具體就是在whoIsDead函數中的for range裡,會把people裡的每個person,都複製給p這個變數,類似於這樣的操作:

p := person

上文說過,struct是實值型別,所以在賦值給p的過程中,實際上需要重建一份person資料,便於for range內部使用,不信試試:

package mainimport "fmt"type person struct {    name   string    age    byte    isDead bool}func main() {    p1 := person{name: "zzy", age: 100}    p2 := p1    p1.name = "changed"    fmt.Println(p2.name)}

所以p.isDead = true這個操作實際上更改的是新產生的p資料,而非people中原本的person,這裡產生了一個bug。
for range內部只需讀取資料而不需要修改的情況下,隨便怎麼寫也無所謂,頂多就是代碼不夠完美,而需要修改資料時,則最好傳遞struct指標:

package mainimport "fmt"type person struct {    name   string    age    byte    isDead bool}func main() {    p1 := &person{name: "zzy", age: 100}    p2 := &person{name: "dj", age: 99}    p3 := &person{name: "px", age: 20}    people := []*person{p1, p2, p3}    whoIsDead(people)    for _, p := range people {        if p.isDead {            fmt.Println("who is dead?", p.name)        }    }}func whoIsDead(people []*person) {    for _, p := range people {        if p.age < 50 {            p.isDead = true        }    }}

運行一下:

who is dead? px

everything is ok,很棒棒的代碼。
還有另外的方法,使用索引訪問people中的person,改動一下whoIsDead函數,也能達到同樣的目的:

func whoIsDead(people []person) {    for i := 0; i < len(people); i++ {        if people[i].age < 50 {            people[i].isDead = true        }    }}

好,for range部分講到這裡,接下來說一說map結構中值的傳遞和修改問題。
這段代碼將之前的people []person改成了map結構,大家覺得有錯誤嗎,如果有錯,錯在哪:

package mainimport "fmt"type person struct {    name   string    age    byte    isDead bool}func main() {    p1 := person{name: "zzy", age: 100}    p2 := person{name: "dj", age: 99}    p3 := person{name: "px", age: 20}    people := map[string]person{        p1.name: p1,        p2.name: p2,        p3.name: p3,    }    whoIsDead(people)    if p3.isDead {        fmt.Println("who is dead?", p3.name)    }}func whoIsDead(people map[string]person) {    for name, _ := range people {        if people[name].age < 50 {            people[name].isDead = true        }    }}

go run一下,報錯:

cannot assign to struct field people[name].isDead in map

這個報錯有點迷,我估計很多人都看不懂了。我解答下,map底層使用了array儲存資料,並且沒有容量限制,隨著map元素的增多,需要建立更大的array來儲存資料,那麼之前的地址就無效了,因為資料被複製到了新的更大的array中,所以map中元素是不可取址的,也是不可修改的。這個報錯的意思其實就是不允許修改map中的元素。
即便map中元素沒有以上限制,這段代碼依然是錯誤的,想一想,為什嗎?答案之前已經說過了。
那麼,怎麼改才能正確呢,老套路,依然是使用指標:

package mainimport "fmt"type person struct {    name   string    age    byte    isDead bool}func main() {    p1 := &person{name: "zzy", age: 100}    p2 := &person{name: "dj", age: 99}    p3 := &person{name: "px", age: 20}    people := map[string]*person{        p1.name: p1,        p2.name: p2,        p3.name: p3,    }    whoIsDead(people)    if p3.isDead {        fmt.Println("who is dead?", p3.name)    }}func whoIsDead(people map[string]*person) {    for name, _ := range people {        if people[name].age < 50 {            people[name].isDead = true        }    }}

另外,在interface{}斷言裡試圖直接修改struct屬性而非通過指標修改時:

package maintype person struct {    name   string    age    byte    isDead bool}func main() {    p := person{name: "zzy", age: 100}    isDead(p)}func isDead(p interface{}) {    if p.(person).age < 101 {        p.(person).isDead = true    }}

會直接報一個編譯錯誤:

cannot assign to p.(person).isDead

即便編譯通過,代碼也是錯誤的 ,始終要記住struct是實值型別的資料,請使用指標去操作它, 正確做法是:

package mainimport "fmt"type person struct {    name   string    age    byte    isDead bool}func main() {    p := &person{name: "zzy", age: 100}    isDead(p)    fmt.Println(p)}func isDead(p interface{}) {    if p.(*person).age < 101 {        p.(*person).isDead = true    }}

最後,不能不說golang中指標真是居家旅行、升職加薪的必備知識啊,希望同學們熟練掌握。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.