头图

现在团队里几乎所有的代码都需要经过 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 内部的逻辑就有可能是:

  1. 如果内存缓存中数据命中,那么返回缓存数据
  2. 如果缓存数据未命中,则 RPC 搜索,得到数据后缓存到内存中

GetUserPermissions 返回的是一个引用,那么它或许返回的是它在内存缓存中的引用。那么在 CopyUserPermissions 中修改了引用的内容,那么下一次请求 fromUserID 的数据信息时,内存缓存启示已经被篡改,数据不一致了,bug 就这么产生。

解决方法

解决方法很简单,将 GetUserPermissionsSetUserPermissions 的出入参 UserPermission,从引用类型改为值类型,也就是去掉 * 指针。即便是内部存储用的是 *,也完全可以用 Go 自带的值语法将数据 (浅) 复制出去。

入参和出参都需要改一下:

// GetUserPermissions 获取指定 user ID 的权限
func GetUserPermissions(userID string) UserPermission

// SetUserPermissions 设置指定 user ID 的权限
func SetUserPermissions(permission UserPermission) error

使用值传递的优点

使用值的优点,笔者这里简单总结一下吧:

  1. 前文提到的,值传递针对原始值多了一次复制动作。作为入参,可以说是起到了类似于 C++ 中 const 参数的部分作用,避免了使用该参数的逻辑,修改参数而导致数据作用域溢出。
  2. 引用是指针类型,有可能为 nil。值传递相当于做了一个默认的声明,向使用方默认提供了一个承诺:这个变量永远是可用的,不会也不需要判断 nil 的问题。

什么时候应该使用引用传递

当然了,其实很多情况下,使用引用传递的还是很多。这一条规范的存在意义是:代码设计开发的时候,要时刻注意逻辑的细节。所以说这条规范,说的是 “非必要”。那么什么情况是必要的呢?笔者觉得有以下几点:

  1. 私有函数,或者用正式点的名称 “不可导出” 函数 / 方法。这种情况下,结构体的安全性完全在当前 package 内部可见,那么由开发者自己就可以确保读写安全。这个时候,不强制使用引用传递。

    • 因此从下一条开始,讨论的都是 “可导出” 的函数 / 方法
  2. 这个 struct 实在是太大了,并且该函数频繁调用。如果使用值传递,会严重影响性能

    • 但是如果命中了这条规则,那么开发者要考虑这样的一个问题:定义一个如此庞大的结构体,是否有必要?
  3. 作为出/入参,这个结构体类型的 nil 值是有明确含义的
  4. 相关结构体类型的典型使用方法就是引用传递,比如通过 protobuf 定义并生成的 RPC 参数类型
  5. 其他约定俗成规则——其实第4条也可以算是约定俗成规则之一

针对值 / 指针,还有另外一个话题,就是作为方法接收器类型的选择。Google 有一个专门的部分解释这个:Should I define methods on values or pointers。有机会笔者也可以写一篇展开讲讲。


本文章采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。

原作者: amc,原文发布于云+社区,也是本人的博客。欢迎转载,但请注明出处。

原作者: amc,欢迎转载,但请注明出处。

原文标题:《每天学点 Go 规范 - 函数传参时,struct 应该传值还是引用》

发布日期:2023-08-16

原文链接:https://segmentfault.com/a/1190000044151865


amc
924 声望223 粉丝

电子和互联网深耕多年,拥有丰富的嵌入式和服务器开发经验。现负责腾讯心悦俱乐部后台开发