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.
// Ejemplo correcto
var a [5]int
// Ejemplo incorrecto
l := 1
var b [l]intPrimero inicialicemos un array de enteros de longitud 5
var nums [5]intTambién se puede inicializar con elementos
nums := [5]int{1, 2, 3}Se puede dejar que el compilador infiera automáticamente la longitud
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 arrayTambién se puede obtener un puntero mediante la función new
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:
length := 5 // esto es una variable
var nums [length]intlength es una variable, por lo tanto no se puede usar para inicializar la longitud de un array. Un ejemplo correcto sería:
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únUso
Con el nombre del array y el índice, se puede acceder al elemento correspondiente del array.
fmt.Println(nums[0])También se pueden modificar los elementos del array
nums[0] = 1También se puede acceder a la cantidad de elementos del array mediante la función incorporada len
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.
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:
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]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
[]intPara 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.
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
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
var nums []int // valor
nums := []int{1, 2, 3} // valor
nums := make([]int, 0, 0) // valor
nums := new([]int) // punteroSe 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.
func append(slice []Type, elems ...Type) []TypePrimero crear un slice vacío de longitud 0 y capacidad 0, luego insertar algunos elementos al final, y finalmente mostrar la longitud y capacidad.
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:
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}Insertar elementos desde el inicio
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
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
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:
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}Eliminar n elementos desde el inicio
nums = nums[n:]
fmt.Println(nums) // n=3 [4 5 6 7 8 9 10]Eliminar n elementos desde el final
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
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
nums = nums[:0]
fmt.Println(nums) // []Copiar
Al copiar un slice, se debe asegurar que el slice destino tenga suficiente longitud, por ejemplo
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
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
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
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:
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
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
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6Esto 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:
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:
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.
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
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.
