前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >每天学点 Go 规范 - 函数传参时,struct 应该传值还是引用

每天学点 Go 规范 - 函数传参时,struct 应该传值还是引用

原创
作者头像
amc
修改2023-08-26 16:28:18
4510
修改2023-08-26 16:28:18
举报
文章被收录于专栏:后台全栈之路后台全栈之路

现在团队里几乎所有的代码都需要经过 Code Review(代码审查)之后才允许合入主分支。笔者在 CR 中看到了不少不适合的问题,也看到了不少值得学习的点,于是决定一点一滴地记录这些做法、经验、教训,以飨读者。如有错误,也欢迎读者不吝指正。

上一篇文章:context 类型的 key 有什么讲究?

一句话规范

  • 当函数的入参、出参是一个结构体时,如无必要,使用值传递而不是引用传递

问题背景

当我们用 Go 开发时,对外暴露一个函数 / 方法时,以结构体作为函数的入参或出参,是非常常见的。比如说,我们实现下面的一个函数,返回一个用户信息。

比如说,我们提供两个函数,分别用来获取相关用户的权限信息:

代码语言: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 作为出入参都是以指针存在的。这种模式的代码非常多,也非常典型,而且大家都会习惯于这么写,特别是有面向对象思路的程序员。

那么,这么写可以吗?有什么问题呢?其实这个要具体问题具体分析,下面我们就来一起看一看。

可能存在问题

假设有一个新需求,是复制一个用户的权限给新用户。这逻辑看起来挺简单,代码里这么写,完全是合情合理的:

代码语言:go
复制
// 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 自带的值语法将数据 (浅) 复制出去。

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

代码语言: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,欢迎转载,但请注明出处。

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

发布日期:2023-08-25

原文链接:https://cloud.tencent.com/developer/article/2317953

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一句话规范
  • 问题背景
  • 可能存在问题
  • 解决方法
  • 使用值传递的优点
  • 什么时候应该使用引用传递
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档