ChatGPT 可用网址,仅供交流学习使用,如对您有所帮助,请收藏并推荐给需要的朋友。
https://ckai.xyz
现在团队里几乎所有的代码都需要经过 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