Go:为什么你应当避免使用指针 为什么在使用各种指针式仪表时,总希望
yuyutoo 2024-10-11 21:40 14 浏览 0 评论
四哥水平有限,如有翻译或理解错误,烦请帮忙指出,感谢!
别被作者的这个标题误导了,其实阅读完全文,发现作者并不是排斥使用指针,而是应选择适当的场景去使用指针。
原文如下:
什么是指针
为了覆盖基础知识,我们先讲解什么是指针。
看下面 CoffeeMachine 的例子,CoffeeMachine 结构体中保存咖啡豆的数量。
为了创建一台“咖啡机”,我需要使用 NewCoffeeMachine() 函数。
这里我创建了一个新的结构体,使用 & 操作符返回结构体的引用。
type CoffeeMachine struct { NumberOfCoffeeBeans int } func NewCoffeeMachine() *CoffeeMachine { return &CoffeeMachine{} }
当我将 CoffeeMachine 结构体的引用传递给其他函数时,在这些函数里可以改变结构体的底层数据。
例如,我可以创建 SetNumberOfCoffeeBeans() 函数,可以像下面这样在函数内部改变 CoffeeMachine 结构体的值:
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() 函数的指针接收者指向 CoffeeMachine() 结构体的底层结构,所以在函数内部可以直接改变结构体字段的值。
因此,当我运行此程序时,显示机器中确实有 100 个咖啡豆!
go run main.go The coffee machine has 100 beans
不使用指针解决这个问题
我们可以使用非指针方式实现同样的“咖啡机”
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) }
现在主要不同的是 SetNumberOfCoffeeBeans() 函数接收的是 CoffeeMachine 结构体的副本,正因为这样,需要返回更新之后的 CoffeeMachine 结构体。
输出结构如下:
go run main.go The coffee machine has 100 beans
性能
好的,到这里你可能会在想:“是不是传值始终都会比传指针效率低”。
现在我们来做个实用性的测试,比较下传指针和传值的效率。
我修改了 CoffeeMachine 结构体,加入了两个字段 UID 和 Description。
type CoffeeMachine struct { UID string Description string NumberOfCoffeeBeans int }
下一步,我使用指针方式给结构体赋值,循环 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) }
同样的,我们再次使用传值的方式实现上面的赋值操作。
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) }
分别执行这两段程序,发现消耗的时间差不多:
With pointers result: 32ms Without pointers result: 31ms
我上面举例子使用的结构体比较小,如果需要拷贝的结构体很大,则性能差距会更大。
“意外之喜”
所以,使用指针的缺点是什么?
当你在函数之间传指针时,你不知道是否会改变指针指向的值。
这增加了代码库的复杂性,并且随着代码的增长,很容易就会出现错误,因为调用堆栈深处的某个地方改变了指针指向的值。
最近,在我的项目里遇到了一个“搜索商品”的函数:
func SearchProducts(criteria *SearchCriteria) []Product { // Searches for products here }
在这个函数里,我不希望 SearchCriteria 被改变。但是,事实证明,在函数某个地方已经将 SearchCriteria 的值改变了。
在我看来,尽可能使用不可变的参数(即值而不是指针)是一种更好的做法,并且可以避免此类bug。
指针的 Nil 值
使用指针的时候,我们都需要考虑指针可能为 nil 的情况。程序员在使用指针之前不会被明确地强制检查指针是否为 nil 的情况,因此在代码里很容易出现这种人为错误。
一起来思考下面这个例子:
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) }
在这个例子中,函数 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
解决这个问题更优雅的做法是,如果商品没有找到就返回空结构体和错误信息,想下面这样:
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,绝对可以确保不会发生 nil pointer 错误。
什么时候使用指针
好吧,使用指针并不总是坏事,下面这两种情况你应当使用指针
当你确实需要修改参数的时候
举个例子,下面的代码片段,通过指针的方式可以直接在函数 setName() 里面修改 User 结构体的 Name 字段。
type User struct { Name string } func (user *User) setName(name string) { user.Name = name } func main() { user := &User{} user.setName("John") }
当使用单例的时候
有时候,当需要在全局保存唯一一个实例时,使用指针就很重要,这样就能确保内存中的数据不会发生多次拷贝(拷贝是需要消耗性能的)。
总结
不要在项目里面疯狂地使用指针,而是要考虑何时以及如何更好地使用指针。
如果你遵循上面的建议,大概率你就不会再次遇到 nil pointer dereference 的错误!
相关推荐
- 《保卫萝卜2》安卓版大更新 壕礼助阵世界杯
-
《保卫萝卜2:极地冒险》本周不仅迎来了安卓版本的重大更新,同时将于7月4日本周五,带来“保卫萝卜2”安卓版本世界杯主题活动的火热开启,游戏更新与活动两不误。一定有玩家会问,激萌塔防到底进行了哪些更新?...
- 儿童手工折纸:胡萝卜,和孩子一起边玩边学carrot
-
1、准备两张正方形纸,一橙一绿,对折出折痕。2、橙色沿其中一条对角线如图折两三角形。3、把上面三角折平,如图。4、绿色纸折成三角形。5、再折成更小的三角形。6、再折三分之一如图。7、打开折纸,压平中间...
- 《饥荒》食物代码有哪些(饥荒最新版代码总汇食物篇)
-
饥荒游戏中,玩家们需要获取各种素材与食物,进行生存。玩家们在游戏中,进入游戏后按“~”键调出控制台使用代码,可以直接获得素材。比如胡萝卜的代码是carrot,玉米的代码是corn,南瓜的代码是pump...
- Skyscanner:帮你找到最便宜机票 订票不求人
-
你喜欢旅行吗?在合适的时间、合适的目的地,来一场说走就走的旅行?机票就是关键!Skyscanner这款免费的手机应用,在几秒钟内比较全球600多家航空公司的航班安排、价格和时刻表,帮你节省金钱和时间。...
- 小猪佩奇第二季50(小猪佩奇第二季英文版免费观看)
-
Sleepover过夜Itisnighttime.现在是晚上。...
- 我在民政局工作的那些事儿(二)(我在民政局上班)
-
时间到了1997年的秋天,经过一年多的学习和实践,我在处理结婚和离婚的事情更加的娴熟,也获得了领导的器重,所以我在处理平时的工作时也能得心应手。这一天我正在离婚处和同事闲聊,因为离婚处几天也遇不到人,...
- 夏天来了就你还没瘦?教你不节食13天瘦10斤的哥本哈根减肥法……
-
好看的人都关注江苏气象啦夏天很快就要来了你是否和苏苏一样身上的肉肉还没做好准备?真是一个悲伤的故事……下面这个哥本哈根减肥法苏苏的同事亲测有效不节食不运动不反弹大家快来一起试试看吧~DAY1...
- Pursuing global modernization for peaceful development, mutually beneficial cooperation, prosperity for all
-
AlocalworkeroperatesequipmentintheChina-EgyptTEDASuezEconomicandTradeCooperationZonei...
- Centuries-old tea road regains glory as Belt and Road cooperation deepens
-
FUZHOU/ST.PETERSBURG,Oct.2(Xinhua)--NestledinthepicturesqueWuyiMountainsinsoutheastChi...
- Ftrace function graph简介(flat function)
-
引言由于android开发的需要与systrace的普及,现在大家在进行性能与功耗分析时候,经常会用到systrace跟pefetto.而systrace就是基于内核的eventtracing来实...
- JAVA历史版本(java各版本)
-
JAVA发展1.1996年1月23日JDK1.0Java虚拟机SunClassicVM,Applet,AWT2.1997年2月19日JDK1.1JAR文件格式,JDBC,JavaBea...
- java 进化史1(java的进阶之路)
-
java从1996年1月第一个版本诞生,到2022年3月最新的java18,已经经历了27年,整整18个大的版本。很久之前有人就说java要被淘汰,但是java活到现在依然坚挺,不知道java还能活...
- 学习java第二天(java学完后能做什么)
-
#java知识#...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- 《保卫萝卜2》安卓版大更新 壕礼助阵世界杯
- 儿童手工折纸:胡萝卜,和孩子一起边玩边学carrot
- 《饥荒》食物代码有哪些(饥荒最新版代码总汇食物篇)
- Skyscanner:帮你找到最便宜机票 订票不求人
- 小猪佩奇第二季50(小猪佩奇第二季英文版免费观看)
- 我在民政局工作的那些事儿(二)(我在民政局上班)
- 夏天来了就你还没瘦?教你不节食13天瘦10斤的哥本哈根减肥法……
- Pursuing global modernization for peaceful development, mutually beneficial cooperation, prosperity for all
- Centuries-old tea road regains glory as Belt and Road cooperation deepens
- 15 THE NUTCRACKERS OF NUTCRACKER LODGE (CONTINUED)胡桃夹子小屋里的胡桃夹子(续篇)
- 标签列表
-
- mybatis plus (70)
- scheduledtask (71)
- css滚动条 (60)
- java学生成绩管理系统 (59)
- 结构体数组 (69)
- databasemetadata (64)
- javastatic (68)
- jsp实用教程 (53)
- fontawesome (57)
- widget开发 (57)
- vb net教程 (62)
- hibernate 教程 (63)
- case语句 (57)
- svn连接 (74)
- directoryindex (69)
- session timeout (58)
- textbox换行 (67)
- extension_dir (64)
- linearlayout (58)
- vba高级教程 (75)
- iframe用法 (58)
- sqlparameter (59)
- trim函数 (59)
- flex布局 (63)
- contextloaderlistener (56)