Golang 合并多个 Slices 方法和注意事项

Published: 2022-11-02

Tags: Golang

本文总阅读量

在项目中看到前辈们留下的代码,其中有一段代码如下

package main

import (
  "fmt"
)

func main() {

  invalidA := []string{"1", "2"}
  invalidB := []string{"3", "4"}
  invalidC := []string{"5", "6"}

  // sample code
  invalid := make([]string, 0)
  invalid = append(invalid, invalidA...)
  invalid = append(invalid, invalidB...)
  invalid = append(invalid, invalidC...)

  fmt.Println(invalid)
}

// Output:[1 2 3 4 5 6]

这段代码功能没有问题,但不够优雅,同时会触发多次 Slice 扩容,性能不够好。

在重构代码之前,先看一个合并数组容易忽略的问题。

package main

import (
  "fmt"
)

func main() {

  s1 := make([]int, 2, 5)
  s1[0], s1[1] = 2, 3
  s2 := []int{7, 8}

  s3 := append(s1, s2...)

  fmt.Println(s1, s3)
  s3[0] = -1
  fmt.Println(s1, s3)
  fmt.Println(s1[:4])
}

输出如下

[2 3] [2 3 7 8]
[-1 3] [-1 3 7 8]
[-1 3 7 8]

本例中,如果 s1 的容量够用,那么 append 会修改 s1,然后将 s1 的地址赋值给 s3,对 s3 的修改其实就是在修改 s1

这大概率不符合预期。

如何避免呢?可以通过 Slice 表达式将 s1 的长度和容量修改为 s1 元素的数量。

s3 = append(s1[:len(s1):len(s1)], s2...)

输出如下,可以看到对 s3 的修改已经不会影响到 s1,append 在追加数据时发现 s1 的容量不够,新申请了一块儿内存。

[2 3] [2 3 7 8]
[2 3] [-1 3 7 8]
[2 3 0 0]

回到最初的代码,append 支持可变参数的追加,但并不支持追加多个 Slices

可以封装两个函数,一个函数处理两个 Slices 的合并以及多个 Slices 合并。

concatSlice.go

func concatSlice[T any](first []T, second []T) []T {
    n := len(first)
    return append(first[:n:n], second...)
}

concatMultipleSlices.go

func concatMultipleSlices[T any](slices [][]T) []T {
    var totalLen int

    for _, s := range slices {
        totalLen += len(s)
    }
    result := make([]T, totalLen)

    var i int
    for _, s := range slices {
        i += copy(result[i:], s)
    }
    return result
}

借助封装好的泛型函数,最初的示例可以修改如下,代码结构及性能都有相应提升。

package main

import (
  "fmt"
)

func main() {

  invalidA := []string{"1", "2"}
  invalidB := []string{"3", "4"}
  invalidC := []string{"5", "6"}

  // sample code  
  invalid := concatMultipleSlices([][]string{invalidA, invalidB, invalidC})
  fmt.Println(invalid)
}

// Output: [1 2 3 4 5 6]

参考

  1. How to Concatenate Two or More Slices in Go