前言
從golang小白到成為golang工程師快兩個(gè)月了,我要分享一下新手在開發(fā)中常犯的錯(cuò)誤,都是我親自踩過的坑。這些錯(cuò)誤中有些會(huì)導(dǎo)致無法通過編譯,這種錯(cuò)容易發(fā)現(xiàn),而有些錯(cuò)誤在編譯時(shí)不會(huì)拋出,甚至在運(yùn)行時(shí)也不會(huì)panic,如果缺少相關(guān)的知識,撓破頭皮都搞不清楚bug出在哪。
1.對nil map、nil slice 添加數(shù)據(jù)
請考慮一下這段代碼是否有錯(cuò),然后運(yùn)行一遍:
package main
func main() {
var m map[string]string
m["name"] = "zzy"
}
不出意外的話,這段代碼將導(dǎo)致一個(gè)panic:
panic: assignment to entry in nil map
這是因?yàn)榇a中只是聲明了map的類型,卻沒有為map創(chuàng)建底層數(shù)組,此時(shí)的map實(shí)際上在內(nèi)存中還不存在,即nil map,可以運(yùn)行下面的代碼進(jìn)行驗(yàn)證:
package main
import "fmt"
func main() {
var m map[string]string
if m == nil {
fmt.Println("this map is a nil map")
}
}
所以想要順利的使用map,一定要使用內(nèi)建函數(shù)make函數(shù)進(jìn)行創(chuàng)建:
m := make(map[string]string)
使用字面量的方式也是可以的,效果同make:
同樣的,直接對nil slice添加數(shù)據(jù)也是不允許的,因?yàn)閟lice的底層也是數(shù)組,沒有經(jīng)過make函數(shù)初始化時(shí),只是聲明了slice類型,而底層數(shù)組是不存在的:
package main
func main() {
var s []int
s[0] = 1
}
上面的代碼將產(chǎn)生一個(gè)panic runtime error:index out of range ,正確做法應(yīng)該是使用make函數(shù)或者字面量:
package main
func main() {
//第二個(gè)參數(shù)是slice的len,make slice時(shí)必須提供,還可以傳入第三個(gè)參數(shù)作為cap
s := make([]int, 1)
s[0] = 1
}
可能有人發(fā)現(xiàn)對nil slice使用append函數(shù)而不經(jīng)過make也是有效的:
package main
import "fmt"
func main() {
var s []int
s = append(s, 1)
fmt.Println(s) // s => [1]
}
那是因?yàn)閟lice本身其實(shí)類似一個(gè)struct,它有一個(gè)len屬性,是當(dāng)前長度,還有個(gè)cap屬性,是底層數(shù)組的長度,append函數(shù)會(huì)判斷傳入的slice的len和cap,如果len即將大于cap,會(huì)調(diào)用make函數(shù)生成一個(gè)更大的新數(shù)組并將原底層數(shù)組的數(shù)據(jù)復(fù)制過來(以上均為本人猜測,未經(jīng)查證,有興趣的同學(xué)可以去挑戰(zhàn)一下源碼),過程類似:
package main
import "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) {
//創(chuàng)建新的slice,其底層數(shù)組擴(kuò)容為原先的兩倍多
newS = make([]int, newLen, newLen*2)
copy(newS, s)
} else {
newS = s[:newLen] //直接在原數(shù)組上切一下就行
}
newS[len(s)] = arg
return newS
}
對nil map、nil slice的錯(cuò)誤使用并不是很可怕,畢竟編譯的時(shí)候就能發(fā)覺,下面要說的一個(gè)錯(cuò)誤則非??拥?,一不小心中招的話,很難排查。
2.誤用:=賦值導(dǎo)致變量覆蓋
先看下這段代碼,猜猜會(huì)打印出什么:
package main
import (
"errors"
"fmt"
)
func main() {
i := 2
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
}
我估計(jì)有人會(huì)認(rèn)為是:
1
1
實(shí)際執(zhí)行一遍,結(jié)果是:
1
2
為什么會(huì)這樣呢???
這是因?yàn)間olang中變量的作用域范圍小到每個(gè)詞法塊(不理解的同學(xué)可以簡單的當(dāng)成 {} 包裹的部分)都是一個(gè)單獨(dú)的作用域,大家都知道每個(gè)作用域的內(nèi)部聲明會(huì)屏蔽外部同名的聲明,而每個(gè) if 語句都是一個(gè)詞法塊,也就是說,如果在某個(gè) if 語句中,不小心用 := 而不是 = 對某個(gè) if 語句外的變量進(jìn)行賦值,那么將產(chǎn)生一個(gè)新的局部變量,并僅僅在 if 語句中的這個(gè)賦值語句后有效,同名的外部變量會(huì)被屏蔽,將不會(huì)因?yàn)檫@個(gè)賦值語句之后的邏輯產(chǎn)生任何變化!
在語言層面這也許并不是個(gè)錯(cuò)誤,但是實(shí)際工作中如果誤用,那么產(chǎn)生的bug會(huì)很隱秘。比如例子中的代碼,因?yàn)?err 是之前未聲明的,所以使用了 := 賦值(圖省事,少寫了 var err error ),然后既不會(huì)在編譯時(shí)報(bào)錯(cuò),也不會(huì)在運(yùn)行時(shí)報(bào)錯(cuò),它會(huì)讓你百思不得其解,覺得自己的邏輯明明走對了,為什么最后的結(jié)果卻總是不對,直到你一點(diǎn)一點(diǎn)調(diào)試,才發(fā)現(xiàn)自己不小心多寫了一個(gè) : 。
我因?yàn)檫@個(gè)被坑過好幾回了,每次都查了好久,以為是自己邏輯有漏洞,最后發(fā)現(xiàn)是把 = 寫成了 := ,唉,說起來都是淚。
3.將值傳遞當(dāng)成引用傳遞
值類型數(shù)據(jù)和引用類型數(shù)據(jù)的區(qū)別我相信在座的各位都能分得清,否則不用往下看了,因?yàn)榭床欢?/p>
在golang中, array 和 struct 都是值類型的,而 slice 、 map 、 chan 是引用類型,所以我們寫代碼的時(shí)候,基本不使用 array ,而是用 slice 代替它,對于 struct 則盡量使用指針,這樣避免傳遞變量時(shí)復(fù)制數(shù)據(jù)的時(shí)間和空間消耗,也避免了無法修改原數(shù)據(jù)的情況。
如果對這點(diǎn)認(rèn)識不清,導(dǎo)致的后果可能是代碼有瑕疵,更嚴(yán)重的是產(chǎn)生bug。
考慮這段代碼并運(yùn)行一下:
package main
import "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 語法的機(jī)制,我絮叨一下:golang中 for range 語法非常方便,可以輕松的遍歷 array 、 slice 、 map 等結(jié)構(gòu),但是它有一個(gè)特點(diǎn),就是會(huì)在遍歷時(shí)把當(dāng)前遍歷到的元素,復(fù)制給內(nèi)部變量,具體就是在 whoIsDead 函數(shù)中的 for range 里,會(huì)把 people 里的每個(gè) person ,都復(fù)制給 p 這個(gè)變量,類似于這樣的操作:
p := person
上文說過, struct 是值類型,所以在賦值給 p 的過程中,實(shí)際上需要重新生成一份 person 數(shù)據(jù),便于 for range 內(nèi)部使用,不信試試:
package main
import "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 這個(gè)操作實(shí)際上更改的是新生成的 p 數(shù)據(jù),而非 people 中原本的 person ,這里產(chǎn)生了一個(gè)bug。
在 for range 內(nèi)部只需讀取數(shù)據(jù)而不需要修改的情況下,隨便怎么寫也無所謂,頂多就是代碼不夠完美,而需要修改數(shù)據(jù)時(shí),則最好傳遞 struct 指針:
package main
import "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
}
}
}
運(yùn)行一下:
who is dead? px
everything is ok,很棒棒的代碼。
還有另外的方法,使用索引訪問 people 中的 person ,改動(dòng)一下 whoIsDead 函數(shù),也能達(dá)到同樣的目的:
func whoIsDead(people []person) {
for i := 0; i len(people); i++ {
if people[i].age 50 {
people[i].isDead = true
}
}
}
好, for range 部分講到這里,接下來說一說 map 結(jié)構(gòu)中值的傳遞和修改問題。
這段代碼將之前的 people []person 改成了 map 結(jié)構(gòu),大家覺得有錯(cuò)誤嗎,如果有錯(cuò),錯(cuò)在哪:
package main
import "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 一下,報(bào)錯(cuò):
cannot assign to struct field people[name].isDead in map
這個(gè)報(bào)錯(cuò)有點(diǎn)迷,我估計(jì)很多人都看不懂了。我解答下, map 底層使用了 array 存儲(chǔ)數(shù)據(jù),并且沒有容量限制,隨著 map 元素的增多,需要?jiǎng)?chuàng)建更大的 array 來存儲(chǔ)數(shù)據(jù),那么之前的地址就無效了,因?yàn)閿?shù)據(jù)被復(fù)制到了新的更大的 array 中,所以 map 中元素是不可取址的,也是不可修改的。這個(gè)報(bào)錯(cuò)的意思其實(shí)就是不允許修改 map 中的元素。
即便 map 中元素沒有以上限制,這段代碼依然是錯(cuò)誤的,想一想,為什么?答案之前已經(jīng)說過了。
那么,怎么改才能正確呢,老套路,依然是使用指針:
package main
import "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 屬性而非通過指針修改時(shí):
package main
type 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
}
}
會(huì)直接報(bào)一個(gè)編譯錯(cuò)誤:
cannot assign to p.(person).isDead
即便編譯通過,代碼也是錯(cuò)誤的 ,始終要記住 struct 是值類型的數(shù)據(jù),請使用指針去操作它, 正確做法是:
package main
import "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中指針真是居家旅行、升職加薪的必備知識啊,希望同學(xué)們熟練掌握。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。
您可能感興趣的文章:- Golang報(bào)“import cycle not allowed”錯(cuò)誤的2種解決方法
- Golang常見錯(cuò)誤之值拷貝和for循環(huán)中的單一變量詳解
- Golang巧用defer進(jìn)行錯(cuò)誤處理的方法
- golang log4go的日志輸出優(yōu)化詳解
- Golang中重復(fù)錯(cuò)誤處理的優(yōu)化方法