乐趣区

关于语言:每天学点-Go-规范-函数传参时struct-应该传值还是引用

当初团队里简直所有的代码都须要通过 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。

退出移动版