Skip to content

Slices

En Go, los arrays y los slices parecen casi idénticos, pero tienen diferencias funcionales importantes. Los arrays son estructuras de datos de longitud fija, una vez especificada su longitud no se puede cambiar. Los slices son de longitud variable y se expanden automáticamente cuando su capacidad es insuficiente.

Arrays

Si se conoce de antemano la longitud de los datos a almacenar y no habrá necesidad de expansión durante el uso posterior, se puede considerar usar un array. En Go, los arrays son tipos de valor, no referencias, y no son punteros al primer elemento.

TIP

Como los arrays son tipos de valor, cuando se pasa un array como parámetro a una función, como Go pasa parámetros por valor, se copia todo el array.

Inicialización

Al declarar un array, la longitud debe ser una constante, no una variable. No se puede declarar una variable y luego usar esa variable como la longitud del array.

go
// Ejemplo correcto
var a [5]int

// Ejemplo incorrecto
l := 1
var b [l]int

Primero inicialicemos un array de enteros de longitud 5

go
var nums [5]int

También se puede inicializar con elementos

go
nums := [5]int{1, 2, 3}

Se puede dejar que el compilador infiera automáticamente la longitud

go
nums := [...]int{1, 2, 3, 4, 5} // equivalente a nums := [5]int{1, 2, 3, 4, 5}, los puntos suspensivos deben estar presentes, de lo contrario se genera un slice, no un array

También se puede obtener un puntero mediante la función new

go
nums := new([5]int)

Todas las formas anteriores asignarán un bloque de memoria de tamaño fijo a nums, la diferencia es que la última obtiene un puntero.

Al inicializar un array, hay que tener en cuenta que la longitud debe ser una expresión constante, de lo contrario no se podrá compilar. Una expresión constante es aquella cuyo resultado final es una constante. Un ejemplo incorrecto sería:

go
length := 5 // esto es una variable
var nums [length]int

length es una variable, por lo tanto no se puede usar para inicializar la longitud de un array. Un ejemplo correcto sería:

go
const length = 5
var nums [length]int // constante
var nums2 [length + 1]int // expresión constante
var nums3 [(1 + 2 + 3) * 5]int // expresión constante
var nums4 [5]int // la más común

Uso

Con el nombre del array y el índice, se puede acceder al elemento correspondiente del array.

go
fmt.Println(nums[0])

También se pueden modificar los elementos del array

go
nums[0] = 1

También se puede acceder a la cantidad de elementos del array mediante la función incorporada len

go
len(nums)

Mediante la función incorporada cap se puede acceder a la capacidad del array. La capacidad de un array es igual a su longitud, la capacidad solo tiene significado para los slices.

go
cap(nums)

Corte

El formato para cortar un array es arr[startIndex:endIndex], el intervalo de corte es cerrado por la izquierda y abierto por la derecha. Además, después de cortar un array, se convierte en tipo slice. Un ejemplo:

go
nums := [5]int{1, 2, 3, 4, 5}

nums[:] // rango del sub-slice [0,5) -> [1 2 3 4 5]
nums[1:] // rango del sub-slice [1,5) -> [2 3 4 5]
nums[:5] // rango del sub-slice [0,5) -> [1 2 3 4 5]
nums[2:3] // rango del sub-slice [2,3) -> [3]
nums[1:3] // rango del sub-slice [1,3) -> [2 3]
go
func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  fmt.Printf("%T\n", arr)
  fmt.Printf("%T\n", arr[1:2])
}

Salida

[5]int
[]int

Para convertir un array a tipo slice, simplemente se corta sin parámetros. El slice convertido apunta a la misma memoria que el array original, modificar el slice causará cambios en el contenido del array original.

go
func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  slice := arr[:]
  slice[0] = 0
  fmt.Printf("array: %v\n", arr)
  fmt.Printf("slice: %v\n", slice)
}

Salida

array: [0 2 3 4 5]
slice: [0 2 3 4 5]

Si se quiere modificar el slice convertido sin afectar el original, se recomienda usar la siguiente forma de conversión

go
func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  slice := slices.Clone(arr[:])
  slice[0] = 0
  fmt.Printf("array: %v\n", arr)
  fmt.Printf("slice: %v\n", slice)
}

Salida

array: [1 2 3 4 5]
slice: [0 2 3 4 5]

Slices

Los slices tienen un rango de aplicación mucho más amplio que los arrays en Go. Se usan para almacenar datos de longitud desconocida, y durante el uso posterior pueden requerir frecuentes inserciones y eliminaciones de elementos.

Inicialización

Hay varias formas de inicializar un slice

go
var nums []int // valor
nums := []int{1, 2, 3} // valor
nums := make([]int, 0, 0) // valor
nums := new([]int) // puntero

Se puede ver que la diferencia entre un slice y un array en apariencia es simplemente que falta una longitud de inicialización. Normalmente, se recomienda usar make para crear un slice vacío. Para los slices, la función make recibe tres parámetros: tipo, longitud y capacidad. Para explicar la diferencia entre longitud y capacidad con un ejemplo: supongamos que hay un cubo de agua que no está lleno. La altura del cubo es su capacidad, representando cuánta agua puede contener en total, mientras que la altura del agua en el cubo representa la longitud. La altura del agua siempre debe ser menor o igual a la altura del cubo, de lo contrario el agua se desbordará. Por lo tanto, la longitud de un slice representa el número de elementos en el slice, y la capacidad representa cuántos elementos puede contener en total. La mayor diferencia entre un slice y un array es que la capacidad del slice se expande automáticamente, mientras que la del array no. Para más detalles, ve a manual de referencia - longitud y capacidad.

TIP

La implementación subyacente de un slice sigue siendo un array, es un tipo de referencia. Se puede entender simplemente como un puntero al array subyacente (en esencia, un slice en Go es una estructura que contiene un puntero al array subyacente, un valor de longitud y un valor de capacidad). Por lo tanto, cuando se pasa un slice como parámetro de función no se copia el array subyacente, y las modificaciones al slice pasado dentro de la función se reflejarán en el slice original.

Un slice declarado mediante var nums []int tiene un valor por defecto de nil, por lo que no se le asignará memoria. Al inicializar con make, se recomienda pre-asignar una capacidad suficiente, lo que puede reducir efectivamente el consumo de memoria en expansiones posteriores.

Uso

El uso básico de un slice es completamente idéntico al de un array, la diferencia es que un slice puede cambiar dinámicamente su longitud. Veamos algunos ejemplos.

Un slice puede implementar muchas operaciones mediante la función append. La firma de la función es la siguiente, slice es el slice destino al que se añadirán elementos, elems son los elementos a añadir, y el valor de retorno es el slice después de añadir los elementos.

go
func append(slice []Type, elems ...Type) []Type

Primero crear un slice vacío de longitud 0 y capacidad 0, luego insertar algunos elementos al final, y finalmente mostrar la longitud y capacidad.

go
nums := make([]int, 0, 0)
nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
fmt.Println(len(nums), cap(nums)) // 7 8 se puede ver que la longitud y capacidad no son consistentes.

El tamaño del buffer reservado para el nuevo slice sigue una cierta regularidad. Antes de la actualización de golang 1.18, la mayoría de artículos en internet describían la estrategia de expansión del slice así: Cuando la capacidad del slice original es menor que 1024, la capacidad del nuevo slice se convierte en 2 veces la original; cuando la capacidad del slice original excede 1024, la capacidad del nuevo slice se convierte en 1.25 veces la original. Después de la actualización de la versión 1.18, la estrategia de expansión del slice cambió a: Cuando la capacidad del slice original (oldcap) es menor que 256, la capacidad del nuevo slice (newcap) es 2 veces la original; cuando la capacidad del slice original excede 256, la capacidad del nuevo slice newcap = oldcap + (oldcap + 3*256) / 4

Insertar elementos

La inserción de elementos en un slice también necesita usarse junto con la función append. Tenemos el siguiente slice:

go
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

Insertar elementos desde el inicio

go
nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]

Insertar elementos desde el índice intermedio i

go
nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...)
fmt.Println(nums) // i=3, [1 2 3 4 999 999 5 6 7 8 9 10]

Insertar elementos al final es el uso más original de append

go
nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]

Eliminar elementos

La eliminación de elementos de un slice necesita usarse junto con la función append. Tenemos el siguiente slice:

go
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

Eliminar n elementos desde el inicio

go
nums = nums[n:]
fmt.Println(nums) // n=3 [4 5 6 7 8 9 10]

Eliminar n elementos desde el final

go
nums = nums[:len(nums)-n]
fmt.Println(nums) // n=3 [1 2 3 4 5 6 7]

Eliminar n elementos desde la posición del índice intermedio i

go
nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums) // i=2, n=3, [1 2 6 7 8 9 10]

Eliminar todos los elementos

go
nums = nums[:0]
fmt.Println(nums) // []

Copiar

Al copiar un slice, se debe asegurar que el slice destino tenga suficiente longitud, por ejemplo

go
func main() {
  dest := make([]int, 0)
  src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
  fmt.Println(src, dest)
  fmt.Println(copy(dest, src))
  fmt.Println(src, dest)
}
[1 2 3 4 5 6 7 8 9] []
0
[1 2 3 4 5 6 7 8 9] []

Modificar la longitud a 10, la salida es:

[1 2 3 4 5 6 7 8 9] [0 0 0 0 0 0 0 0 0 0]
9
[1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9 0]

Recorrer

El recorrido de un slice es completamente idéntico al de un array, con un bucle for

go
func main() {
   slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
   for i := 0; i < len(slice); i++ {
      fmt.Println(slice[i])
   }
}

Con un bucle for range

go
func main() {
  slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
  for index, val := range slice {
    fmt.Println(index, val)
  }
}

Slices multidimensionales

Veamos el siguiente ejemplo, la documentación oficial también lo explica: Effective Go - Two-dimensional slices

go
var nums [5][5]int
for _, num := range nums {
   fmt.Println(num)
}
fmt.Println()
slices := make([][]int, 5)
for _, slice := range slices {
   fmt.Println(slice)
}

El resultado de la salida es

[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]

[]
[]
[]
[]
[]

Se puede ver que, siendo ambos bidimensionales, un array y un slice tienen estructuras internas diferentes. Al inicializar un array, sus dimensiones ya están fijadas, mientras que la longitud de un slice no es fija, cada slice dentro de un slice puede tener una longitud diferente, por lo que se debe inicializar individualmente. Modificando la inicialización del slice de la siguiente manera:

go
slices := make([][]int, 5)
for i := 0; i < len(slices); i++ {
   slices[i] = make([]int, 5)
}

El resultado final de la salida es

[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]

[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]

Expresión extendida

TIP

Solo los slices pueden usar expresiones extendidas

Tanto los slices como los arrays pueden usar expresiones simples para cortar, pero las expresiones extendidas solo pueden ser usadas por los slices. Esta característica se añadió en la versión Go 1.2, principalmente para resolver el problema de lectura y escritura de slices que comparten arrays subyacentes. El formato principal es el siguiente, debe cumplirse la relación low <= high <= max <= cap, la capacidad del slice cortado con una expresión extendida es max - low

go
slice[low:high:max]

low y high siguen teniendo el mismo significado que antes, mientras que el nuevo max se refiere a la capacidad máxima. Por ejemplo, en el siguiente caso se omite max, entonces la capacidad de s2 es cap(s1) - low

go
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6

Esto crea un problema evidente, s1 y s2 comparten el mismo array subyacente, al leer y escribir en s2, es posible que se afecten los datos de s1. El siguiente código es un ejemplo de esta situación:

go
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4]                          // cap = 9 - 3 = 6
s2 = append(s2, 1)   // añadir nuevo elemento, como la capacidad es 6 no hay expansión, se modifica directamente el array subyacente
fmt.Println(s2)
fmt.Println(s1)

La salida final es

[4 1]
[1 2 3 4 1 6 7 8 9]

Se puede ver que claramente se añadió un elemento a s2, pero s1 también se modificó. Las expresiones extendidas existen precisamente para resolver este tipo de problemas, con una pequeña modificación se puede resolver:

go
func main() {
   s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
   s2 := s1[3:4:4]                        // cap = 4 - 3 = 1
   s2 = append(s2, 1)    // capacidad insuficiente, se asigna un nuevo array subyacente
   fmt.Println(s2)
   fmt.Println(s1)
}

Ahora el resultado obtenido es correcto

[4 1]
[1 2 3 4 5 6 7 8 9]

clear

En go 1.21 se añadió la función incorporada clear. clear establecerá todos los valores dentro del slice a su valor cero.

go
package main

import (
    "fmt"
)

func main() {
    s := []int{1, 2, 3, 4}
    clear(s)
    fmt.Println(s)
}

Salida

[0 0 0 0]

Si se quiere vaciar un slice, se puede hacer

go
func main() {
  s := []int{1, 2, 3, 4}
    s = s[:0:0]
  fmt.Println(s)
}

Limitando la capacidad del slice después de cortar, esto evita sobrescribir los elementos posteriores del slice original.

Golang editado por www.golangdev.cn