当初团队里简直所有的代码都须要通过 Code Review(代码审查)之后才容许合入主分支。笔者在 CR 中看到了不少不适宜的问题,也看到了不少值得学习的点,于是决定一点一滴地记录这些做法、教训、教训,以飨读者。如有谬误,也欢送读者不吝指正。
上一篇文章: context 类型的 key 有什么考究?
- 一句话标准
- 问题背景
- 可能存在问题
- 解决办法
- 应用值传递的长处
- 什么时候应该应用援用传递
一句话标准
- 当函数的入参、出参是一个构造体时,如无必要,应用值传递而不是援用传递
问题背景
当咱们用 Go 开发时,对外裸露一个函数 / 办法时,以构造体作为函数的入参或出参,是十分常见的。比如说,咱们实现上面的一个函数,返回一个用户信息。
比如说,咱们提供两个函数,别离用来获取相干用户的权限信息:
package permission
type UserPermission struct {
UserID string
Permissions []string}
// GetUserPermissions 获取指定 user ID 的权限
func GetUserPermissions(userID string) *UserPermission
// SetUserPermissions 设置指定 user ID 的权限
func SetUserPermissions(permission *UserPermission) error
能够看到,在下面的代码中,UserInfo
作为出入参都是以指针存在的。这种模式的代码十分多,也十分典型,而且大家都会习惯于这么写,特地是有面向对象思路的程序员。
那么,这么写能够吗?有什么问题呢?其实这个要具体问题具体分析,上面咱们就来一起看一看。
可能存在问题
假如有一个新需要,是复制一个用户的权限给新用户。这逻辑看起来挺简略,代码里这么写,齐全是荒诞不经的:
// CopyUserPermissions 复制用户权限
func CopyUserPermissions(ctx context.Context, fromUserID, toUserID string) error {pms := permission.GetUserPermissions(fromUserID)
pms.UserID = toUserID
return permission.SetUserPermissions(psm)
}
这种写法,节俭了内存应用,逻辑也十分清晰,code review 的时候直呼赞。
有什么问题吗?隐含的问题不在 CopyUserPermissions
上,而在 GetUserPermissions
中。有时候某些数据,咱们可能是通过本地缓存来实现的,基于这种模式,GetUserPermissions
外部的逻辑就有可能是:
- 如果内存缓存中数据命中,那么返回缓存数据
- 如果缓存数据未命中,则 RPC 搜寻,失去数据后缓存到内存中
GetUserPermissions
返回的是一个援用,那么它或者返回的是它在内存缓存中的援用。那么在 CopyUserPermissions
中批改了援用的内容,那么下一次申请 fromUserID
的数据信息时,内存缓存启发曾经被篡改,数据不统一了,bug 就这么产生。
解决办法
解决办法很简略,将 GetUserPermissions
和 SetUserPermissions
的出入参 UserPermission
,从援用类型改为值类型,也就是去掉 *
指针。即使是外部存储用的是 *
,也齐全能够用 Go 自带的值语法将数据 (浅) 复制进来。
入参和出参都须要改一下:
// GetUserPermissions 获取指定 user ID 的权限
func GetUserPermissions(userID string) UserPermission
// SetUserPermissions 设置指定 user ID 的权限
func SetUserPermissions(permission UserPermission) error
应用值传递的长处
应用值的长处,笔者这里简略总结一下吧:
- 前文提到的,值传递针对原始值多了一次复制动作。作为入参,能够说是起到了相似于 C++ 中 const 参数的局部作用,防止了应用该参数的逻辑,批改参数而导致数据作用域溢出。
- 援用是指针类型,有可能为
nil
。值传递相当于做了一个默认的申明,向应用方默认提供了一个承诺:这个变量永远是可用的,不会也不须要判断 nil 的问题。
什么时候应该应用援用传递
当然了,其实很多状况下,应用援用传递的还是很多。这一条标准的存在意义是:代码设计开发的时候,要时刻留神逻辑的细节。所以说这条标准,说的是“非必要”。那么什么状况是必要的呢?笔者感觉有以下几点:
-
公有函数,或者用正式点的名称“不可导出”函数 / 办法。这种状况下,构造体的安全性齐全在以后 package 外部可见,那么由开发者本人就能够确保读写平安。这个时候,不强制应用援用传递。
- 因而从下一条开始,探讨的都是“可导出”的函数 / 办法
-
这个 struct 切实是太大了,并且该函数频繁调用。如果应用值传递,会重大影响性能
- 然而如果命中了这条规定,那么开发者要思考这样的一个问题:定义一个如此宏大的构造体,是否有必要?
- 作为出 / 入参,这个构造体类型的
nil
值是有明确含意的 - 相干构造体类型的典型应用办法就是援用传递,比方通过 protobuf 定义并生成的 RPC 参数类型
- 其余约定俗成规定——其实第 4 条也能够算是约定俗成规定之一
针对值 / 指针,还有另外一个话题,就是作为办法接收器类型的抉择。Google 有一个专门的局部解释这个:Should I define methods on values or pointers。有机会笔者也能够写一篇开展讲讲。
本文章采纳 常识共享署名 - 非商业性应用 - 雷同形式共享 4.0 国内许可协定 进行许可。
原作者:amc,原文公布于云 + 社区,也是自己的博客。欢送转载,但请注明出处。
原作者:amc,欢送转载,但请注明出处。
原文题目:《每天学点 Go 标准 – 函数传参时,struct 应该传值还是援用》
公布日期:2023-08-16
原文链接:https://segmentfault.com/a/1190000044151865。