Maps, Arrays y Slices

Arrays

En Go los arrays son de de una longitud estatica. La declaracion de un array requiere que le especifiquemos el size, una vez este size sea establecido no puede crecer.

var scores [10]int
scores[0] = 339

El array de arriba puede almacenar 10 puntajes usando los indices scores[0] hasta el scores[9]. Intentar acceder por fuera de ese rango resultara en errores en tiempo de compilacion.

Podemos inicializar el array con valores:

scores := [4]int{9001, 9333, 212, 33}

Podemos usar len para obtener el largo del array.

len(scores) //4

range puede ser usado para iterar sobre ellos:

for index, value := range scores {

  }

Los arrays son eficientes pero rigidos. A menudo no sabemos el numero de elementos que estaremos recibiendo. Para eso nos pasaremos a slices.

Informacion adicional

Podemos crear un array con un length indefinido con la notacion […]

myArr := [...]int{1,2,3,4,....N}

Slices

En Go, raramente, si es que nunca, usaras arrays directamente. En su lugar usaremos slices. Un slice es una estrucutura ligera que envuelve y representa una porcion de un array.

Hay algunas maneras de crear un slice. Una es una variacion de como creariamos un array.

scores := []int{1,2,3,4,555,15034,2}

A diferencia de la declaracion de los arrays, nuestro slice no necesita declarar un largo entre los corchetes.

Podemos hacer uso de make para crear un slice:

scores := make([]int, 10)

En el codigo anterior inicializamos la longitud en 10.

Longitud (length):

  • La longitud de un slice se refiere al número actual de elementos que contiene el slice.
  • Se obtiene utilizando la función len().
  • La longitud refleja la cantidad de elementos reales en el slice.
  • Por ejemplo, si tienes un slice de longitud 5, significa que contiene 5 elementos.
  • Si no definimos una capacidad, esta sera por defecto la longitud que hayamos puesto.
miSlice := []int{1, 2, 3, 4, 5}
longitud := len(miSlice) // longitud es igual a 5
capacidad := cap(miSlice) // capacitdad es igual a 5

Entonces, después de ejecutar make([]int, 10), tendrás un nuevo slice de enteros que se ve así:

[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Otra forma es hacerlo definiendo la capacidad:

scores := make([]int, 0,10)

Esto crea un slice con una longitud de 0 y una capacidad de 10.

Capacidad (Capacity):

  • La capacidad de un slice se refiere al número máximo de elementos que puede contener el slice sin necesidad de asignar más memoria.
  • Se obtiene utilizando la función cap().
  • La capacidad es igual o mayor que la longitud.
  • Cuando un slice alcanza su capacidad máxima y se agregan más elementos, Go asigna automáticamente una nueva área de memoria y duplica la capacidad.
  • La capacidad es importante cuando se agrega dinámicamente elementos a un slice, ya que determina cuántos elementos puedes agregar antes de que se realloque memoria.
miSlice := make([]int, 0, 10) // Slice de longitud 0 y capacidad 10
capacidad := cap(miSlice)     // capacidad es igual a 10

Para entender mejor length y capacity veamos algunos ejemplos:

func main() {
  scores := make([]int, 0, 10)
  scores[7] = 1000
  fmt.Println(scores)
}

El codigo anterior es incorrecto porque estás intentando acceder a un índice fuera del rango válido del slice. En Go, cuando creas un slice utilizando make(), inicializas la longitud con 0, lo que significa que inicialmente no hay elementos en el slice. Luego, especificas una capacidad de 10, lo que significa que el slice tiene la capacidad de contener 10 elementos, pero la longitud es 0.

Para agregar elementos tenemos que hacerlo con append().

func main() {
  scores := make([]int, 0, 10)
  scores = append(scores, 5)
  fmt.Println(scores) // prints [5]
}

Esto agregara 5 como primer elemento scores[0] //5

Pero no es lo que buscabamos inicialmente, nosotros queriamos poner el valor 1000 en el indice 7, para lograrlo vamos a usar la notacion de corte, slice.

Esta notacion modifica tanto la longitud como la capacidad de un slice cuando se usa para crear un slice basado en otro.

func main(){
    scores := make([]int,0,10)
    scores = scores[0:8]
    scores[7] = 1000
  }

Por último, imprimes el slice scores, que ahora contiene los valores [0, 0, 0, 0, 0, 0, 0, 9033], ya que los elementos en los índices 0 a 6 todavía no han sido inicializados y, por defecto, tienen el valor 0.

La razón por la que este código funciona es que la notación de corte scores[0:8] modifica la longitud del slice y permite acceder a los elementos en el rango válido. Después de modificar la longitud, puedes asignar un valor al elemento en el índice 7 sin errores.

Que tanto podemos cambiar el tamanio de un slice? Hasta su capacidad que en este caso es 10

Podes pensar que actualmente no resolvimos el problema de los arrays con indice fijo. Pero append es especial. Si el array subyacente esta lleno, se creara una copia en otro espacio de memoria con el doble de capacidad. (así es exactamente como funcionan las matrices dinámicas en PHP, Python, Ruby, JavaScript).

Como ejemplo final a considerar.

func main() {
  scores := make([]int, 5)
  scores = append(scores, 9332)
  fmt.Println(scores)
}

Si usamos append en un slice que tiene longitud definada mayor a 0 veremos que el nuevo elemento se posicionara en len(score) + 1.

Por lo que el codigo anterior resultara en :

[0 0 0 0 0 9332]

Finalmente tenemos cuatro maneras de inicializar un slice

names := []string{"leto", "jessica", "paul"}
checks := make([]bool, 10)
var names []string
scores := make([]int, 0, 20)

Cuando usar cada una?

El primero, cuando conoces los valores que queres tener en el array.

El segundo es util cuando escribiremos en indices especificos de un slice.

func extractPowers(saiyans []*Saiyan) []int {
	powers := make([]int, len(saiyans))
	for index, saiyan := range saiyans {
		powers[index] = saiyan.Power
	}
	return powers
}

La tercera version es un slice con valor nil, es usado en conjunto con append, cuando el numero de elemetos es desconocido.

La ultima version nos permite especificar una capacidad unicial, util si sabemos mas o menos cuantos elementos necesitaremos.

Incluso cuando sabemos el size, append puede ser usado.Es una cuestion de preferencias.

func extractPowers(saiyans []*Saiyan) []int {
	powers := make([]int, 0, len(saiyans))
	for _, saiyan := range saiyans {
		powers = append(powers, saiyan.Power)
	}
	return powers
}

Informacion adicional

Slicing mantiene la referencia.

Al hacer un slice de un slice o array estamos manteniendo la referencia por lo que lo que le hagamos a uno le pasara al otro:

scores := []int{1, 2, 3, 4, 5}
	other := scores[2:4]
	other[0] = 999
	fmt.Println(scores) // [1 2 999 5]

Uso basico del Slicing

Al utilizar la notación de corte (slicing) en un slice en Go, puedes seleccionar un subconjunto de elementos de ese slice.

  • Si deseas incluir desde el principio del slice hasta una cierta posición, puedes omitir el índice de inicio (0) y simplemente escribir set[:5]. Esto te dará los primeros 5 elementos del slice.
  • De manera similar, si quieres incluir desde una cierta posición hasta el final del slice, puedes omitir el índice final. Por ejemplo, set[4:] te dará todos los elementos desde el índice 4 hasta el final del slice.
  • Si deseas incluir todos los elementos del slice sin omitir ninguno, puedes usar la notación set[:], lo que te dará una copia de todo el array original.

Perdiendo la referencia de un slice

Cuando un slice se queda sin capacidad o mejor dicho se sobrepasa a traves del uso de append, Go automáticamente duplica la capacidad asignando un nuevo array subyacente y copiando los elementos del slice original al nuevo array. Esto significa que se pierde la referencia al array original, y el slice apunta al nuevo array con capacidad duplicada.

func main() {
	//Se crea un slice con tamanio 0 y capacidad 2
	originalSlice := make([]int, 0, 2)
	//se llena con valores hasta su capacidad maxima
	originalSlice = append(originalSlice, 1, 2)
	//creamos otro slice con los valores de originalSlice y hacemos un
	//append, en este punto se rompe la referencia ya que se hizo
	//un relocate en la memoria y se extendio la capacidad a 4
	otroSlice := append(originalSlice, 3)

	fmt.Println(originalSlice) // [1 2]
	fmt.Println(otroSlice)     // [1 2 3]

	//Se modifica el indice 0 a 50
	otroSlice[0] = 50

	//observamos que el array original no fue  afectado
	fmt.Println(originalSlice)
	fmt.Println(otroSlice)

}

Maneras explicitas de copiar y perder la referencia.

Con append y el operador …
originalSlice := []int{1, 2, 3, 4, 5}

copiedSlice := append([]int(nil), originalSlice...)
Con coppy
originalSlice := []int{1, 2, 3, 4, 5}

copiedSlice := make([]int, len(originalSlice))

copy(copiedSlice, originalSlice)
Con bucle for
originalSlice := []int{1, 2, 3, 4, 5}

copiedSlice := make([]int, len(originalSlice))

for i, v := range originalSlice {
	copiedSlice[i] = v
}

Maneras explicitas de copiar y mantener la referencia.

Con slicing
originalSlice := []int{1, 2, 3, 4, 5}
copiedSlice := originalSlice[:]

Maneras de eliminar elementos de un slice.

  1. Con un for
func main() {
	original := []int{1, 2, 3, 4, 5, 6}
	removed := removeLoop(original, 5)
	fmt.Printf("%v", removed)//[1 2 3 4 5]

}

// No modifica el original
func removeLoop(slice []int, index int) []int {
	//como vamos a elimiar un elemento de slice creamos
	//uno nuevo con la copacidad del original menos uno.
	//make([]int,0,len(slice)-1)
	result := make([]int, 0, len(slice)-1)

	//si el indice del elemento del slice
	//es el mismo que le pasamos por parametro
	// lo saltamos, para que no lo tenga en cuenta
	//en esta iteracion,
	for i, _ := range slice {
		if i == index {
			continue
		}

		result = append(result, slice[i])
	}
	return result
}

Hay que tener cuidado cuando usamos for , ya que puede afectar al rendimiento de nuestra app, pensa que si tenes un slice de miles de elemetos, vas a recorrerlos todos!, ademas como vimos en el este post, si por algun motivo superamos la capacidad del nuevo slice result := make([]int, 0, len(slice)-1) se hara un relocate con el doble de memoria.

  1. Con un nuevo slice
package main

import "fmt"

func main() {
	original := []int{10, 20, 30, 40, 50, 60}
	removed := removeLoop(original, 3)
	fmt.Printf("%v", removed)

}

// No modifica el original
func removeNewSlice(slice []int, index int) []int {
	result := make([]int, len(slice)-1)
	// El primer parametro es el destino
	// El segundo es la fuente
	copy(result[:index], slice[:index])
	copy(result[index:], slice[index+1:])
	return result
}

De una manera mas visual seria asi como es que esta funcion elimina el elemento indicado (en este caso quitaremos el 40).

original tiene 6 de logitud

original = [] [] [] [] [] []

Result tiene esa misma cantidad menos uno, en este caso omitimos la capacidad y dejamos solo la longitud para tener acceso a los indices vacios.Re cordemos que cuando hacemos make([]int,0,10) solo estamso diciendo que el array subyacente tiene una capacidad de 10, pero no esta rellenado con nada, por lo que al querer acceder a un indice especifico resultara en error.

result := make([]int, len(slice)-1) = [] [] [] [] []

El primer copy, copia del indice 0 al 3 ([10,20,30,40]) pero como excluye al ultimo solo toma los primeros 3, [10,20,30]

copy(result[:index], slice[:index])

EL segundo copy copia desde el indice 3 hasta el final, pero en es este caso lo incluye,result[index:] en el indice 3 no tiene nada, ya que [10,20,30] es 0,1,2 en indices y slice[index+1:] seria a partir del 50 inclusive,hasta el final [50,60].

copy(result[index:], slice[index+1:])

De esta manera el resultado final seria [10,20,30,50,60]

  1. Con un solo copy pero modificando al original
package main

import (
	"fmt"
)

func main() {
	original := []int{10, 20, 30, 40, 50, 60}
	removed := removeCopy(original, 3)
	fmt.Printf("%v", original) // [10 20 30 50 60 60]
	fmt.Printf("%v", removed)  // [10 20 30 50 60 ]

}

// Si modifica el original
func removeCopy(slice []int, index int) []int {
	copy(slice[index:], slice[index+1:])
	return slice[:len(slice)-1]
}

  1. Con append pero modificando el original
package main

import (
	"fmt"
)

func main() {
	original := []int{10, 20, 30, 40, 50, 60}
	removed := removeAppend(original, 3)
	fmt.Printf("%v", original) // [10 20 30 50 60 60]
	fmt.Printf("%v", removed)  // [10 20 30 50 60 ]

}

// Si modifica el original
func removeAppend(slice []int, index int) []int {

	return append(slice[:index], slice[index+1:]...)
}
  1. Con el paquete Slices pero modificando al original
package main

import (
	"fmt"
	"slices"
)

func main() {
	original := []int{10, 20, 30, 40, 50, 60}
	removed := removeWithSlicesPackage(original, 3)
	fmt.Printf("%v", original) // [10 20 30 50 60 60]
	fmt.Printf("%v", removed)  // [10 20 30 50 60 ]

}

// Si modifica el original
func removeWithSlicesPackage(slice []int, index int) []int {

	return slices.Delete(slice, index, index+1)
}

Este ultimo solo disponible desde go 1.21

El orden de rendimiento es el siguiente de mas lento a mas rapido:

  1. Con un for (80ns)
  2. Con un nuevo slice (70ns)
  3. Con Copy modificando el original (4ns)
  4. Con append pero modificando el original (4ns)
  5. Con el paquete Slices pero modificando al original (4ns)

Encontrar indice de un elemento con slices

package main

import (
	"fmt"
	"strings"
)

func main() {
	original := "texto loco"
	x := strings.Index(original[:], " ")
	fmt.Printf("%v", x) //5 es el indice donde aparece el espacio
}

Maps

Los mapas en Go son lo que en otros lenguajes se llaman hashtables o diccionarios. Funcionan con una clave y un valor,y podes obtener, setear y eliminar valores con ellos.

Los mapas como los slices, son creados con la funcion make .

func main() {
	lookup := make(map[string]int)
	lookup["goku"] = 9001
	power, exists := lookup["vegeta"]
	// prints 0, false
	// 0 is the default value for an integer
	fmt.Println(power, exists)
}

Para saber el numero de keys usamos len y para eliminar un valor bansandonos en la key usamos delete

// returns 1
total := len(lookup)
// has no return, can be called on a non­existing key
delete(lookup, "goku")

Los mapas crecen de manera dinamica. Podemos definir el segundo parametro para setear un tamanio inicial

lookup := make(map[string]int, 100)

Si tenes una idea de cauntas keys va a tener tu map, inicializar con un tamanio puede ayudar al performance.

Cuando necesitamos un map como un campo en una estructura lo hacemos asi

type Saiyan struct {
	Name string
	Friends map[string]*Saiyan
}

Una manera de inicalizar lo anterior es asi :

goku := &Saiyan{
	Name: "Goku",
	Friends: make(map[string]*Saiyan),
}
goku.Friends["krillin"] = ... //todo load or create Krillin

Otra manera de declarar e inicializar un map es con su manera literal:

lookup := map[string]int{
	"goku": 9001,
	"gohan": 2044,
}

Podemos iterar sobre los mapas con renge

for key, value := range lookup {
	...
}

Las iteraciones sobre maps no son ordenadas. Cada iteracion sobre lookup retornara el par key value en un orden aleatorio.

Post Relacionados