【问题标题】:How to slice a map[string]int into chunks如何将 map[string]int 切片成块
【发布时间】:2020-01-12 05:56:44
【问题描述】:

我的目标是获取一个包含可能多达一百万个条目的map[string]int,并将其分块为最大 500 的大小,并将地图发布到外部服务。我是 golang 的新手,所以我现在正在 Go Playground 中修修补补。

任何关于如何提高我的代码库效率的技巧,请分享!

游乐场:https://play.golang.org/p/eJ4_Pd9X91c

我看到的 CLI 输出是:

original size 60
chunk bookends 0 20
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
chunk bookends 20 40
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,
chunk bookends 40 60
0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,

这里的问题是,当块书挡被正确计算时,x 的值每次都从 0 开始。我想我应该期望它从块书挡最小值开始,即 0、20、40 等。范围为什么每次都从零开始?

来源:

package main

import (
    "fmt"
    "math/rand"
    "strconv"
)

func main() {
    items := make(map[string]int)

    // Generate some fake data for our testing, in reality this could be 1m entries
    for i := 0; i < 60; i ++ {
        // int as strings are intentional here
        items[strconv.FormatInt(int64(rand.Int()), 10)] = rand.Int()
    }

    // Create a map of just keys so we can easily chunk based on the numeric keys
    i := 0
    keys := make([]string, len(items))
    for k := range items {
            keys[i] = k
            i++
    }

    fmt.Println("original size", len(keys))
    //batchContents := make(map[string]int)

    // Iterate numbers in the size batch we're looking for
    chunkSize := 20
    for chunkStart := 0; chunkStart < len(keys); chunkStart += chunkSize {
        chunkEnd := chunkStart + chunkSize

        if chunkEnd > len(items) {  
            chunkEnd = len(items)
        }

        // Iterate over the keys
        fmt.Println("chunk bookends", chunkStart, chunkEnd)
        for x := range keys[chunkStart:chunkEnd] {
            fmt.Print(x, ",")

            // Build the batch contents with the contents needed from items
            // @todo is there a more efficient approach?
            //batchContents[keys[i]] = items[keys[i]]
        }
        fmt.Println()

        // @todo POST final batch contents
        //fmt.Println(batchContents)
    }

}

【问题讨论】:

    标签: dictionary go slice


    【解决方案1】:

    当你处理一个块时:

    for x := range keys[chunkStart:chunkEnd] {}
    

    您正在迭代一个切片,并且有一个迭代变量,它将是切片索引,而不是切片中的元素(在给定索引处)。因此它将始终从0 开始。 (当你迭代一个映射时,第一个迭代变量是键,因为那里没有索引,第二个是与该键关联的值。)

    你想要的是这个:

    for _, key := range keys[chunkStart:chunkEnd] {}
    

    另外请注意,首先收集切片中的键,然后处理它们是多余的。起初,您可以在遍历地图一次时这样做。只需保留一个变量来计算迭代次数,以了解何时达到块大小,如果您使用保持此值的数据结构(例如键批处理切片的大小),这可能是隐含的。

    例如(在Go Playground 上试试):

    chunkSize := 20
    batchKeys := make([]string, 0, chunkSize)
    process := func() {
        fmt.Println("Batch keys:", batchKeys)
        batchKeys = batchKeys[:0]
    }
    
    for k := range items {
        batchKeys = append(batchKeys, k)
        if len(batchKeys) == chunkSize {
            process()
        }
    }
    // Process last, potentially incomplete batch
    if len(batchKeys) > 0 {
        process()
    }
    

    【讨论】:

    • :facepalm: 是的,for _, key 完全有道理。感谢您指出了这一点。我也非常感谢这种更有效的方法。所以[:0]基本上是不选择batch key,从而返回一个空map,有效清除?
    • 额外问题:如果 for k := range items { 需要 30 秒来处理这些项目,但在这 30 秒内,items 的长度会发生变化,并且 int 的某些值也会发生变化,是否会会遇到错误?我很好,不接受这些更新...
    • @Webnet 批处理密钥收集在一个名为 batchKeys 的 slice 中。 batchKeys = batchKeys[:0] 表示重新切片此切片,新长度为0。这具有“清除”切片的效果(丢弃前一批的键),并具有保留后备数组的好处,因此可以重复使用,无需重新分配,不会产生垃圾。
    • @Webnet Bonus 回答:如果您在迭代时向地图添加条目,这不是问题,但新元素可能会或可能不会被循环访问,请参阅How to add into map in range loop。跨度>
    • ... 如果您需要在另一个 goroutine 中的映射中添加元素,同时迭代它,请参阅这个问题如何应用正确的同步:Concurrent access to maps with 'range' in Go。虽然最简单的方法是在迭代时阻止修改(但如果循环需要很长时间,这对你来说可能是个问题)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-15
    • 1970-01-01
    • 2023-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多