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 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.
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.
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
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.
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.