12. Go to go: slices (trozos) en Golang

Publicado por

Cuando estudiamos los arreglos pudiste identificar que, a pesar de ser poderosas herramientas, resultan ser poco flexibles puesto que solamente permiten almacenar la cantidad de datos que sea especificada al momento de declararlos.

Nota: en esta entrada considero que ya has trabajado con arreglos en Go. Si no lo has hecho, te recomiendo leer la entrada al respecto que puedes encontrar en codingornot dando clic aquí.

Go nos proporciona una forma alternativa para trabajar con arreglos por medio de los slices (trozos, rebanadas, porciones etc.) los cuales nos brindan las mismas ventajas de los arreglos pero con algunas funciones y características únicas que los vuelven más flexibles que estos últimos. Inclusive, es considerablemente más común encontrar programas que trabajen con slices a que lo hagan con arreglos¹.

Los slices (trozos)

Antes de continuar, es conveniente aclarar que ni “slice” ni tampoco “slices” son palabras reservadas de Golang, es decir, pueden ser usadas libremente en identificadores porque no son un tipo de dato, sino que son descriptores de un espacio de memoria en el que se almacenan datos contiguos (¡hola, arreglo!), es por eso que mencioné que esta es una forma alternativa para trabajar con arreglos.

La sintaxis que se utiliza para declarar un slice es la siguiente:

SliceType = "[" "]" ElementType
ElementType = Type
Type = TypeName | TypeLit | "(" Type ")"

Más información al respecto aquí.

Ejemplo de declaración de un slice:

var nums1 []int //Slice sin tamaño definido

Como puede notarse, la forma para declarar slices es como se hacía con los arreglos pero sin especificar el tamaño de elementos que almacenarán. También puedes utilizar la función make para la declaración. La sintaxis de dicha función es la siguiente:

func make([]T, len, cap)[]T

  • Donde T es el tipo de dato de los elementos que contendrá nuestro slice, len es la cantidad de elementos que tendrá al momento de declararlo (inicializados en 0 automáticamente), mientras que cap es la capacidad total del slice (puede modificarse más adelante). Cuando make es llamado, se reserva espacio en memoria para un arreglo de la capacidad especificada y nuestro slice será el que almacene la información necesaria para realizar operaciones sobre dicho arreglo.

Ejemplo de uso:

var nums []int //Declaración de slice
nums = make([]int, 3, 3) //Inicialización con 3 elementos y capacidad de 3

O bien, si deseas utilizar solamente una línea de código:

nums := make([]int, 3, 3)

No es necesario especificar la capacidad del slice, si se pasa solamente un parámetro, make establecerá la capacidad igual a la longitud de nuestro arreglo, es decir:

//Las dos sentencias siguientes hacen lo mismo
nums := make([]int, 3)
nums := make([]int, 3, 3)

Recordemos que cuando declaramos que la longitud de nuestro slice sería de tres elementos, estos se inicializaron en 0.

¿Slices o arreglos?

Puede llegar a ser confuso el uso de ambos términos. Podemos comprender internamente cómo funcionan los slices e identificar por qué se habla de arreglos de datos en slices con la siguiente representación:

Estrructura interna de un slice
Estrructura interna de un slice

Ejemplo de slices en acción:

package main
import "fmt"

func main(){
  //Slice que se declara e inicializa por separado
  var nums1 []int
  nums1 = make([]int, 3, 3)

  //Slice que se declara e inicializa en la misma línea
  nums2:=make([]int, 4)

  fmt.Println(nums1)
  fmt.Println(nums2)
}

golang-slices-ejemplo-01
Ejemplo de slices

Salida:

Operaciones y acciones sobre slices

Se puede acceder a los elementos de un slice uno a uno por medio de índices, tal como se hace con los arreglos:

//Modificar el elemento en el índice 0
nums[0] = 5

//Imprimir el elemento en el índice 0
fmt.Println(nums[0])

También es posible acceder a diversos índices de un slice a la vez por medio de un rango, como se muestra a continuación:

slice[a:b] //Acceder a los alementos desde a-b
slice[:b] //Acceder a los alementos desde 0-b
slice[a:] //Acceder a los alementos desde a-len(a)
slice[:] //Acceder a todos los elementos

  • Donde a es el límite inferior (inclusivo) y b el límite superior (exclusivo). Cuando se deja en blanco el primer campo, se considera que se desea acceder desde el índice 0 hasta b; en cambio, si se deja en blanco el segundo campo del rango, se considera que el límite será el último elemento del slice o len(slice).

Puedes inicializar un slice con la información total o parcial de otro gracias a los límites antes mencionados:

slice2 := slice1[1:3] //Crea e inicializa slice2 con los índices 1 y 2 de slice1
slice3 := slice1[:] //Crea e inicializa slice3 con el contenido total de slice1

¡NOTA IMPORTANTE!: Si se inicializa un slice como en los ejemplos anteriores, es importante tener en cuenta que lo que se crea realmente es un apuntador al espacio de memoria del slice que se usa como información base; por tanto, si se hace un cambio sobre alguno de ellos, el otro también se verá afectado.

slice2:=slice1[:] //slice2 es un apuntador al arreglo de datos de slice1

len(), cap() y copy()

Es posible determinar la cantidad de elementos y capacidad de un arreglo por medio de las funciones len() y cap() respectivamente. La cantidad de elementos es el número de elementos a los que se hace referencia con el slice, la capacidad es la cantidad total de espacios disponibles para almacenar elementos.

len(slice) //Regresa la cantidad de elementos en slice
cap(slice) //Regresa la cantidad de espacios total del slice

Puedes realizar una copia de un slice con la función copy() como se muestra a continuación:

copy(slice_destino, silice_original)

Una de las más importantes características de los slices es que permiten aumentar la capacidad máxima de elementos por medio de la función append() la cual añade uno o varios elementos nuevos desde el último índice del slice. Si el slice aún tiene sus últimos índices disponibles, simplemente se añade el nuevo elemento a la cola; en cambio, si el último índice ya está utilizado, se aumenta dinámicamente la capacidad del slice en 1 y se añade dicho elemento en la última posición.

slice = append(slice, “uno”, “dos”, “tres”) //Se añaden 3 elementos nuevos
slice = append(slice, “cuatro”) //Se pueden añadir N o 1 elemento nuevo

Ejemplo de acceso a índices y rangos:

package main
import "fmt"

func main(){
  nums1:=make([]int, 5, 5) //Inicializa en 0 los 5 elementos del slice

  nums1[3] = 77 //Modifica el índice 3 del slice nums1

  fmt.Println("nums1[]= ", nums1)

  nums2 := []int{9, 8, 7, 6} //Nuevo slice nums2 inicializado similar a un array

  fmt.Println("nums2[]= ", nums2)

  //Acceder a rangos (sub slices) de slices
  fmt.Println("nums2[0:2]=", nums2[:2])
  fmt.Println("nums2[2:4]=", nums2[2:])
}

Salida:

golang-slices-ejemplo-02
Ejemplo 2 de slices

Ejemplo de len(), cap() y append():

package main
import "fmt"

func main(){
  var palabras = []string{"ab", "cd", "fg", "hi"}

  //Inicializar un slice con un segmento del array
  slice_palab:=palabras[1:3]

  fmt.Println("Arreglo palabras= ", palabras)
  fmt.Println("Slice palabras[1:3]= ", slice_palab)

  //Funciones len() y cap() en acción
  fmt.Printf("slice_palab len = %d cap = %d\n", len(slice_palab), cap(slice_palab))

  slice_palab = append(slice_palab, "jk", "lm") //Se añaden 2 elementos

  /*Capacidad de slice_palab = 6 porque aumenta la capacidad
  * del array que referencia (palabras)*/
  fmt.Printf("slice_palab len = %d cap = %d\n", len(slice_palab), cap(slice_palab))
}

Salida:

golang-slices-ejemplo-03

Ejemplo de uso de copy():

package main
import "fmt"

func main(){
  nums1:=make([]int, 5, 5) //Nuevo slice
  nums2:=nums1[:] //Nuevo slice referenciando nums1

  fmt.Println("nums1 = ", nums1)

  nums2[1] = 99 //Modifica nums2

  //Los cambios hecho en nums2 se reflejan en nums1
  fmt.Println("nums1 después de cambios en nums2 = ",nums1)

  nums3:=make([]int, 5, 5) //nuevo slice

  //Ejemplo de copy()
  copy(nums3, nums1)

  fmt.Println("nums3 (usando copy() con nums1) = ", nums3)

  nums1[2] = 55

  //Usando copy(), nums3 no referencia a nums1
  fmt.Println("Valores de nums1 después de cambio = ", nums1)
  fmt.Println("nums3 no cambió al modificar nums1 = ", nums3)
}

Salida:

golang-slices-ejemplo-04

Finalizando…

Espero que todo te haya sido de utilidad, recuerda que para comprender mejor un concepto te recomiendo realizar muchas pruebas hasta que lo domines. Esto ha sido todo por hoy, te espero la próxima, see ya!

¹ Blog Golang – “Arrays have their place, but they’re a bit inflexible, so you don’t see them too often in Go code. Slices, though, are everywhere. They build on arrays to provide great power and convenience.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *