当前位置:首页 > 文章列表 > Golang > Go问答 > Go 中的垃圾收集和指针的正确使用

Go 中的垃圾收集和指针的正确使用

来源:Golang技术栈 2023-04-15 22:25:41 0浏览 收藏

大家好,今天本人给大家带来文章《Go 中的垃圾收集和指针的正确使用》,文中内容主要涉及到golang,如果你对Golang方面的知识点感兴趣,那就请各位朋友继续看下去吧~希望能真正帮到你们,谢谢!

问题内容

我来自 Python/Ruby/JavaScript 背景。我了解指针的工作原理,但是,我不完全确定如何在以下情况下利用它们。

假设我们有一个虚构的 Web API,它搜索一些图像数据库并返回一个 JSON 描述在找到的每个图像中显示的内容:

[
    {
        "url": "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
        "description": "Ocean islands",
        "tags": [
            {"name":"ocean", "rank":1},
            {"name":"water", "rank":2},
            {"name":"blue", "rank":3},
            {"name":"forest", "rank":4}
        ]
    },

    ...

    {
        "url": "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg",
        "description": "Bridge over river",
        "tags": [
            {"name":"bridge", "rank":1},
            {"name":"river", "rank":2},
            {"name":"water", "rank":3},
            {"name":"forest", "rank":4}
        ]
    }
]

我的目标是在 Go 中创建一个数据结构,它将每个标签映射到一个图像 URL 列表,如下所示:

{
    "ocean": [
        "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
    ],
    "water": [
        "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
        "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
    ],
    "blue": [
        "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
    ],
    "forest":[
        "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg", 
        "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
    ],
    "bridge": [
        "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
    ],
    "river":[
        "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
    ]
}

如您所见,每个图像 URL 可以同时属于多个标签。如果我有数千张图像甚至更多标签,如果图像 URL 字符串按每个标签的值复制,则此数据结构可能会变得非常大。这是我想利用指针的地方。

我可以用 Go 中的两个结构来表示 JSON API 响应,func searchImages()模仿假 API:

package main

import "fmt"


type Image struct {
    URL string
    Description string
    Tags []*Tag
}

type Tag struct {
    Name string
    Rank int
}

// this function mimics json.NewDecoder(resp.Body).Decode(&parsedJSON)
func searchImages() []*Image {
    parsedJSON := []*Image{
        &Image {
            URL: "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
            Description: "Ocean islands",
            Tags: []*Tag{
                &Tag{"ocean", 1},
                &Tag{"water", 2},
                &Tag{"blue", 3},
                &Tag{"forest", 4},
            }, 
        },
        &Image {
            URL: "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg",
            Description: "Bridge over river",
            Tags: []*Tag{
                &Tag{"bridge", 1},
                &Tag{"river", 2},
                &Tag{"water", 3},
                &Tag{"forest", 4},
            }, 
        },
    }
    return parsedJSON
}

现在,导致内存中数据结构非常大的次优映射函数可能如下所示:

func main() {
    result := searchImages()

    tagToUrlMap := make(map[string][]string)

    for _, image := range result {
        for _, tag := range image.Tags {
            // fmt.Println(image.URL, tag.Name)
            tagToUrlMap[tag.Name] = append(tagToUrlMap[tag.Name], image.URL)
        }
    }

    fmt.Println(tagToUrlMap)
}

我可以修改它以使用指向Image结构URL字段的指针,而不是按值复制它:

    // Version 1

    tagToUrlMap := make(map[string][]*string)

    for _, image := range result {
        for _, tag := range image.Tags {
            // fmt.Println(image.URL, tag.Name)
            tagToUrlMap[tag.Name] = append(tagToUrlMap[tag.Name], &image.URL)
        }
    }

它有效,我的第一个问题是result在我以这种方式构建映射后数据结构会发生什么?字符串字段会Image URL以某种方式留在内存中,其余的result将被垃圾收集吗?或者result数据结构是否会因为某些东西指向它的成员而保留在内存中直到程序结束?

另一种方法是将 URL 复制到中间变量并使用指向它的指针:

    // Version 2

    tagToUrlMap := make(map[string][]*string)

    for _, image := range result {
        imageUrl = image.URL
        for _, tag := range image.Tags {
            // fmt.Println(image.URL, tag.Name)    
            tagToUrlMap[tag.Name] = append(tagToUrlMap[tag.Name], &imageUrl)
        }
    }

这是否更好?数据结构会result被正确地垃圾收集吗?

或者也许我应该在结构中使用指向字符串的指针Image

type Image struct {
    URL *string
    Description string
    Tags []*Tag
}

有一个更好的方法吗?我也很感激 Go 上的任何资源,这些资源深入描述了指针的各种用途。谢谢!

https://play.golang.org/p/VcKWUYLIpH7

更新: 我担心最佳内存消耗并且不会产生最多不需要的垃圾。我的目标是尽可能使用最少的内存。

正确答案

前言: 我在我的github.com/icza/gox库中发布了提供的字符串池,请参阅stringsx.Pool.


首先是一些背景。stringGo 中的值由一个类似结构的小型数据结构表示reflect.StringHeader

type StringHeader struct {
        Data uintptr
        Len  int
}

所以基本上传递/复制一个string值会传递/复制这个小的结构值,无论string. 在 64 位架构上,它只有 16 个字节,即使它string有一千个字符。

所以基本上string值已经充当指针。引入另一个指针*string只会使使用复杂化,并且您不会真正获得任何显着的内存。为了内存优化,忘记使用*string.

它有效,我的第一个问题是,在我以这种方式构建映射后,结果数据结构会发生什么?图像 URL 字符串字段是否会以某种方式留在内存中,而其余结果将被垃圾收集?或者结果数据结构是否会因为某些东西指向它的成员而保留在内存中直到程序结束?

如果你有一个指针值指向一个结构值的一个字段,那么整个结构将被保存在内存中,它不能被垃圾收集。请注意,虽然可以释放为结构的其他字段保留的内存,但当前的 Go 运行时和垃圾收集器并没有这样做。因此,为了实现最佳内存使用,您应该忘记存储结构字段的地址(除非您还需要完整的结构值,但仍然需要小心存储字段地址和切片/数组元素地址)。

这样做的原因是因为结构值的内存被分配为一个连续的段,因此只保留一个引用的字段会强烈地分割可用/空闲内存,并且会使优化内存管理更加困难和效率更低。对这些区域进行碎片整理还需要复制引用字段的内存区域,这将需要“实时更改”指针值(更改内存地址)。

因此,虽然使用指向string值的指针可能会为您节省一些微小的内存,但增加的复杂性和额外的间接性使其不值得。

那么该怎么办呢?

“最佳”解决方案

所以最干净的方法是继续使用string值。

还有一个我们之前没有谈到的优化。

您可以通过解组 JSON API 响应来获得结果。这意味着如果在 JSON 响应中多次包含相同的 URL 或标记值,string将为它们创建不同的值。

这是什么意思?如果您在 JSON 响应中有两次相同的 URL,则在解组后,您将有 2 个不同的string值,其中包含 2 个不同的指针,指向 2 个不同的分配字节序列(字符串内容,否则将是相同的)。包encoding/json不做 string 实习

这是一个证明这一点的小应用程序:

var s []string
err := json.Unmarshal([]byte(`["abc", "abc", "abc"]`), &s)
if err != nil {
    panic(err)
}

for i := range s {
    hdr := (*reflect.StringHeader)(unsafe.Pointer(&s[i]))
    fmt.Println(hdr.Data)
}

上面的输出(在Go Playground上试试):

273760312
273760315
273760320

我们看到 3 个不同的指针。它们可能相同,因为string值是不可变的。

json包不会检测重复string值,因为检测会增加内存和计算开销,这显然是不需要的。但是在我们的例子中,我们追求最佳的内存使用,因此“初始”的额外计算确实值得大量的内存增益。

所以让我们做我们自己的字符串实习。怎么做?

在解组 JSON 结果之后,在构建tagToUrlMap地图期间,让我们跟踪string我们遇到的值,如果string之前已经看到后续值,则使用之前的值(它的字符串描述符)。

这是一个非常简单的字符串内部实现:

var cache = map[string]string{}

func interned(s string) string {
    if s2, ok := cache[s]; ok {
        return s2
    }
    // New string, store it
    cache[s] = s
    return s
}

让我们在上面的示例代码中测试这个“内部”:

var s []string
err := json.Unmarshal([]byte(`["abc", "abc", "abc"]`), &s)
if err != nil {
    panic(err)
}

for i := range s {
    hdr := (*reflect.StringHeader)(unsafe.Pointer(&s[i]))
    fmt.Println(hdr.Data, s[i])
}

for i := range s {
    s[i] = interned(s[i])
}

for i := range s {
    hdr := (*reflect.StringHeader)(unsafe.Pointer(&s[i]))
    fmt.Println(hdr.Data, s[i])
}

上面的输出(在Go Playground上试试):

273760312 abc
273760315 abc
273760320 abc
273760312 abc
273760312 abc
273760312 abc

精彩的!正如我们所看到的,在使用我们的interned()函数之后,我们的数据结构中只使用了字符串的一个实例"abc"(实际上是第一次出现)。这意味着所有其他实例(假设没有其他人使用它们)可以被“正确地”垃圾收集(由垃圾收集器,在未来的某个时间)。

这里不要忘记一件事:字符串内部使用一个cache字典来存储所有以前遇到的字符串值。因此,要让这些字符串消失,您也应该“清除”这个缓存映射,最简单的方法是为其分配一个nil值。

事不宜迟,让我们看看我们的解决方案:

result := searchImages()

tagToUrlMap := make(map[string][]string)

for _, image := range result {
    imageURL := interned(image.URL)

    for _, tag := range image.Tags {
        tagName := interned(tag.Name)
        tagToUrlMap[tagName] = append(tagToUrlMap[tagName], imageURL)
    }
}

// Clear the interner cache:
cache = nil

要验证结果:

enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", "  ")
if err := enc.Encode(tagToUrlMap); err != nil {
    panic(err)
}

输出是(在Go Playground上试试):

{
  "blue": [
    "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
  ],
  "bridge": [
    "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
  ],
  "forest": [
    "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
    "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
  ],
  "ocean": [
    "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg"
  ],
  "river": [
    "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
  ],
  "water": [
    "https://c8.staticflickr.com/4/3707/11603200203_87810ddb43_o.jpg",
    "https://c3.staticflickr.com/1/48/164626048_edeca27ed7_o.jpg"
  ]
}

进一步的内存优化:

我们使用内置append()函数将新的图像 URL 添加到标签。append()可能(并且通常确实)分配比需要更大的切片(考虑未来的增长)。在我们的“构建”过程之后,我们可能会遍历我们的tagToUrlMap地图并将这些切片“修剪”到所需的最小值。

这是可以做到的:

for tagName, urls := range tagToUrlMap {
    if cap(urls) > len(urls) {
        urls2 := make([]string, len(urls))
        copy(urls2, urls)
        tagToUrlMap[tagName] = urls2
    }
}

以上就是《Go 中的垃圾收集和指针的正确使用》的详细内容,更多关于golang的资料请关注golang学习网公众号!

版本声明
本文转载于:Golang技术栈 如有侵犯,请联系study_golang@163.com删除
在 Go 中使用匿名成员扁平化编组的 JSON 结构在 Go 中使用匿名成员扁平化编组的 JSON 结构
上一篇
在 Go 中使用匿名成员扁平化编组的 JSON 结构
将 VueJS 与 Go 后端一起使用
下一篇
将 VueJS 与 Go 后端一起使用
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    542次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    508次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    497次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • AI Make Song:零门槛AI音乐创作平台,助你轻松制作个性化音乐
    AI Make Song
    AI Make Song是一款革命性的AI音乐生成平台,提供文本和歌词转音乐的双模式输入,支持多语言及商业友好版权体系。无论你是音乐爱好者、内容创作者还是广告从业者,都能在这里实现“用文字创造音乐”的梦想。平台已生成超百万首原创音乐,覆盖全球20个国家,用户满意度高达95%。
    17次使用
  • SongGenerator.io:零门槛AI音乐生成器,快速创作高质量音乐
    SongGenerator
    探索SongGenerator.io,零门槛、全免费的AI音乐生成器。无需注册,通过简单文本输入即可生成多风格音乐,适用于内容创作者、音乐爱好者和教育工作者。日均生成量超10万次,全球50国家用户信赖。
    13次使用
  •  BeArt AI换脸:免费在线工具,轻松实现照片、视频、GIF换脸
    BeArt AI换脸
    探索BeArt AI换脸工具,免费在线使用,无需下载软件,即可对照片、视频和GIF进行高质量换脸。体验快速、流畅、无水印的换脸效果,适用于娱乐创作、影视制作、广告营销等多种场景。
    13次使用
  • SEO标题协启动:AI驱动的智能对话与内容生成平台 - 提升创作效率
    协启动
    SEO摘要协启动(XieQiDong Chatbot)是由深圳协启动传媒有限公司运营的AI智能服务平台,提供多模型支持的对话服务、文档处理和图像生成工具,旨在提升用户内容创作与信息处理效率。平台支持订阅制付费,适合个人及企业用户,满足日常聊天、文案生成、学习辅助等需求。
    16次使用
  • Brev AI:零注册门槛的全功能免费AI音乐创作平台
    Brev AI
    探索Brev AI,一个无需注册即可免费使用的AI音乐创作平台,提供多功能工具如音乐生成、去人声、歌词创作等,适用于内容创作、商业配乐和个人创作,满足您的音乐需求。
    18次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码