Gustavo Castillo

Gustavo Castillo | Software Engineer

Slices en Go

21 diciembre, 2020

Slices en Go

Foto por: Mariana Kurnyk

Disclaimer: muchos de los ejemplos y conceptos han sido directamente tomados y traducidos del libro The Go programming language, todo el crédito para los autores, los muestro en este post únicamente con fines didácticos para enseñar el lenguaje de programación Go.

Wow no se tú pero para mi el pan en esa foto luce delicioso 🤤… pero bueno no estoy aquí para hablar de pan, sino más bien para hablar acerca de slices.

¿Qué es un Slice?

Un slice representa una secuencia de elementos de un mismo tipo de longitud variable. El tipo de un slice es escrito []T, donde los elementos son del tipo T; luce como un arreglo pero sin su tamaño.

Los arreglos y los slices están íntimamente conectados. Un slice es una estructura de datos ligera que le da acceso a un subconjunto (o quizá a todos) de elementos de un arreglo, el cual es conocido como el arreglo subyacente del slice o underlying array en inglés.

package main

import "fmt"

func main()  {
  var slice []string
  fmt.Println(slice)
}

¿Qué elementos forman un slice?

Un slice tiene tres componentes: un puntero, una longitud, y una capacidad.

  • Puntero: apunta al primer elemento del arreglo que es alcanzable por el slice, el cual no necesariamente es el primer elemento del arreglo.

  • Longitud: representa el número de elementos que contiene el slice, este no puede ser mayor a la capacidad del mismo.

  • Capacidad: representa el número de elementos entre el principio del slice y el final del arreglo subyacente.

Nota: las funciones len y cap nos permiten calcular la longitud y capacidad respectivamente de un slice.

package main

import "fmt"

func main()  {
  languages := []string{"go", "javascript", "php"}
  fmt.Println(languages)
  fmt.Println(len(languages)) // 3
  fmt.Println(cap(languages)) // 3
}

Representación del arreglo subyacente (underlying array)

Multiples slices pueden compartir un mismo arreglo subyacente y pueden referirse a partes superpuestas de ese arreglo. En la imagen debajo podemos ver un slice de strings que representa los meses del año, y dos slices superpuestos de este.

Slices in go underlying array

Tal que Enero esta en months[1] y Diciembre en months[12].

Operador slice

El operador slice s[i:j], crea un nuevo slice que hace referencia a elementos de i a través de j-1 (se omite el último elemento) de la secuencia s, el cual puede ser un arreglo, un puntero a un arreglo, u otro slice.

El slice resultante contiene j-i elementos, si i es omitido su valor será 0, y si j es omitido, su valor será len(s). Así que el slice months[1:13] hace referencia a el rango completo de meses válidos, de igual forma el slice months[1:]; el slice months[:] hace referencia al arreglo completo.

package main

import "fmt"

func main()  {
  months := [...]string{
    0: "",
    1: "Enero",
    2: "Febrero",
    3: "Marzo",
    4: "Abril",
    5: "Mayo",
    6: "Junio",
    7: "Julio",
    8: "Agosto",
    9: "Septiembre",
    10: "Octubre",
    11: "Noviembre",
    12: "Diciembre",
  }

  fmt.Printf("%q\n", months[1:13]) // ["Enero" "Febrero" "Marzo" "Abril" "Mayo" "Junio" "Julio" "Agosto" "Septiembre" "Octubre" "Noviembre" "Diciembre"]
  fmt.Printf("%q\n", months[1:]) // ["Enero" "Febrero" "Marzo" "Abril" "Mayo" "Junio" "Julio" "Agosto" "Septiembre" "Octubre" "Noviembre" "Diciembre"]
  fmt.Printf("%q\n", months[:]) // ["" "Enero" "Febrero" "Marzo" "Abril" "Mayo" "Junio" "Julio" "Agosto" "Septiembre" "Octubre" "Noviembre" "Diciembre"]
}

Cuando se usa el operador slice más allá de la capacidad del slice causa un panic, pero hacer slice más allá de la longitud extiende el slice, entonces el resultado puede ser un slice más grande que el original.

package main

import "fmt"

func main()  {
  // same definition for the months slice...

  summer := months[6:9]
  fmt.Println(summer[:20]) // panic: runtime error: slice bounds out of range [:20] with capacity 7

  endlessSummer := summer[:5] // extiende la capacidad el slice
  fmt.Printf("%q\n", endlessSummer) // ["Junio" "Julio" "Agosto" "Septiembre" "Octubre"]
}

Comparando slices

A diferencia de los arreglos, los slices no son comparables, por lo tanto no podemos usar el operador de comparación == para determinar si dos slices contienen los mismos elementos. La librería estándar de Go provee la función bytes.Equal altamente optimizada para comparar dos slices de tipo []byte, pero para los otros tipos de slice debemos hacer la comparación nosotros mismos.

package main

import "fmt"

func main()  {
  x := []string{"go", "php", "javascript"}
  y := []string{"go", "php", "javascript"}

  fmt.Printf("x == y : %t\n", equal(x, y)) // x == y : true
}

func equal(x, y []string) bool {
  if len(x) != len(y) {
    return false
  }

  for i := range x {
    if x[i] != y[i] {
      return false
    }
  }
  return true
}

La única comparación legal de un slice es compararlo contra nil, por ejemplo:

package main

import "fmt"

func main()  {
  // same definition for the months slice...

  summer := months[6:9]
  if summer == nil {
    fmt.Println("summer es igual a nil")
    return
  }
  // en este caso imprimiría esto porque summer no es igual a nil
  fmt.Println("summer no es igual a nil")
}

El valor zero de un tipo slice es nil. Un slice con valor nil no tiene un arreglo subyacente. El slice con valor nil tiene una longitud y capacidad cero, pero existen slices que no tienen valor nil con longitud y capacidad cero, por ejemplo []int{} o make([]int, 3)[3:]. Como sucede con cualquier tipo que pueda tener valores nil, el valor nil de un slice en particular puede ser escrito usando una expresión de conversión tal como []int(nil).

package main

import "fmt"

func main() {
  fmt.Printf("%s\t| %s\n", "¿Longitud igual a cero?", "¿slice igual a nil?")
  fmt.Printf("%s\n", "- - - - - - - - - - - - - - - - - - - - - - -")

  var s []int // len(s) == 0, s == nil
  fmt.Printf("%t\t\t\t| %t\n", len(s) == 0, s == nil)

  s = nil // len(s) == 0, s == nil
  fmt.Printf("%t\t\t\t| %t\n", len(s) == 0, s == nil)

  s = []int(nil) // len(s) == 0, s == nil
  fmt.Printf("%t\t\t\t| %t\n", len(s) == 0, s == nil)

  s = []int{} // len(s) == 0, s != nil
  fmt.Printf("%t\t\t\t| %t\n", len(s) == 0, s == nil)
}
// Resultado de este programa:
/*
¿Longitud igual a cero?	| ¿slice igual a nil?
- - - - - - - - - - - - - - - - - - - - - - -
true					| true
true					| true
true					| true
true					| false
*/

Si necesitas probar si un slice esta vacío, usa la expresión len(s) == 0, en lugar de s == nil.

Función make

La función pre construida make crea un slice del tipo de elemento, longitud y capacidad especificado. El argumento de la capacidad puede ser omitido, y en ese caso el valor de la capacidad es igual a la longitud.

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

Función append

La función pre construida append nos permite añadir elementos a un slice.

package main

import "fmt"

func main() {
  var runes []rune
  fmt.Println(runes) // []

  for _, r := range "Hi gophers!" {
    runes = append(runes, r)
  }

  fmt.Printf("%q\n", runes) // ['H' 'i' ' ' 'g' 'o' 'p' 'h' 'e' 'r' 's' '!']
}

La función append es crucial para entender el funcionamiento de los slices, veamos más de cerca como funciona.

package main

import "fmt"

func main() {
  var x, y []int
  for i := 0; i < 10; i++ {
    y = appendInt(x, i)
    fmt.Printf("%d  cap=%d\t%v\n", i, cap(y), y)
    x = y
  }
}

func appendInt(x []int, y int) []int {
  var z []int
  zlen := len(x) + 1
  if zlen <= cap(x) {
    // Hay espacio para crecer. Extiende el slice.
    z = x[:zlen]
  } else {
    // No hat suficiente espacio. Asigna un nuevo arreglo.
    // Duplica el arreglo, para amortizar la complejidad linear.
    zcap := zlen
    if zcap < 2*len(x) {
      zcap = 2 * len(x)
    }
    z = make([]int, zlen, zcap)
    copy(z, x) // realiza una copia del slice.
  }
  z[len(x)] = y
  return z
}

/*
// Resultado del programa:
0  cap=1	[0]
1  cap=2	[0 1]
2  cap=4	[0 1 2]
3  cap=4	[0 1 2 3]
4  cap=8	[0 1 2 3 4]
5  cap=8	[0 1 2 3 4 5]
6  cap=8	[0 1 2 3 4 5 6]
7  cap=8	[0 1 2 3 4 5 6 7]
8  cap=16	[0 1 2 3 4 5 6 7 8]
9  cap=16	[0 1 2 3 4 5 6 7 8 9]
*/

En la iteración i=0, el slice x contiene tres elementos [0, 1, 2] pero tiene capacidad 4, entonces hay un único elemento suelto al final, y appendInt de el elemento 3 puede proceder sin re-asignación (de memoria). El slice resultante y tiene longitud y capacidad 4, y tiene el mismo arreglo subyacente que el slice original x.

Conclusión

En próximos posts aprenderemos sobre maps en Go. Hasta la próxima 👋🏽.


© 2021, Sígueme en: |