Go:為什么你應(yīng)當避免使用指針
via:
https://medium.com/better-programming/why-you-should-avoid-pointers-in-go-36724365a2a7
作者:Dirk Hoekstra
四哥水平有限,如有翻譯或理解錯誤,煩請幫忙指出,感謝!
別被作者的這個標題誤導了,其實閱讀完全文,發(fā)現(xiàn)作者并不是排斥使用指針,而是應(yīng)選擇適當?shù)膱鼍叭ナ褂弥羔?/strong>。關(guān)于指針的基礎(chǔ)知識,可以閱讀公號之前發(fā)的文章?指針。
原文如下:
什么是指針
為了覆蓋基礎(chǔ)知識,我們先講解什么是指針。
看下面 CoffeeMachine 的例子,CoffeeMachine 結(jié)構(gòu)體中保存咖啡豆的數(shù)量。
為了創(chuàng)建一臺“咖啡機”,我需要使用 NewCoffeeMachine() 函數(shù)。
這里我創(chuàng)建了一個新的結(jié)構(gòu)體,使用 & 操作符返回結(jié)構(gòu)體的引用。
type?CoffeeMachine?struct?{
????NumberOfCoffeeBeans?int
}
func?NewCoffeeMachine()?*CoffeeMachine?{
????return?&CoffeeMachine{}
}
當我將 CoffeeMachine 結(jié)構(gòu)體的引用傳遞給其他函數(shù)時,在這些函數(shù)里可以改變結(jié)構(gòu)體的底層數(shù)據(jù)。
例如,我可以創(chuàng)建 SetNumberOfCoffeeBeans() 函數(shù),可以像下面這樣在函數(shù)內(nèi)部改變 CoffeeMachine 結(jié)構(gòu)體的值:
package?main
import?"fmt"
type?CoffeeMachine?struct?{
????NumberOfCoffeeBeans?int
}
func?NewCoffeeMachine()?*CoffeeMachine?{
????return?&CoffeeMachine{}
}
func?(cm?*CoffeeMachine)?SetNumberOfCoffeeBeans(n?int)?{
????cm.NumberOfCoffeeBeans?=?n
}
func?main()?{
????cm?:=?NewCoffeeMachine()
????cm.SetNumberOfCoffeeBeans(100)
????fmt.Printf("The?coffee?machine?has?%d?beans\n",?cm.NumberOfCoffeeBeans)
}
因為 SetNumberOfCoffeeBeans() 函數(shù)的指針接收者指向 CoffeeMachine() 結(jié)構(gòu)體的底層結(jié)構(gòu),所以在函數(shù)內(nèi)部可以直接改變結(jié)構(gòu)體字段的值。
因此,當我運行此程序時,顯示機器中確實有 100 個咖啡豆!
go?run?main.go
The?coffee?machine?has?100?beans
不使用指針解決這個問題
我們可以使用非指針方式實現(xiàn)同樣的“咖啡機”
func?NewCoffeeMachine()?CoffeeMachine?{
????return?CoffeeMachine{}
}
func?(cm?CoffeeMachine)?SetNumberOfCoffeeBeans(n?int)?CoffeeMachine?{
????cm.NumberOfCoffeeBeans?=?n
????return?cm
}
func?main()?{
????cm?:=?NewCoffeeMachine()
????cm?=?cm.SetNumberOfCoffeeBeans(100)
????fmt.Printf("The?coffee?machine?has?%d?beans\n",?cm.NumberOfCoffeeBeans)
}
現(xiàn)在主要不同的是 SetNumberOfCoffeeBeans() 函數(shù)接收的是 CoffeeMachine 結(jié)構(gòu)體的副本,正因為這樣,需要返回更新之后的 CoffeeMachine 結(jié)構(gòu)體。
輸出結(jié)構(gòu)如下:
go?run?main.go
The?coffee?machine?has?100?beans
性能
好的,到這里你可能會在想:“是不是傳值始終都會比傳指針效率低”。
現(xiàn)在我們來做個實用性的測試,比較下傳指針和傳值的效率。
我修改了 CoffeeMachine 結(jié)構(gòu)體,加入了兩個字段 UID 和 Description。
type?CoffeeMachine?struct?{
????UID?string
????Description?string
????NumberOfCoffeeBeans?int
}
下一步,我使用指針方式給結(jié)構(gòu)體賦值,循環(huán) 100000 次,測量需要消耗多長時間。
func?main()?{
????cm?:=?NewCoffeeMachine()
????start?:=?time.Now()
????for?i?:=?0;?i<100000;?i++?{
????????cm.SetUID(fmt.Sprintf("random-generated-uid-%d",?i))
????????cm.SetNumberOfCoffeeBeans(i)
????????cm.SetDescription(fmt.Sprintf("This?is?the?best?coffee?machine?that?is?around!?This?is?version?%d",?i))
????}
????elapsed?:=?time.Since(start)
????fmt.Printf("It?took?%s\n",?elapsed)
}
同樣的,我們再次使用傳值的方式實現(xiàn)上面的賦值操作。
func?main()?{
????cm?:=?NewCoffeeMachine()
????start?:=?time.Now()
????for?i?:=?0;?i<100000;?i++?{
????????cm?=?cm.SetUID(fmt.Sprintf("random-generated-uid-%d",?i))
????????cm?=?cm.SetNumberOfCoffeeBeans(i)
????????cm?=?cm.SetDescription(fmt.Sprintf("This?is?the?best?coffee?machine?that?is?around!?This?is?version?%d",?i))
????}
????elapsed?:=?time.Since(start)
????fmt.Printf("It?took?%s\n",?elapsed)
}
分別執(zhí)行這兩段程序,發(fā)現(xiàn)消耗的時間差不多:
With?pointers?result:?????32ms
Without?pointers?result:?31ms
我上面舉例子使用的結(jié)構(gòu)體比較小,如果需要拷貝的結(jié)構(gòu)體很大,則性能差距會更大。
“意外之喜”
所以,使用指針的缺點是什么?
當你在函數(shù)之間傳指針時,你不知道是否會改變指針指向的值。
這增加了代碼庫的復(fù)雜性,并且隨著代碼的增長,很容易就會出現(xiàn)錯誤,因為調(diào)用堆棧深處的某個地方改變了指針指向的值。
最近,在我的項目里遇到了一個“搜索商品”的函數(shù):
func?SearchProducts(criteria?*SearchCriteria)?[]Product?{
????//?Searches?for?products?here
}
在這個函數(shù)里,我不希望 SearchCriteria 被改變。但是,事實證明,在函數(shù)某個地方已經(jīng)將 SearchCriteria 的值改變了。
在我看來,盡可能使用不可變的參數(shù)(即值而不是指針)是一種更好的做法,并且可以避免此類bug。
指針的 Nil 值
使用指針的時候,我們都需要考慮指針可能為 nil 的情況。程序員在使用指針之前不會被明確地強制檢查指針是否為 nil 的情況,因此在代碼里很容易出現(xiàn)這種人為錯誤。
一起來思考下面這個例子:
package?main
import?"fmt"
type?Product?struct?{
????Price?string
}
func?GetProduct(productUid?string)?*Product?{
????//?Code?that?retrieves?a?product?or?nil?if?not?found.
????//?Let's?simulate?a?"not?found"?scenario.
????return?nil
}
func?main()?{
????product?:=?GetProduct("corona-face-mask")
????fmt.Println("The?Corona?Face?mask?is?currently?%d?euro's",?product.Price)
}
在這個例子中,函數(shù) GetProduct() 返回一個 nil 值,但是我們沒有強制檢查返回值是否為 nil,所以運行這代代碼會報錯 nil pointer:
panic:?runtime?error:?invalid?memory?address?or?nil?pointer?dereference
[signal?SIGSEGV:?segmentation?violation?code=0x1?addr=0x8?pc=0x10994f3]
goroutine?1?[running]:
main.main()
?main.go:17?+0x23
exit?status?2
解決這個問題更優(yōu)雅的做法是,如果商品沒有找到就返回空結(jié)構(gòu)體和錯誤信息,想下面這樣:
package?main
import?(
????"fmt"
????"errors"
)
type?Product?struct?{
????Price?string
}
func?GetProduct(productUid?string)?(Product,?error)?{
????//?Code?that?retrieves?a?product?or?nil?if?not?found.
????//?Let's?simulate?a?"not?found"?scenario.
????return?Product{},?errors.New("Product?not?found")
}
func?main()?{
????product,?err?:=?GetProduct("corona-face-mask")
????if?err?!=?nil?{
????????fmt.Println("Error,?product?not?found")
????}?else?{
????????fmt.Println("The?Corona?Face?mask?is?currently?%d?euro's",?product.Price)
????}
}
像上面那樣,判斷返回值是否為 nil,絕對可以確保不會發(fā)生 nil pointer 錯誤。
什么時候使用指針
好吧,使用指針并不總是壞事,下面這兩種情況你應(yīng)當使用指針
當你確實需要修改參數(shù)的時候
舉個例子,下面的代碼片段,通過指針的方式可以直接在函數(shù) setName() 里面修改 User 結(jié)構(gòu)體的 Name 字段。
type?User?struct?{
????Name?string
}
func?(user?*User)?setName(name?string)?{
????user.Name?=?name????
}
func?main()?{
????user?:=?&User{}
????user.setName("John")
}
當使用單例的時候
有時候,當需要在全局保存唯一一個實例時,使用指針就很重要,這樣就能確保內(nèi)存中的數(shù)據(jù)不會發(fā)生多次拷貝(拷貝是需要消耗性能的)。
總結(jié)
不要在項目里面瘋狂地使用指針,而是要考慮何時以及如何更好地使用指針。
如果你遵循上面的建議,大概率你就不會再次遇到 nil pointer dereference 的錯誤!
推薦閱讀
站長 polarisxu
自己的原創(chuàng)文章
不限于 Go 技術(shù)
職場和創(chuàng)業(yè)經(jīng)驗
Go語言中文網(wǎng)
每天為你
分享 Go 知識
Go愛好者值得關(guān)注
