V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
dyllen
V2EX  ›  Go 编程语言

go 返回值是否需要尽量返回值,而不返回指针呢?

  •  
  •   dyllen · 2023-12-27 23:25:54 +08:00 · 3490 次点击
    这是一个创建于 366 天前的主题,其中的信息可能已经有所发展或是发生改变。

    理由如下:

    根据内存逃逸的规则,把函数局部变量的指针返回会逃逸,结果导致 gc 压力变大。

    所以是否需要尽量返回值,而不返回指针呢?

    比如查询一个数据模型返回它,还有其他许多情况。

    第 1 条附言  ·  2023-12-28 10:46:30 +08:00
    纯讨论一个点,不加入其他什么没必要,性能没影响什么的类似的额外影响话题。
    第 2 条附言  ·  2023-12-28 10:49:06 +08:00
    比如有些 orm 库的数据库查询,都不是直接返回结果 model 的指针,而是传指针进去,数据填充到里面。
    30 条回复    2023-12-28 13:52:01 +08:00
    nagisaushio
        1
    nagisaushio  
       2023-12-27 23:35:11 +08:00
    小类型尽量返回值,大类型看情况
    lianyue
        2
    lianyue  
       2023-12-27 23:48:31 +08:00
    我是 结构体 指针否 值
    rekulas
        3
    rekulas  
       2023-12-27 23:48:34 +08:00
    对看情况 有些情况下返回值可能压力更大
    Maboroshii
        4
    Maboroshii  
       2023-12-27 23:52:01 +08:00 via Android
    我写 go 基本都有指针,除非是明确返回不可修改的值才用值返回。写 web 应用 gc 压力不存在的,pprof 看下,全耗在序列化上了
    BeautifulSoap
        5
    BeautifulSoap  
       2023-12-28 00:44:14 +08:00 via Android   ❤️ 3
    不要过早地进行优化
    绝大部分项目的性能要求还没高到需要抠这种细节的地步
    e7
        6
    e7  
       2023-12-28 09:27:04 +08:00
    八股就是这么衍生出来的吗。。。大概按 1L 说的就能避免 80%的你认为的不合理,然后真的哪天出现了性能问题再去 pprof ,而且大概率不会是这个原因
    body007
        7
    body007  
       2023-12-28 09:27:57 +08:00   ❤️ 5
    结构体统统用指针,普通类型用值。因为结构体你不知道啥时候加需求往里面加大类型,而且结构体一般会到处传,如果所有地方都用值传递,每次赋值都内存拷贝一份也是有开销的,指针的话只拷贝指针地址更快。

    另外 https://www.cnblogs.com/janbar/p/17072751.html 这篇文章探讨了直接赋值的深拷贝问题,即使你值传递,结构体内部有指针,那么这些指针在赋值时也是赋值指针地址。如果用值值类型,你到时候还得思考结构体内部哪些字段是深拷贝,哪些是浅拷贝。

    http 提供下面方式克隆对象,就是因为值传递内部字段也存在浅拷贝问题,需要编写深拷贝代码。与其值传递增加心智负担,还不如无脑指针传递。
    func (r *Request) Clone(ctx context.Context) *Request {

    综上所述,我觉得结构体一律用指针。

    我在用的 [go-zero]( https://github.com/zeromicro/go-zero/pull/1211/files#diff-a650192c5b74f391823e44c0b326c07abe5c2544ab386b1ce73ce6b293d78a4c) 框架,在这次改动中将参数值传递改为指针传递,导致我某次升级改了好多文件代码,连大佬都觉得结构体指针传递好些吧。


    如果你明确的知道你需要值传递,并且清楚这个对象赋值后内部字段存在浅拷贝也不会影响逻辑,那么可以用值传递。
    veightz
        8
    veightz  
       2023-12-28 09:41:11 +08:00 via Android
    https://go.dev/wiki/CodeReviewComments#pass-values

    有时候也不要只看内存尺寸,而忽视了栈性能远快于堆性能,以及用指针带来了内存逃逸开销,是需要综合考虑的。
    veightz
        9
    veightz  
       2023-12-28 09:44:03 +08:00 via Android
    在写代码的时候,尽量把方法往 值传递优化 和 不可变数据 去设计和实现,对象之海迟早会反噬的…
    vultr
        10
    vultr  
       2023-12-28 09:45:26 +08:00
    我也认为应该尽量用指针,如果为了减少 GC ,可以用 sync.Pool 重用它。
    cheng6563
        11
    cheng6563  
       2023-12-28 09:46:07 +08:00
    @body007 chan 呢
    8355
        12
    8355  
       2023-12-28 09:53:41 +08:00
    我的理解是以业务逻辑为核心,99%的业务返回值,而 1%的业务是很明显需要指针的,对于不能完全理解以上描述的开发者先返回值是没问题的。
    body007
        13
    body007  
       2023-12-28 10:00:32 +08:00
    @cheng6563 你得了解 go 的引用类型,chan 是引用类型,直接返回就是引用。作为返回值,引用类型本身类似指针,返回的就是引用。作为参数在引用类型加指针貌似只有需要修改引用类型的时候才用到吧。
    MoYi123
        14
    MoYi123  
       2023-12-28 10:24:18 +08:00
    是的, 对于降低 p99 有明显的效果
    Ryans
        15
    Ryans  
       2023-12-28 10:27:13 +08:00
    小型数据结构:如果是小型结构或不频繁修改的数据,优先返回值以减少 GC 压力。
    大型或复杂对象:对于大型或需要频繁修改的对象,考虑返回指针以避免大量数据复制。
    keakon
        16
    keakon  
       2023-12-28 10:29:35 +08:00
    之前看过这篇,性能上大部分情况下是返回 struct 更快:
    https://cloud.tencent.com/developer/article/1861199

    不过如果要和 nil 区分,或者最终要放到堆里,那就继续用指针。

    然后如果是参数的话,记得是超过 32 字节传指针更快,而且每个字段是单独用一个 MOV 指令来复制的,不是一整块复制的。
    chenchengbin
        17
    chenchengbin  
       2023-12-28 10:45:38 +08:00
    虽然返回指针会导致逃逸,但是存在即合理
    dyllen
        18
    dyllen  
    OP
       2023-12-28 10:45:40 +08:00
    @Maboroshii
    @BeautifulSoap
    纯讨论一个点,不加入其他无压力,性能没影响什么的类似的额外影响话题。
    chenchengbin
        19
    chenchengbin  
       2023-12-28 10:47:56 +08:00
    @veightz 字符串直接传不会因为传值复制导致内存激增吗,我的理解会复制一个相同大小的字符串
    dyllen
        20
    dyllen  
    OP
       2023-12-28 10:48:49 +08:00
    比如有些 orm 库的数据库查询,都不是直接返回结果 model 的指针,而是传指针进去,数据填充到里面。
    dyllen
        21
    dyllen  
    OP
       2023-12-28 10:49:51 +08:00
    @dyllen 有些又是直接返回结果 model 的指针。
    keakon
        22
    keakon  
       2023-12-28 10:51:05 +08:00
    @chenchengbin 字符串是浅拷贝 StringHeader
    thinkershare
        23
    thinkershare  
       2023-12-28 10:51:18 +08:00
    @chenchengbin 当然不会,golang 的字符串本质上就不是一个纯值类型。
    Maboroshii
        24
    Maboroshii  
       2023-12-28 10:52:14 +08:00 via Android
    orm 一般用到反射,反射会引起逃逸,所以直接用指针了
    @dyllen
    Maboroshii
        25
    Maboroshii  
       2023-12-28 10:58:06 +08:00
    orm 用参数当返回值的另一个点,就是需要调用方来控制这段内存的申请和销毁(在 go 里,你可以用 sync.Pool 来优化)。 也是 C/C++里面的原则吧,谁声明,谁处理, 调用方 malloc 了 ,调用方来 free ,方法内部是不会帮你初始化堆上的内存的。
    chenchengbin
        26
    chenchengbin  
       2023-12-28 10:59:46 +08:00
    @Maboroshii 都在 orm 上了各种反射
    flmn
        27
    flmn  
       2023-12-28 11:32:25 +08:00
    尽量用值吧,编译器会帮你优化的
    ZSeptember
        28
    ZSeptember  
       2023-12-28 11:38:38 +08:00
    尽量用值,除非你的业务量真的到需要用这种细节来优化了,不过真的到时候也是应该先 bench 一下。
    我们的业务代码基本不用指针,入参,出参都不用,推荐方法无状态,副作用可控,容易测试。
    szzhiyang
        29
    szzhiyang  
       2023-12-28 12:03:48 +08:00
    @chenchengbin 字符串(数组)的内部实现: https://go.dev/blog/slices
    chenchengbin
        30
    chenchengbin  
       2023-12-28 13:52:01 +08:00
    @szzhiyang 擦,一直以为 go 的 string 是复制一个字符串,我知道内部实现是这玩意, 但是也没深入看过, 学习了学习了。。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2876 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 08:29 · PVG 16:29 · LAX 00:29 · JFK 03:29
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.