En la sección anterior, discutimos los arrays (arreglos) en Go. Sin embargo, los arrays tienen limitaciones: una vez declarados e inicializados, su longitud no se puede cambiar. Por lo tanto, los arrays no se utilizan ampliamente en la programación diaria. En contraste, las slices (rebanadas) se utilizan con más frecuencia y proporcionan una estructura de datos más flexible.
Puntos de conocimiento:
Definir una slice
Inicializar una slice
Operaciones en slices, es decir, agregar, eliminar, modificar y buscar
Expandir una slice
Truncar una slice
Slices multidimensionales
Skills Graph
%%%%{init: {'theme':'neutral'}}%%%%
flowchart RL
go(("Golang")) -.-> go/BasicsGroup(["Basics"])
go(("Golang")) -.-> go/DataTypesandStructuresGroup(["Data Types and Structures"])
go(("Golang")) -.-> go/FunctionsandControlFlowGroup(["Functions and Control Flow"])
go/BasicsGroup -.-> go/values("Values")
go/BasicsGroup -.-> go/variables("Variables")
go/DataTypesandStructuresGroup -.-> go/arrays("Arrays")
go/DataTypesandStructuresGroup -.-> go/slices("Slices")
go/DataTypesandStructuresGroup -.-> go/pointers("Pointers")
go/FunctionsandControlFlowGroup -.-> go/for("For")
subgraph Lab Skills
go/values -.-> lab-149077{{"Estructuras de datos de slices en Golang"}}
go/variables -.-> lab-149077{{"Estructuras de datos de slices en Golang"}}
go/arrays -.-> lab-149077{{"Estructuras de datos de slices en Golang"}}
go/slices -.-> lab-149077{{"Estructuras de datos de slices en Golang"}}
go/pointers -.-> lab-149077{{"Estructuras de datos de slices en Golang"}}
go/for -.-> lab-149077{{"Estructuras de datos de slices en Golang"}}
end
¿Qué es una Slice?
Las slices (rebanadas) son similares a los arrays (arreglos); son contenedores que almacenan elementos del mismo tipo de datos. Sin embargo, los arrays tienen limitaciones: una vez declarados e inicializados, su longitud no se puede cambiar. Aunque los arrays tienen sus casos de uso, no son tan flexibles. Por lo tanto, las slices se utilizan con más frecuencia en la programación diaria.
En Go, las slices se implementan utilizando arrays. Una slice es esencialmente un array dinámico cuya longitud puede cambiar. Podemos realizar operaciones como agregar, eliminar, modificar y buscar elementos en una slice, lo cual no se puede hacer con un array.
Definir una Slice
La sintaxis de inicialización de una slice (rebanada) es muy similar a la de un array (arreglo). La principal diferencia es que no es necesario especificar la longitud de los elementos. Echemos un vistazo al siguiente código:
// Declare an array with a length of 5
var a1 [5]byte
// Declare a slice
var s1 []byte
Una Slice es una Referencia a un Array
Si declaramos un array (arreglo) de tipo int, el valor cero de cada elemento del array será 0.
Sin embargo, si declaramos una slice (rebanada), el valor cero de la slice será nil. Creemos un archivo llamado slice.go para verificar esto:
touch ~/project/slice.go
Ingrese el siguiente código:
package main
import "fmt"
func main() {
var a [3]int
var s []int
fmt.Println(a[0] == 0) // true
fmt.Println(s == nil) // true
}
go run slice.go
Después de ejecutar el código, se mostrará la siguiente salida:
true
true
Hemos creado un array a y una slice s. Comparamos el primer elemento del array a con cero y verificamos si la slice s es nil.
Como podemos ver, cuando declaramos una slice, su valor cero es nil. Esto se debe a que las slices no almacenan ningún dato; solo hacen referencia a arrays. La slice apunta a la estructura del array subyacente.
Estructura de Datos de una Slice
Una slice (rebanada) es un tipo de dato compuesto, también conocido como estructura (struct). Es un tipo compuesto formado por campos de diferentes tipos. La estructura interna de una slice consta de tres elementos: un puntero, una longitud y una capacidad.
type slice struct {
elem *type
len int
cap int
}
Como se mencionó anteriormente, la estructura hace referencia al array (arreglo) subyacente. El puntero elem apunta al primer elemento del array, y type es el tipo del elemento del array referenciado.
len y cap representan la longitud y la capacidad de la slice, respectivamente. Puedes usar las funciones len() y cap() para obtener la longitud y la capacidad de la slice.
La siguiente imagen muestra que la slice hace referencia a un array subyacente de tipo int, y tiene una longitud de 8 y una capacidad de 10:
Cuando defines una nueva slice, el puntero elem se inicializa con el valor cero (es decir, nil). El concepto de punteros se introducirá en laboratorios posteriores. Por ahora, ten en cuenta que un puntero apunta a la dirección de memoria de un valor. En la imagen anterior, el puntero elem apunta a la dirección del primer elemento del array subyacente.
Operaciones en Slices: Agregar, Eliminar, Modificar y Buscar
Truncar Arrays o Slices
Dado que la estructura subyacente de una slice (rebanada) es un array (arreglo), podemos extraer una longitud especificada del array como referencia para la slice. El siguiente segmento de código demuestra esto:
package main
import "fmt"
func main() {
// Define an integer array with a length of 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Declare an empty slice
var s1 []int
fmt.Println("Slice s1 is empty:", s1 == nil)
// Use array truncation to obtain the slice
s1 = a[1:5]
s2 := a[2:5]
s3 := a[:]
fmt.Println("Slice s1, s2, and s3 are empty:", s1 == nil, s2 == nil, s3 == nil)
fmt.Println("Elements in array a:", a)
fmt.Println("Elements in slice s1:", s1)
fmt.Println("Elements in slice s2:", s2)
fmt.Println("Elements in slice s3:", s3)
}
La salida es la siguiente:
Slice s1 is empty: true
Slice s1, s2, and s3 are empty: false false false
Elements in array a: [0 1 2 3 4 5 6 7 8 9]
Elements in slice s1: [1 2 3 4]
Elements in slice s2: [2 3 4]
Elements in slice s3: [0 1 2 3 4 5 6 7 8 9]
En este programa, primero declaramos e inicializamos el array a, y luego usamos la truncación de array para asignar una parte del array a la slice vacía s1. De esta manera, creamos una nueva slice.
s1[1:5] representa la creación de una slice del array. El rango de la slice es desde el índice 1 del array a hasta el índice 5, excluyendo el quinto elemento.
Nota: En los lenguajes de programación, el primer elemento tiene índice 0, no 1. Del mismo modo, el segundo elemento en el array tiene un índice de 1.
Usamos el operador := para asignar directamente el array truncado a la slice s2. Lo mismo se aplica a s3, pero no se especifica ningún rango, por lo que se truncan todos los elementos del array.
La imagen siguiente representa la truncación. Tenga en cuenta que la parte verde de la slice representa una referencia al array azul. En otras palabras, ambos comparten el mismo array subyacente, que es a.
La sintaxis de truncación para slices es la siguiente:
[start:end]
Tanto start como end son argumentos opcionales. Cuando queremos obtener todos los elementos del array, podemos omitir ambos argumentos start y end. Esto se demuestra en s3 := a[:] en el programa anterior.
Si queremos recuperar todos los elementos después de un cierto índice, podemos omitir el parámetro end. Por ejemplo, a1[3:] recuperará todos los elementos a partir del índice 3.
Para recuperar todos los elementos antes de un cierto índice, podemos omitir el parámetro start. Por ejemplo, a1[:4] extraerá todos los elementos desde el índice 0 hasta el índice 4, excluyendo el elemento en el índice 4.
Además de extraer la slice de un array, también podemos extraer una nueva slice de una existente. La operación es la misma que con los arrays. El siguiente es un ejemplo sencillo:
package main
import "fmt"
func main() {
// Define an integer array with a length of 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Create the initial slice s1
var s1 []int
s1 = a[1:7]
fmt.Printf("Slice s1: %d\tLength: %d\tCapacity: %d\n", s1, len(s1), cap(s1))
// Extract a new slice s2 from the initial slice s1
s2 := s1[2:4]
fmt.Printf("Slice s1: %d\tLength: %d\tCapacity: %d\n", s2, len(s2), cap(s2))
}
En este programa, obtuvimos la slice s1 truncando el array a. El rango de s1 es desde el índice 1 hasta el índice 7. Usando :=, extrajimos una nueva slice s2 de s1. Dado que la truncación de s1 es contigua, la nueva slice s2 también será contigua.
Notamos que la capacidad de una slice cambia a medida que la truncamos. Las reglas son las siguientes:
Si truncamos una slice con una capacidad de c, la longitud de s[i:j] será j - i, y la capacidad será c - i.
Para s1, el array subyacente es a, y la capacidad de a[1:7] es 9 (es decir, 10 - 1).
Para s2, el array subyacente es el mismo que el de s1. Dado que la parte truncada tiene una capacidad de 9 en el paso anterior, la capacidad de s1[2:4] se convierte en 7 (es decir, 9 - 2).
Las Modificaciones a los Valores de una Slice Afectan Simultáneamente a los Valores de los Elementos del Array Subyacente
Dado que la slice no almacena datos, sino que solo hace referencia a un array, modificar el valor de una slice también cambiará simultáneamente el valor del array subyacente. Demostremos esto:
package main
import "fmt"
func main() {
// Define an integer array with a length of 10
a := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := a[2:5]
s2 := a[:]
fmt.Println("Before Modification: ")
fmt.Println("Elements in array a: ", a)
fmt.Println("Elements in array a: ", s1)
fmt.Println("Elements in array a: ", s2)
// Modify the value at index 2 of the slice s1 to 23
s1[2] = 23
fmt.Println("After Modification: ")
fmt.Println("Elements in array a: ", a)
fmt.Println("Elements in array a: ", s1)
fmt.Println("Elements in array a: ", s2)
}
La salida es la siguiente:
Before Modification:
Elements in array a: [0 1 2 3 4 5 6 7 8 9]
Elements in array a: [2 3 4]
Elements in array a: [0 1 2 3 4 5 6 7 8 9]
After Modification:
Elements in array a: [0 1 23 3 4 5 6 7 8 9]
Elements in array a: [2 3 23]
Elements in array a: [0 1 23 3 4 5 6 7 8 9]
En este programa, tanto la slice s1 como s2 hacen referencia al array a. Cuando la slice s1 modifica el valor en el índice 2 a 23, los valores del array a y de la slice s2 también se actualizan.
Podemos ver que el valor en el índice 2 del array se modifica a 23, lo que resulta en la modificación del valor en el índice 4 de la slice s2.
Esto causaría errores difíciles de depurar en el desarrollo de programas. Por lo tanto, en la programación diaria, debemos tratar de evitar en la mayor medida posible que múltiples slices hagan referencia al mismo array subyacente.
Añadir Elementos a una Slice
En esta sección, introduciremos la función append, que se utiliza para agregar elementos a una slice. La sintaxis es la siguiente:
func append(slice []Type, elems...Type) []Type
El primer argumento es la slice slice, y los argumentos restantes son los elementos que se agregarán a la slice. El []Type al final indica que la función append devolverá una nueva slice con el mismo tipo de datos que slice.
El elems después de ... representa que este es un parámetro variádico, lo que significa que se pueden ingresar uno o más parámetros.
A continuación, un ejemplo de uso de la función append:
En este programa, primero creamos la slice s1 truncando el array a. El rango de s1 es desde el índice 1 hasta el índice 7. Usamos el operador := para asignar el valor devuelto por append a s1. Cuando agregamos un elemento usando append, la capacidad de la slice cambiará. La capacidad de s1 se duplicará si el número de elementos supera la capacidad.
Las reglas de expansión para slices son las siguientes:
Si el array subyacente de una slice puede acomodar nuevos elementos, la capacidad de la slice no cambiará.
Si el array subyacente de una slice no puede acomodar nuevos elementos, Go creará un array más grande, copiará los valores de la slice original en el nuevo array y luego agregará el valor anexado al nuevo array.
Nota: La expansión de la slice no siempre es una duplicación; depende del tamaño de los elementos, el número de elementos que se están expandiendo y el hardware de la computadora, entre otros factores. Para obtener más información, consulte la sección avanzada sobre slices.
Eliminar Elementos en una Slice
Go no proporciona palabras clave o funciones para eliminar elementos de una slice, pero podemos usar la truncación de array para lograr la misma funcionalidad, o una capacidad más poderosa.
El siguiente código elimina el elemento en el índice 5 de la slice s y lo asigna a una nueva slice s1:
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// Print the slice `s` before deleting
fmt.Println(s)
s1 := append(s[:5], s[6:]...)
// Print the slices `s` and `s1` after deleting the element at index 5
fmt.Printf("%d\n%d\n", s, s1)
}
La clave para eliminar un índice específico radica en la declaración append. Anexa los elementos después del índice 6 a los elementos antes del índice 5.
Esta operación es equivalente a una sobrescritura anticipada. Por ejemplo, el elemento 5 se sobrescribe con el elemento 6, el elemento 6 se sobrescribe con el elemento 7, y así sucesivamente, hasta el elemento 9, que se agrega a la nueva slice. Por lo tanto, el valor del array subyacente en este momento es [0 1 2 3 4 6 7 8 9 9].
En este momento, si verificamos la longitud y la capacidad de s1, podemos ver que la longitud es 9, pero la capacidad es 10. Esto se debe a que s1 es una referencia a la truncación de s. Comparten el mismo array subyacente.
package main
import "fmt"
func main() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Println(s)
s1 := append(s[:5], s[6:]...)
// Check the length and capacity of s1
fmt.Println(len(s1), cap(s1))
fmt.Printf("\n%d\n%d\n\n", s, s1)
// Modify the slice s
s[3] = 22
fmt.Printf("%d\n%d\n", s, s1)
}
Además de eliminar elementos en posiciones específicas, también podemos eliminar un rango específico de elementos de una slice usando la truncación. La operación es la misma que eliminar un elemento en un índice específico. Hagamos un pequeño ejercicio.
Crea un archivo slice1.go. Crea una slice a e inicialízala de la siguiente manera. Luego, usa la truncación para crear otra slice s que no incluya elementos mayores que 3 o menores que 7. Finalmente, imprime la nueva slice.
a := []int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
Salida:
[9 8 7 3 2 1 0]
Pista: Presta atención al índice inicial de la slice (recuerda, el índice comienza desde 0).
Requisitos: El archivo slice1.go debe colocarse en el directorio ~/project.
Una slice (rebanada) tiene dos atributos: len y cap. len representa el número de elementos actualmente en la slice, mientras que cap representa el número máximo de elementos que la slice puede contener.
¿Qué sucede cuando el número de elementos agregados a una slice supera su capacidad? Descubrámoslo juntos:
Para ejecutar el programa, ejecute el siguiente comando:
go run slice.go
La salida es la siguiente:
Before Append in s1: [0 0 0] Length: 3 Capacity: 3
Before Append in s2: [0 0 0] Length: 3 Capacity: 5
After Append in s1: [0 0 0 12] Length: 4 Capacity: 6
After Append in s2: [0 0 0 22] Length: 4 Capacity: 5
Como podemos ver en este programa, cuando agregamos elementos a una slice y el número de elementos supera su capacidad original, la capacidad de la slice se incrementa automáticamente.
Las reglas para expandir una slice son las siguientes:
Si el array (arreglo) subyacente de una slice puede contener nuevos elementos, la capacidad de la slice no cambiará.
Si el array subyacente de una slice no puede contener nuevos elementos, Go creará un array más grande, copiará los valores de la slice original al nuevo array y luego agregará los valores anexados al nuevo array.
Nota: Duplicar la capacidad de una slice no siempre es el caso; depende del tamaño de los elementos, el número de elementos y el hardware de la computadora. Para obtener más información, consulte la sección avanzada sobre slices.
Copiar Slices
Podemos usar la función copy para duplicar una slice (rebanada) en otra. La sintaxis es la siguiente:
func copy(dst, src []Type) int
dst es la slice de destino, src es la slice de origen, y el int final indica el número de elementos copiados, que es el mínimo entre len(dst) y len(src).
Para ejecutar el programa, ejecute el siguiente comando:
go run slice.go
La salida es la siguiente:
2 [0 1 2 3] [0 1]
2 [8 9 2 3] [8 9]
En este programa, copiamos los valores de la slice s1 a la slice s2, y los valores de la slice s4 a la slice s3. La función copy devuelve el número de elementos copiados.
Notamos que los valores de s1 y s2 son los mismos, al igual que los valores de s3 y s4. La primera función copy copia s1[0, 1, 2, 3] a s2[8, 9]. Dado que la longitud mínima de s1 y s2 es 2, copia 2 valores. La slice de destino s2 se modifica a [0, 1].
La segunda función copy copia s4[8, 9] a s3[0, 1, 2, 3]. Dado que la longitud mínima de s3 y s4 es 2, copia 2 valores. Como resultado, s3 se modifica a [8, 9, 2, 3].
Recorrer Slices
Recorrer una slice (rebanada) es similar a recorrer un array (arreglo). Todos los métodos de recorrido de arrays también se pueden utilizar para slices.
Ejercicio
En este ejercicio, probaremos y reforzaremos nuestra comprensión del recorrido de slices y arrays.
Crea un archivo slice2.go. Declara un array a1 y una slice s1, e initíalos de la siguiente manera. Luego, itera sobre los elementos en el array a1 y la slice s1, e imprime sus índices y valores.
Element a1 at index 0 is 1
Element a1 at index 1 is 2
Element a1 at index 2 is 3
Element a1 at index 3 is 9
Element a1 at index 4 is 7
Element s1 at index 0 is 1
Element s1 at index 1 is 8
Element s1 at index 2 is 12
Element s1 at index 3 is 1
Element s1 at index 4 is 3
Requisitos: El archivo slice2.go debe colocarse en el directorio ~/project.
Pista: Puedes usar el formato 'range' o el formato de índice para iterar a través de los elementos.
En esta sección, hemos aprendido sobre las slices (rebanadas) y su uso. Comparativamente, las slices son más flexibles y versátiles que los arrays (arreglos). Cuando trabajamos con múltiples slices, debemos tener cuidado para evitar operaciones inesperadas en las slices.