Skip to content

Latest commit

 

History

History
215 lines (186 loc) · 8.83 KB

05.guardando-en-la-memoria.md

File metadata and controls

215 lines (186 loc) · 8.83 KB

Guardando en la memoria

En capítulos anteriores, estuvimos viendo los distintos tipos de datos de los que disponemos en Go. Ahora, vamos a bajar un poco a las profundidades del lenguaje para entender cómo se guarda todo esto en memoria y cómo trabajar con ello. Es decir,

vamos a aprender a trabajar con variables.

Vamos a introducir ahora un concepto que, si vienes de otros lenguajes de programación, conoces sí o sí: las variables. Podemos definir variable como un contenedor donde meter "cosas". Entendamos por "cosas" datos básicos o combinaciones de los mismos en lo que se denominan estructuras de datos. En Go declaras una variable de la siguiente forma: var <nombre de la variable> <tipo>. O si vas a asignar directamente un valor var <nombre de la variable> = <valor>. Incluso, puedes usar directamente :=. Ejemplos válidos serían:

gore> variando := 700
gore> var mivariable = 1

Lamentablemente, los tipos no tienen nombres tan sencillos como "número" o "cadena de caracteres". Pero no te preocupes, que vamos a verlo despacito.

Variables numéricas

Podemos empezar hablando de los números enteros (integers). Son aquellos sin una base decimal, ya sean positivos o negativos. Go tiene distintas representaciones de los números enteros: int, int8, int16, int32, int64, uint, uint8, uint16, uint32 y uint64. int es utilizado para representar números con signo, mientras que uint representa números enteros sin signo (números positivos o cero). Por otro lado, los números que acompañan a estas palabras reservadas están relacionados con la precisión de los números, es decir, cuántos bytes podemos almacenar en cada tipo. También disponemos de los alias byte, alias de uint8, y rune, un alias para int32. Si te decides por utilizar uint, int o uintptr, usado para los punteros, tienes que saber que su precisión puede variar en función de la arquitectura donde se haya compilado el programa.

También tenemos la posibilidad de trabajar con números decimales (floating point numbers). float32 y float64 son las palabras reservadas para trabajar con números decimales. Al igual que en el caso de los números enteros, 32 y 64 indican la precisión de la que disponemos para almacenar nuestro dato.

Por otra parte, tenemos los números complejos. Para representarlos se utilizan los tipos complex64 y complex128. En este caso, la parte real e imaginaria será un float32 en el caso de complex64 o un float64 si estás utilizando complex128.

¿Cómo decide Go que estoy trabajando con un int, un float o un complex? Retomamos el concepto de tipado pato del que ya hablamos en la introducción. Dependiendo de la pinta que tenga lo que se ha tecleado, Go lo considerará una cosa u otra: si anda como pato y camina como un pato, es un pato. Y si tiene números y un . decimal, pues será un float.

Cuando combinamos varias operaciones, el intérprete tiene que averiguar qué es lo que hay que aplicar primero. Los paréntesis ayudan: siempre se ejecuta primero la operación dentro de los paréntesis. En ausencia de paréntesis, hay que considerar las reglas de precedencia. Tienen incluso un nemónico: Please excuse my dear aunt Sally; tomando las iniciales, paréntesis, multiplicación, división y finalmente adición y sustracción. Si lanzas la siguiente operación:

gore>5*3-2+(2+3)

ejecutará primero el paréntesis, después la multiplicación y, finalmente, la resta y la suma. No es habitual que uno se encuentre con operaciones demasiado complicadas salvo transcribiendo fórmulas, pero es conveniente saberlo y en caso de duda siempre están los paréntesis, que además hacen más legible la expresión.

Ejercicio: usando una variable para recordar el valor anterior y ↑ para repetir la última orden en el intérprete, ir calculando una docena o más de valores de una progresión aritmética o geométrica.

Punteros

Otro tipo de números son los punteros. Los punteros son direcciones de memoria a otro valor. Si vienes de otros lenguajes como C o C++, no te asustes: en Go los punteros solo se usan como referencia. No existe la aritmética de punteros.

Para un tipo de dato T, declaramos un puntero como T*. Cuando un puntero no apunta a nada, tiene el valor nil.

Si quieres obtener el puntero a un valor, tienes que utilizar el operando &. Para acceder al valor dentro de un puntero, el operando *.

gore> var a = 55
gore> var b = &a
gore> b
(*int)(0xc42000e2b0)
gore> *b
55

Tras ver cómo tratar las cadenas de texto y los números en memoria, se han cubierto los tipos básicos de Go. Ahora toca ver

los tipos complejos (pero no mucho) de Go

Igual que en otros lenguajes, en Go también tenemos la posibilidad de agrupar datos para moldear conceptos más complejos. El primer, y más básico, son las estructuras (structs). Funcionan de la misma forma que en C o C++: son agrupaciones de elementos. Así, puedes definir un nuevo tipo de datos que contendrá otros tipos de datos básicos. Nada nuevo respecto a otros lenguajes.

type Persona struct {
  nombre string
	apellido string
  edad int
}

Los tipos de datos lineales también hacen su aparición en Go a través de los arrays. Funcionan igual que en la mayoría de lenguajes: elementos del mismo tipo a los que se puede acceder de forma secuencial. En este caso, es importante remarcar que el tamaño del array forma parte del tipo y, como consecuencia, no se puede cambiar. ¿Qué hago entonces si quiero una estructura secuencial que me permita cambiar el tamaño? Fácil: usa slices. Podemos decir que los slices son referencias a arrays. Vamos a verlo con un ejemplo:

gore> numeros:= [4]int{1,2,3,4}
[4]int{1, 2, 3, 4}
gore> a := numeros[0:2]
[]int{1, 2}
gore> b := numeros[1:3]
[]int{2, 3}
gore> fmt.Println(a, b)
[1 2] [2 3]
gore> b[0] = 99999
99999
gore> fmt.Println(a, b)
[1 99999] [99999 3]
gore> fmt.Println(numeros)
[1 99999 3 4]

¿Qué ha pasado? numeros es un array de enteros. De numeros, sacamos los slices a, que corresponde a las dos primeras posiciones de numeros, y b, que corresponde a la segunda y tercera posición de numeros. La segunda posición de numeros y a y la primera de b estarán apuntando a la misma dirección de memoria. En caso de modificar lo que hay en esa dirección, estaremos modificando el valor para todos los elementos que referencian a esa posición de memoria.

Mientras en los array hablamos de longitud, en los slices hablamos de longitud y capacidad. La longitud es el número de elementos que contiene y la capacidad es el número de elementos que puede llegar a albergar. Como un slice apunta a un array, podemos decir que la capacidad del slice es la longitud del array al que apunta. Para obtener la capacidad de un slice, puedes usar la función cap(). Si quieres evitar tener que crear explícitamente un array para trabajar con un slice, puedes usar la función make. Por ejemplo:

gore> miSlice:=make([]float32, 0, 3)
[]float32{}
gore> cap(miSlice)
3
gore> len(miSlice)
0
gore> miSlice=append(miSlice,1,2,3)
[]float32{1, 2, 3}
gore> len(miSlice)
3
gore> cap(miSlice)
3

Y así podemos declararlo todo de una vez. De paso, hemos visto cómo añadir elementos nuevos al slice mediante la función append(). Es importante que, tras llamar a la función append(), asignemos la salida a nuestro antiguo slice. append() nos devuelve un nuevo slice con los elementos que hayamos añadido.

También tenemos los maps, estructuras clave-valor. De nuevo, esta estructura de datos está disponible también en otros lenguajes. Imagina que queremos hacer un diccionario como el de la RAE:

gore> midiccionario := make(map[string]string)
map[string]string{}
gore> midiccionario["aprender"]="Adquirir conocimientos"
"Adquirir conocimientos"
gore> midiccionario["programar"]="Elaborar programas para su empleo en computadoras"
"Elaborar programas para su empleo en computadoras"
gore> midiccionario["aprender"]
"Adquirir conocimientos"
gore> 

Por supuesto, puedes crear slices de maps, de structs y de otros slices. Así como maps de slices y todas las combinaciones que se te ocurran.

Concluyendo

Tenemos tipos de datos básicos y otros más complejos. Los básicos nos permiten guardar números y caracteres. Los complejos no son más que una forma de guardar agrupaciones de tipos básicos. ¿Y todo esto para qué? Nuestros programas resolverán problemas y esos problemas tienen datos, que hay que guardar de alguna forma. En cada momento tendremos que elegir qué tipo de dato será el más correcto para representar un aspecto de nuestro problema o, dado el caso, solución.