Variables y Funciones
Ya tanemos Go instalado y listo para empezar. Escribimos unas cosas y de golpe aparecio en pantalla pero... ¿Qué fue lo que escribimos? Para explicar esto vamos a volver 3 pasos hacia atras y vamos a empezar a desmembrar ese código y hablar de las estructuras básicas que maneja Go y, claramente, como se usan.
Para esto empezaremos a definir dos terminos muy simples y complejos de todo lenguaje de programación ¿Qué es una Variable? y ¿Qué es un Función?
Una variable es una representación simbolica de un espacio de memoria que nosotros estamos reservando. Pero, ¿Qué significa esto?... Cuando nosotros necesitamos hacer operaciones, ya sea declarar un entero, una cadena de caracteres debemos reservar un espacio de memoria en nuestra computadora para poderla guardar y poderla utilizar. Una variable representa esto, un espacio de memoria en el sistema con un nombre simbolico que utilizaremos que normalmente es representativo.
Por otro lado, una función es una "subrutina" o "procedimiento" que contiene un segmento de código separado de otros bloques de código que puede ser invocado en cualquier momento desde la rutina principal u otra subrutina. Toda función tiene parametros de entrada y parametros de salida. Cuando se invoca o ejecuta su segmento de código dicha subrutina solo tendrá acceso a sus parametros de entrada y variables globales.
Bueno... Hasta aca creo que venimos bien... Pero si quedaron dudas, vamos a empezar a hablar de Golang y como interpreta todo esto el lenguaje.
Variables
Golang es un lenguaje de bajo tipado. ¿Qué significa esto? El lenguaje nos permite definir una variable sin indicarle el tipo de dato que va a tener. Por otro lado, tambien podemos definir una variable indicandole su tipado asignándole valor o no. De esta manera tenemos tres formas distintas de definir una variable:
package main
import "fmt"
func main() {
noTipado := 123456
var tipadoSinValor int
var tipadoConValor int = 456789
fmt.Println("Valor no tipado: ", noTipado)
fmt.Println("Valor tipado sin valor: ", tipadoSinValor)
fmt.Println("Valor tipado con valor: ", tipadoConValor)
}
Valor no tipado: 123456
Valor tipado sin valor: 0
Valor tipado con valor: 456789
Cabe resaltar que cuando nosotros declaramos una variable tipada sin valor Golang predeterminadamente le asigna uno dependiendo el tipo de dato. Cada valor varia del tipo de dato y lo veremos a continuación.
Golang posee tipos de datos básicos y tipos de datos compuestos. Vamos a ver.
Variables - Tipos de datos básicos
Dentro de Golang existen cinco tipos de datos basicos:
// Tipos de datos basicos
var soyUnBooleano bool;
var soyUnMensaje string;
var soyUnEntero int;
var soyUnFlotante float32;
var soyUnByte byte;
Bool
bool
Representa un valor verdadero o falso. Si no se especifica ningun valor, Golang asigna predeterminadamente el valor "0" (falso). A continuación un ejemplo:
package main
import "fmt"
func main() {
var booleanSinValor bool
var booleanConValor bool = true
fmt.Println("Boolean sin valor: ", booleanSinValor)
fmt.Println("Boolean con valor: ", booleanConValor)
}
Boolean sin valor: false
Boolean con valor: true
String
string
Representa una secuencia de caracteres. Un tipo de cadena por defecto es "". A continuación un ejemplo:
package main
import "fmt"
func main() {
var stringSinValor string
var stringConValor string = "Soy una cadena"
fmt.Println("String sin valor: ", stringSinValor)
fmt.Println("String con valor: ", stringConValor)
}
String sin valor:
String con valor: Soy una cadena
Integer
int
Representa un valor numérico entero. Su valor por defecto es cero. A continuación un ejemplo:
package main
import "fmt"
func main() {
var intSinValor int
var intConValor int = 123
fmt.Println("Entero sin valor: ", intSinValor)
fmt.Println("Entero con valor: ", intConValor)
}
Entero sin valor: 0
Entero con valor: 123
Float
float32
y float64
Representan numéros de punto flotante. La diferencia entre uno y el otro es el tamaño máximo que puede alcanzar siendo el primero de 32 de bits y el segundo de 64. Su valor por defecto es 0. A continuación un ejemplo:
package main
import "fmt"
func main() {
var floatSinValor float32
var floatConValor float32 = 17.23
fmt.Println("Flotante sin valor: ", floatSinValor)
fmt.Println("Flotante con valor: ", floatConValor)
}
Flotante sin valor: 0
Flotante con valor: 17.23
Byte
byte
Representa un entero de 8 bits sin signo. Su dato por defecto es 0. Este tipo de dato normalmente sirve para guardar datos que no sabemos que tipo tendran y luego trnasformarlos de uno a otro lado. A continuación un ejemplo:
package main
import "fmt"
func main() {
var byteSinValor byte
var byteConValor byte = 1
fmt.Println("Byte sin valor: ", byteSinValor)
fmt.Println("Byte con valor: ", byteConValor)
}
Byte sin valor: 0
Byte con valor: 1
Variables - Tipos de datos compuestos
Los tipos de datos compuestos son aquellos que poseen una iteracción entre los anteriores. Dentro de Go existen cuatro tipos:
array
: representa una colección de elementos del mismo tipo.slice
: es similar a un array, pero su tamaño puede ser modificado dinámicamente.map
: representa una colección de pares clave-valor.struct
: representa un conjunto de campos con diferentes tipos de datos.
Vamos a ver mas en detalle cada uno...
Array
El tipo de dato Array representa una coleccion de datos del mismo tipo. Los arrays en Go tiene una longitud fija que se define al momento de la creacion del array. Las posiciones dentro del array comienzan desde 0, osea el primer valor de la colección es el indice 0. El valor por default de un array es el valor default del tipo de dato básico que se este utilizando en cada una de sus posiciones. A continuación un ejemplo
package main
import "fmt"
func main() {
var arraySinValor [5]int
var arrayConValor [5]int = [5]int{1, 2, 3, 4, 5}
fmt.Println("Array sin valor: ", arraySinValor)
fmt.Println("Array con valor: ", arrayConValor)
// El indice comienza desde la posicion 0, por lo que el
// valor 3 es la posición 2 del array
fmt.Println("Valor 3 del array: ", arrayConValor[2])
}
Array sin valor: [0 0 0 0 0]
Array con valor: [1 2 3 4 5]
Valor 3 del array: 3
Slice
El tipo de datos Slice es similar a un array, pero su tamaño puede ser modificar dinámicamente. Un slice se crea a partir de un array y se puede acceder a sus elementos utilizando la mismas sintaxis que en un array. El valor predeterminado de un slice es la coleccion sin elementos (colección vacia). A continuación un ejemplo:
package main
import "fmt"
func main() {
var sliceSinValor []int
var sliceConValor []int = []int{1, 2, 3, 4, 5}
fmt.Println("Slice sin valor: ", sliceSinValor)
fmt.Println("Slice con valor: ", sliceConValor)
// Imprimo el tamaño del slice con valor
var tamañoSliceConValor = len(sliceConValor)
fmt.Println("Tamaño del slice con valor: ", tamañoSliceConValor)
// Agrego un elemento al slice
sliceConValor = append(sliceConValor, 6)
// Imprimo el nuevo tamaño del slice con valor
tamañoSliceConValor = len(sliceConValor)
fmt.Println("Nuevo tamaño del slice con valor: ", tamañoSliceConValor)
// Imprimo el nuevo Slice
fmt.Println("Nuevo slice con valor: ", sliceConValor)
// El indice comienza desde la posicion 0, por lo que el
// valor 3 es la posición 2 del slice
fmt.Println("Valor 3 del slice: ", sliceConValor[2])
}
Slice sin valor: []
Slice con valor: [1 2 3 4 5]
Tamaño del slice con valor: 5
Nuevo tamaño del slice con valor: 6
Nuevo slice con valor: [1 2 3 4 5 6]
Valor 3 del slice: 3
Por otro lado, Golang no posee la función de eliminar un índice en particular de un slice sino que debe hacerse a través de hacer sub slice (osea quedarse con una parte del slice original). Para esto, les dejamos un ejemplo de su funcionamiento.
package main
import "fmt"
func main() {
var mySlice []int = []int{1, 2, 3, 4, 5}
// Para obtener los primeros dos elementos del array
fmt.Println("Primeros dos elementos:", mySlice[:2])
// Para quedarme con todos los elementos posterior al indice 2 (inclusive)
fmt.Println("Ultimos elementos:", mySlice[2:])
// Para quitar el elemento de indice 2
fmt.Println("Slice sin el elemento 3:", append(mySlice[:2], mySlice[2+1:]...))
}
Primeros dos elementos: [1 2]
Ultimos elementos: [3 4 5]
Slice sin el elemento 3: [1 2 4 5]
Map
El Map representa una colección de pares clave-valor. Cada clave en el mapa debe ser única y el valor asociado puede ser cualquier tipo de datos. El valor por defecto es la colección sin elementos (colección vacia). Una vez generado el mapa se pueden acceder a sus valores indicando como "indice" el valor de la Key buscada. Cabe destacar que si la Key no existe el valor que retornará será el valor predefinido del tipo de dato que se encuentre como "valor". A continuación un ejemplo con todas las acciones posibles sobre un mapa:
package main
import "fmt"
func main() {
var mapSinValor map[string]int
// Declarar un map con claves de tipo cadena y valores de tipo entero
var mapConValor map[string]int = map[string]int{"Juan": 28, "Ana": 30}
fmt.Println("Map sin valor: ", mapSinValor)
fmt.Println("Map con valor: ", mapConValor)
// Verificamos la edad de Juan
fmt.Println("Valor para Juan: ", mapConValor["Juan"])
// Verificamos la edad de NoExiste
fmt.Println("Valor para NoExiste: ", mapConValor["NoExiste"])
// Agregamos el dato Nahuel y lo imprimimos
mapConValor["Nahuel"] = 32
fmt.Println("Valor para Nahuel: ", mapConValor["Nahuel"])
// Eliminamos a Ana e imprimimos el mapa
delete(mapConValor, "Ana")
fmt.Println("Map con valor: ", mapConValor)
}
Map sin valor: map[]
Map con valor: map[Ana:30 Juan:28]
Valor para Juan: 28
Valor para NoExiste: 0
Valor para Nahuel: 32
Map con valor: map[Juan:28 Nahuel:32]
Struct
El struct
representa un conjunto de campos con diferentes tipos de datos. Se utlizan para agrupar diferentes tipos de datos en un única variable. Para esto, debe ser definido previamente. El valor predeterminado es la estructura con sus datos con valor predeterminado dependiendo cada uno de sus tipos. A continuación un ejemplo:
package main
import "fmt"
// Declarar una estructura con diferentes campos
type Persona struct {
nombre string
edad int
altura float32
}
func main() {
var structSinValor Persona
var structConValor Persona = Persona{"Nahuel", 32, 1.77}
fmt.Println("Struct sin valor:", structSinValor)
fmt.Println("Struct con valor:", structConValor)
// Para acceder a una única propiedad
fmt.Println("Nombre del struct:", structConValor.nombre)
// Para redefinir una propiedad
structConValor.edad = 33
fmt.Println("Edad del struct:", structConValor.edad)
}
Struct sin valor: { 0 0}
Struct con valor: {Nahuel 32 1.77}
Nombre del struct: Nahuel
Edad del struct: 33
Variables - Transformación cadena <-> byte
Bueno, ya conocemos todos los tipos de datos que usaremos a lo largo de nuestra cursada pero falta un poquito mas... A lo largo de nuestro trabajo práctico por diferentes razones seguro tengamos que transformar una cadena (string
) en un array de bytes. Sin indagar mucho en el porque vamos a explicar el como se hace y lo que pasa al hacerlo.
package main
import "fmt"
func main() {
//Definimos las variables
var cadena string = "Hola"
var arrayBytes []byte
// Transformamos nuestra cadena en un array de bytes
arrayBytes = []byte(cadena)
// Imprimimos el array
fmt.Println("Array de bytes:", arrayBytes)
// Convertimos el array de bytes en una cadena
var nuevaCadena string = string(arrayBytes)
// Imprimimos la nueva cadena
fmt.Println("Nueva cadena:", nuevaCadena)
}
Array de bytes: [72 111 108 97]
Nueva cadena: Hola
Nuestro código posee una transformación de una cadena con el valor "Hola" a través de la sentencia []byte(cadena)
. Esta sentencia lo que hace es "descomprimir" la cadena original definiendo el valor de cada uno de sus caracteres a su interpretación en UTF-8. De esta manera el resultado nos queda un array de 4 caracteres que representan las cuatro letras de nuestra cadena.
Luego para volver a obtener la cadena a partir del array de bytes se ejecuta la operación string(arrayBytes)
obteniendo nuevamente la palabra "Hola".
Hasta aca todo es muy lindo y color de rosas peeero... La interpretacón de UTF-8 no administra todos los "simbolos" de nuestro lenguaje. Vayamos con un nuevo ejemplo:
package main
import "fmt"
func main() {
//Definimos las variables
var cadena string = "ABC€"
var arrayBytes []byte
// Transformamos nuestra cadena en un array de bytes
arrayBytes = []byte(cadena)
// Imprimimos el array
fmt.Println("Array de bytes:", arrayBytes)
// Convertimos el array de bytes en una cadena
var nuevaCadena string = string(arrayBytes)
// Imprimimos la nueva cadena
fmt.Println("Nueva cadena:", nuevaCadena)
}
Array de bytes: [65 66 67 226 130 172]
Nueva cadena: ABC€
En este ejemplo estamos usando el simbolo "€" que en codigo UTF-8 se representa por 3 bytes!! Así tenemos que nuestra cadena en vez de ocupar cuatro bytes como en el primero ejemplo acá ocupa 6.
La pregunta es, ¿Cual es el objetivo de esto? Si en algún momento (SPOILER ALERT!!) deben guardar cosas en un []byte
tienen que saber que no siempre una secuencia de N caracteres va a ocupar N bytes. Y como deberán manejar esto, con sabiduria, inteligencia (y spoiler, las operaciones que antes vimos de slices como por ejemplo len()
).
Funciones
Golang maneja funciones a través de su palabra reservada func
. Las funciones de Golang deben ser declaradas por medio de una firma en la cual se especifiquen que valores reciben y que valores retornan. Para esto, haremos unos ejemplos:
package main
import "fmt"
func main() {
var valor1 int = 1;
var valor2 int = 2;
var suma int = sumar(valor1, valor2)
fmt.Println("Resultado:", suma)
}
// Definición de función
func sumar(a int, b int) int {
return a + b
}
Resultado: 3
Acá vemos que declaramos dos variables e invocamos a la función suma pasandole dichos valores como argumentos. Nuestra función sumar
recibe dos parametros a
y b
y realiza un retorno con valor int
expresado en la linea return a + b
Cabe aclarar que el orden de los parametros puede no ser arbitrario y el resultado de una función no siempre puede corresponder con el de sus parametros de entrada. Veamos un ejemplo:
package main
import "fmt"
func main() {
var valor1 int = 20
var valor2 int = 8
fmt.Println("Dividir Enteros 20/8:", divideInts(valor1, valor2))
fmt.Println("Dividir 20/8:", divideFloat(valor1, valor2))
fmt.Println("Dividir 8/20:", divideFloat(valor2, valor1))
}
func divideInts(a int, b int) int {
return a / b
}
func divideFloat(a int, b int) float32 {
return float32(a) / float32(b)
}
Dividir Enteros 20/8: 2
Dividir 20/8: 2.5
Dividir 8/20: 0.4
Primero vemos que aca tenemos dos funciones. divideInts
se encarga de dividir dos enteros y retornar su valor mientras que divideFloat
se encarga de dividir dos enteros pero antes los transforma en float32
. Golang cuando opera sobre dos tipos de dato del mismo tipo retorna un valor del MISMO tipo de dato. Esto significa que al dividir dos enteros se obtendra otro ENTERO y no la "división" que todos esperabamos. De esta manera, vemos que divideFloat
recibe dos enteros y retorna un flotante.
Ahora, podemos tener una función que no tenga un valor de retorno. Veamos un ejemplo:
package main
import "fmt"
func main() {
var valor1 int = 20
var valor2 int = 8
imprimir(valor1)
imprimir(valor2)
}
func imprimir(a int) {
fmt.Println("El valor es:", a)
}
El valor es: 20
El valor es: 8
En esta función vemos que tenemos una función imprimir
que se encarga solamente de imprimir en pantalla el argumentos que le llega sin tener retorno alguno.
Scopes - Variables y Funciones globales
Hemos visto que es una variable y que es una función. Ahora empecemos a hablar de scope
. Utilizamos la palabra scope
para definir la región en la que existe una variable o función dentro de nuestro código, osea desde "donde se puede acceder a ellos". De esta manera, podemos empezar a definir variables o funciones que sean accesibles por todos o solo dentro de determinado fragmento de codigo. Analicemos un poco mas:
package main
import "fmt"
func main() {
var valor1 int = 1
var valor2 int = 2
var suma int = sumar(valor1, valor2)
fmt.Println("Resultado:", suma)
}
func sumar(a int, b int) int {
return a + b
}
Resultado: 3
En este ejemplo que vimos antes, tenemos dos variables valor1
y valor2
que se encuentran dentro del scope de la función main
indicando que SOLO pueden ser usadas dentro de dicha función. Ahora podemos mover la variable valor1
a un scope global
package main
import "fmt"
var valor1 int = 3
func main() {
var valor2 int = 2
fmt.Println("Resultado sumar2:", sumar2(valor2))
}
func sumar2(b int) int {
return valor1 + b
}
Resultado sumar2: 5
En este ejemplo, vemos que tenemos una variable declarada de forma GLOBAL pudiendo acceder la funcion sumar2 directamente al valor de la variable valor1
. Pero, que pasa si empiezo a jugar descaradamente con los scopes...
package main
import "fmt"
var valor1 int = 3
func main() {
var valor1 int = 1
var valor2 int = 2
fmt.Println("Resultado sumar:", sumar(valor1, valor2))
fmt.Println("Resultado sumar2:", sumar2(valor2))
}
func sumar(a int, b int) int {
return a + b
}
func sumar2(b int) int {
return valor1 + b
}
Resultado sumar: 3
Resultado sumar2: 5
Viendo este ejemplo tenemos una variable global llamada valor1
que es accesible por todas las funciones. Dentro de nuestra función main
vemos que declaramos una nueva variable con el mismo nombre pero asignando otro valor. Golang primero va a buscar la variable desde el scope actual hasta el mas externo. Dentro del scope de main cuando llamemos a valor1
estaremos hablando de la variable que tiene como valor 1 resultando que el valor de sumar
sea 3, mientras que cuando invocamos a sumar2
se utilice el valor de la variable declarada globalmente resultando el valor 5 ya que dentro del scope de dicha función no existe una variable declarada con dicho valor.
El objetivo de esto es "CUIDAR" como se nombran a las variables en nuestro código. Golang va a ser exclusivamente lo que nosotros le indiquemos y va a tomar el valor de la variable en el scope en el que este en ese momento.
Por ultimo y de manera semejante se pueden declarar funciones dentro de funciones. Osea podemos declarar una función con distinto scope. Veamos un ejemplo rapido.
package main
import "fmt"
func main() {
var valor1 int = 1
var valor2 int = 2
var sumar = func(a int, b int) int {
return a + a + b
}
fmt.Println("Resultado sumar:", sumar(valor1, valor2))
}
func sumar(a int, b int) int {
return a + b
}
Resultado sumar: 4
En este ejemplo vemos que declaramos dentro de la funcion main una nueva variable sumar
que representa una funcion que recibe dos parametros (de igual manera a la que esta de manera global). Al igual que con las variables, Golang va a ir buscando la funcion sumar
desde el scope actual hasta el mas externo. Al tener tanto una funcion global como una particular definida dentro del scope de la función main
terminará ejecutando la interna.
A donde queremos llegar con esto... Golang nos permite la construcción de nuestro código de manera Imperativa (código lineal con funciones e instrucciones paso a paso) o nos permite si somos muy cuidadoso trabajar nuestro código orientado a Objetos. Esto lo veremos cuando estemos mas avanzados en este tutorial.