在Go中创建2D切片的简洁方法是什么?


103

我正在通过“围棋之旅”学习围棋。其中的一项练习要求我创建包含的dy行和dx列的2D切片uint8。我目前有效的方法是:

a:= make([][]uint8, dy)       // initialize a slice of dy slices
for i:=0;i<dy;i++ {
    a[i] = make([]uint8, dx)  // initialize a slice of dx unit8 in each of dy slices
}

我认为遍历每个切片以初始化它太冗长。而且,如果切片具有更大的尺寸,则代码将变得笨拙。在Go中,有没有一种简便的方法来初始化2D(或n维)切片?

Answers:


148

没有一种更简洁的方法,您所做的就是“正确”的方法。因为切片始终是一维的,但可以组成更高维度的对象。有关更多详细信息,请参见此问题:Go:二维数组的内存表示如何

您可以简化的一件事是使用for range构造:

a := make([][]uint8, dy)
for i := range a {
    a[i] = make([]uint8, dx)
}

另请注意,如果您使用复合文字初始化切片,则会获得“免费”,例如:

a := [][]uint8{
    {0, 1, 2, 3},
    {4, 5, 6, 7},
}
fmt.Println(a) // Output is [[0 1 2 3] [4 5 6 7]]

是的,这似乎有其局限性,因为您似乎必须枚举所有元素。但是有一些技巧,即您不必枚举所有值,只需枚举不是切片元素类型的零值的那些值即可。有关此的更多详细信息,请参阅golang数组初始化中的键控项

例如,如果您想要一个前10个元素为零的切片,然后跟随12,则可以这样创建切片:

b := []uint{10: 1, 2}
fmt.Println(b) // Prints [0 0 0 0 0 0 0 0 0 0 1 2]

还要注意,如果使用数组而不是slice,则可以很容易地创建它:

c := [5][5]uint8{}
fmt.Println(c)

输出为:

[[0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0] [0 0 0 0 0]]

如果是数组,则不必迭代“外部”数组并初始化“内部”数组,因为数组不是描述符而是值。请参阅博客文章数组,切片(和字符串):有关更多详细信息请参见“追加”的机制。

Go Playground上尝试示例。


由于使用数组简化了代码,因此我想这样做。如何在结构中指定呢?cannot use [5][2]string literal (type [5][2]string) as type [][]string in field value当我尝试将数组分配给我想告诉Go的是切片时,我得到了。
埃里克·林赛

我自己弄清楚了,然后编辑答案以添加信息。
埃里克·林赛

1
@EricLindsey虽然您的编辑很好,但我仍然会拒绝它,因为我不想仅仅因为初始化更容易而鼓励使用数组。在Go中,数组是次要的,而切片则是方法。有关详细信息,请参阅在Go中将一个数组追加到另一个数组的最快方法是什么?数组也有自己的位置,有关详细信息,请参阅为什么Go中有数组?
icza

足够公平,但是我相信这些信息仍然值得。我试图通过编辑来解释的是,如果您需要对象之间不同尺寸的灵活性,那么切片是可行的方法。另一方面,如果您的信息是严格结构化的并且将始终相同,那么数组不仅易于初始化,而且效率更高。我该如何改善编辑?
埃里克·林赛

@EricLindsey我看到您进行了另一项编辑,但该编辑已被其他人拒绝。在编辑中,您说要使用数组来更快地访问元素。请注意,Go可以优化许多功能,但事实并非如此,切片速度可能一样快。有关详细信息,请参见数组与切片:访问速度
icza,

12

有两种使用切片创建矩阵的方法。让我们看看它们之间的区别。

第一种方法:

matrix := make([][]int, n)
for i := 0; i < n; i++ {
    matrix[i] = make([]int, m)
}

第二种方法:

matrix := make([][]int, n)
rows := make([]int, n*m)
for i := 0; i < n; i++ {
    matrix[i] = rows[i*m : (i+1)*m]
}

关于第一种方法,进行连续make调用并不能确保您最终得到一个连续的矩阵,因此可能会将矩阵划分到内存中。让我们考虑一个带有两个Go例程的示例,这可能会导致这种情况:

  1. 例程#0运行make([][]int, n)以获取分配的内存matrix,从0x000到0x07F获取一块内存。
  2. 然后,它开始循环并执行第一行make([]int, m),从0x080到0x0FF。
  3. 在第二次迭代中,它被调度程序抢占。
  4. 调度程序使处理器进入例程#1,然后开始运行。这也使用make(出于自己的目的),并且从0x100到0x17F(紧接例程#0的第一行)。
  5. 片刻之后,它被抢占,例程#0重新开始运行。
  6. make([]int, m)与第二个循环迭代相对应,第二行从0x180到0x1FF。至此,我们已经得到了两行。

使用第二种方法,例程可以make([]int, n*m)确保在单个切片中分配所有矩阵,从而确保连续性。之后,需要一个循环来更新指向与每一行相对应的子片段的矩阵指针。

您可以在Go Playground中使用上面显示的代码来查看使用这两种方法分配的内存的差异。请注意,我runtime.Gosched()仅用于产生处理器并强制调度程序切换到另一个例程的目的。

使用哪一个?想象一下第一种方法的最坏情况,即,每一行在内存中都不是另一行。然后,如果您的程序遍历矩阵元素(以读取或写入它们),则由于第二种方法的数据局部性较差,与第二种方法相比,可能会有更多的缓存未命中(因此延迟时间更长)。另一方面,使用第二种方法,由于理论上内存碎片(块散布在整个内存中),即使在理论上可能有足够的可用内存,也可能无法为矩阵分配单个内存。

因此,除非存在大量的内存碎片,并且要分配的矩阵足够大,否则您总是想使用第二种方法来利用数据局部性。


2
golang.org/doc/effective_go.html#slices显示了一种巧妙的方法来利用切片本机语法(例如,无需使用(i + 1)* m这样的表达式显式计算切片边界)来进行连续内存技术
Magnus

0

在先前的答案中,我们没有考虑初始长度未知的情况。对于这种情况,您可以使用以下逻辑来创建矩阵

items := []string{"1.0", "1.0.1", "1.0.2", "1.0.2.1.0"}
mx := make([][]string, 0)
for _, item := range items {
    ind := strings.Count(item, ".")
    for len(mx) < ind+1 {
        mx = append(mx, make([]string, 0))
    }
    mx[ind] = append(mx[ind], item)

}

fmt.Println(mx)

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


1
我不确定这是否在“简洁方式”的OP范围内,因为他说“我认为遍历每个切片以初始化它太冗长”。
Marcos Canales Mayo
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.