-
Notifications
You must be signed in to change notification settings - Fork 0
Aprende Go Desde 0
Ejecuta el msi descargado en el caso de windows o descomprime donde quieras si lo prefieres antes que un instalador, asegurate de que sea el cliente de 64 bits
go version -> go version go1.17.1 windows/386
where go -> C:\Users\MaQuiNa1995\Herramientas\Go\bin\go.exe
- GOPATH
Es la ruta donde tendrás tu Go descargado Valor por defecto: %USERPROFILE%\go
- PATH
PATH -> otrasEntradas;%GOPATH%
- GOPATH
Es la ruta donde tendrás tu Go descargado Valor por defecto: $HOME/go
Al ejecutar el siguiente comando deberías de obtener una respuesta parecida
go version -> go version go1.17.1 windows/386
Como IDE usaremos Visual studio code en el que al entrar te dará la opción de descargar e instalar módulos que puedan sernos útiles como autocompletado y demás
Descárgalo aquí desde la página oficial
Cuando lo abrais un pop-up os dará la opción de instalar esos módulos que hablé antes cuando estén todos listos os pondrá en la terminal algo parecido a "you are ready to Go :)"
Si habeis instalado el cliente de 32 bits de golang tendreis problemas con el analizador de código y no podreis usarle
Crearemos un proyecto Golang y del sistema de archivos que nos creará iremos a la carpeta src y crearemos una carpeta adicional será en esta donde crearemos nuestro archivo Main.go con el siguiente contenido
package main
import (
"fmt"
)
func main() {
fmt.Println("Hola mundo")
}
Para ejecutar el programa iremos a la vista de la terminal en nuestro visual studio y como este estará ya ubicado en la carpeta de nuestro proyecto solo tendremos que ejecutar el siguiente comando
go run .\src\carpetaCreadaPreviamente\Main.go
Adicionalmente tambien podeis a una consola online y probar allí el mismo código
Deberíamos de poder ver nuestro hola mundo
package main
Aqui irían todas las declaraciones de importación sobre librerías de sistema por poner un ejemplo
import (
"fmt"
"strconv"
)
func main() {
// Aqui iría el código de nuestro punto de entrada al proyecto
}
En go tenemos diferentes formas de declaración de variables
- Pascal case -> Export variables (Variables Globales)
- Camel Case -> internal variables (Variables de paquete)
https://golangbyexample.com/all-data-types-in-golang-with-examples/#Basic_Types
Tipo | Tamaño |
---|---|
int | Dependiente De Plataforma |
int8 | 8 bits/1 byte |
int16 | 16 bits/2 byte |
int32 | 32 bits/4 byte |
int64 | 64 bits/8 byte |
Tipo | Tamaño |
---|---|
uint | Dependiente De Plataforma |
uint8 | 8 bits/1 byte |
uint16 | 16 bits/2 byte |
uint32 | 32 bits/4 byte |
uint64 | 64 bits/8 byte |
Tipo | Tamaño |
---|---|
float32 | 32 bits or 4 bytes |
float64 | 64 bits or 8 bytes |
Tipo | Tamaño |
---|---|
complex64 | Parte real e imaginaria son float32 |
complex128 | Parte real e imaginaria son float64 |
El tipo complejo por defecto es complex128
byte es un alias para uint8 vamos que se considera un valor entero de 8 bits y representa un byte (0-255) este a su vez puede representar un caracter ASCII Golang no tiene un tipo char aluso como por ejemplo Java
rune es un alias para int32 por lo tanto se le considera un entero se usa para representar un Unicode Code Point
Unicode es un superconjunto de caracteres ASCII que asigna un número único a cada carácter que existe. Este número único se llama Unicode Code Point
Algunos ejemplos:
- El dígito 0 se representa como punto Unicode U+0030 (valor decimal – 48)
- La letra pequeña b se representa como punto Unicode U+0062 (valor decimal – 98)
- Un símbolo de libra £ se representa como Punto Unicode U+00A3 (Valor decimal – 163)
Puedes aprender mas sobre esto en: The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets
utf-8 guarda cada Unicode Code Point utilizando 1, 2, 3 o 4 bytes.
Los puntos ASCII se almacenan utilizando 1 byte. Es por eso que rune es un alias para int32 porque un Unicode Code Point puede ser de un máximo de 4 bytes en Go, ya que cada cadena está codificada usando utf-8 cada rune está destinada a referirse a un Unicode Code Point.
Por ejemplo, si imprime una cadena después de encasillarla en una matriz de runes, imprimirá el Unicode Code Point para cada uno de los caracteres. Para la siguiente cadena "0b£" la salida será – [U+0030 U+0062 U+00A3]
En go se puede declarar una String con comillas simples y con comillas dobles
Pueden tomar o true o false Por defecto su valor es false
- AND –> &&
- OR –> ||
- Negación –> !
var numero int = 1
var numero3 int
numero3 = 3
numero2 := 2
El único problema de esta forma de declaración es que cuando, por ejemplo tenemos numero2 := 2.
que representaría un número decimal, es que nos inicializa la variable con float64 no hay ninguna forma de crear un float32 por esta vía
Podemos como en otros lenguajes usar la asignación múltiple con cualquier de las 3 formas anteriores
var nombre, github string = "MaQuiNa", "https://github.com/MaQuiNa1995"
numero, numero2 := 1, 2
profesiones := [3]string{"Caballero Cebolla", "Ilusionista", "Francotirador"}
En esta forma se inicializa de manera completa el array declarando el tamaño y los elementos que lo conforman
profesiones2 := [...]string{"Caballero Cebolla", "Ilusionista", "Francotirador"}
Esta forma es equivalente a la anterior solo que omitiendo el tamaño ya que en este caso resulta dedundante ya que tenemos 3 elementos
Hay casos en los que no deberías de utilizar esta forma ya que tendrías que ir 1 a 1 contando manualmente los elementos entre { } entonces solo eligiría esta manera si es visualmente muy fácil saber cuentos elementos conforman el array
var profesiones3 [3]string
profesiones3[0] = "Caballero Cebolla"
profesiones3[1] = "Ilusionista"
profesiones3[2] = "Francotirador"
En esta forma dejamos la declaración de los elementos para "mas tarde"
Como dato adicional
- Aqui no se pueden usar los [...] que utilizamos en la anterior forma
- Se pueden dejar posiciones vacías en cuyo caso dependiendo del tipo de objeto cogerían el valor por defecto
profesiones := [...]string{"Caballero Cebolla", "Ilusionista", "Francotirador"}
profesionesCopia := profesiones
profesionesCopia[0] = "Mago Azul"
fmt.Printf("Profesiones: %v\nProfesiones Copia: %v\n", profesiones, profesionesCopia)
Resultado:
Profesiones: [Caballero Cebolla Ilusionista Francotirador]
Profesiones Copia: [Mago Azul Ilusionista Francotirador]
En Go cada "copia" ya sea por asignación o por el envío de estos a una función genera una nueva copia de estos, en otros lenguajes siempre se apunta a la misma dirección de memoria y si modificas uno el otro tambien se ve afectado, en Go no es asi siempre se crea uno nuevo a no ser que hagas lo siguiente
profesiones := [...]string{"Caballero Cebolla", "Ilusionista", "Francotirador"}
profesionesCopia := &profesiones
profesionesCopia[0] = "Domador"
fmt.Printf("Profesiones: %v\nProfesiones Copia: %v\n", profesiones, profesionesCopia)
Resultado:
Profesiones: [Domador Ilusionista Francotirador]
Profesiones Copia: &[Domador Ilusionista Francotirador]
Con el modificador & en la asignación le decimos a go que los dos arrays apunten a la misma dirección de memoria por lo tanto si modificas uno, el otro tambien se ve afectado
Si recuerdas de los arrays si no usábamos el operador &
no provocabamos que 2 variables apuntasen al mismo hueco de memoria en los slice esto siempre es asi de tal manera que si cambiamos una slice el otro tambien se verá afectado, un ejemplo:
profesiones := []string{"Caballero Cebolla", "Ilusionista", "Francotirador"}
profesionesCopia := profesiones
profesionesCopia[0] = "Alquimista"
fmt.Printf("Profesiones: %v\nProfesiones Copia: %v\n", profesiones, profesionesCopia)
Resultado:
Profesiones: [Alquimista Ilusionista Francotirador]
Profesiones Copia: [Alquimista Ilusionista Francotirador]
Los slice son y tienen la misma funcionalidad que un array con alguna que otra excepción que luego veremos
sliceOriginal := []int{1, 2, 3, 4, 5}
fmt.Printf("Slice Original: %v\n", sliceOriginal)
slice2 := sliceOriginal[:]
fmt.Printf("Slice Copia de Slice Original: %v\n", slice2)
Una regla que siguen las siguientes formas dependiendo donde esté el número a la derecha o izquierda o en ambos de :
Inclusivo:Exclusivo
Tambien decir que está nomenclatura tambien se aplica a Arrays
slice3 := sliceOriginal[3:] //Se descartan los 3 primeros valores de sliceOriginal
slice4 := sliceOriginal[:4] // Se cogen solo los 4 primeros elementos de sliceOriginal
slice5 := sliceOriginal[2:4] // Se descartan los 2 primeros elementos y se obtienen los siguientes elementos (despues del primer descarte) hasta la posicion X del slice original
fmt.Printf("Slice Cortado con principio definido: %v\n", slice3)
fmt.Printf("Slice Cortado desde el inicio hasta un final definido: %v\n", slice4)
fmt.Printf("Slice Cortado descartando X elementos y seleccionado Y de los primeros restantes: %v", slice5)
Podemos usar 2 argumentos
- tipo -> []int
- Tamaño -> 4
slice := make([]int, 4)
fmt.Println(slice)
fmt.Printf("Tamaño: %v\n", len(slice))
fmt.Printf("Capacidad: %v\n", cap(slice))
Resultado:
[0 0 0 0]
Tamaño: 4
Capacidad: 4
Tambien podemos usar 3 argumentos
- tipo -> []int
- Tamaño -> 4
- Capacidad -> 4
slice2 := make([]int, 4, 10)
fmt.Println(slice2)
fmt.Printf("Tamaño: %v\n", len(slice2))
fmt.Printf("Capacidad: %v", cap(slice2))
Resultado:
[0 0 0 0]
Tamaño: 4
Capacidad: 10
Podemos añadir y quitar elementos de un slice dinámicamente a diferencia de un array no tiene un tamaño predefinido
slice := []string{}
fmt.Println(slice)
fmt.Printf("Tamaño: %v\n", len(slice))
fmt.Printf("Capacidad: %v\n", cap(slice))
slice = append(slice, "Mago del Tiempo") // Tenemos que igualarlo al slice
fmt.Println(slice)
fmt.Printf("Tamaño: %v\n", len(slice))
fmt.Printf("Capacidad: %v\n", cap(slice))
A tener muy en cuenta: cuando un slice tiene capacidad 2 y tamaño 2 (Osea está lleno) y metemos un nuevo elemento la capacidad del slice se doblará en el caso de que el slice fuese su capacidad de 0 pasará a 2
Con el siguiente ejemplo se ve muy claro:
slice := []string{"Mago del Tiempo", "Monje"}
fmt.Println(slice)
fmt.Printf("Tamaño: %v\n", len(slice))
fmt.Printf("Capacidad: %v\n", cap(slice))
slice = append(slice, "Paladín")
fmt.Println(slice)
fmt.Printf("Tamaño: %v\n", len(slice))
fmt.Printf("Capacidad: %v\n", cap(slice))
Resultado:
[Mago del Tiempo Monje]
Tamaño: 2
Capacidad: 2
[Mago del Tiempo Monje Paladín]
Tamaño: 3
Capacidad: 4 // la capacidad se ha doblado
Se pueden añadir de 2 a mas elementos a un slice añadiendo en el append mas argumentos:
slice := []string{"Mago del Tiempo", "Monje"}
slice = append(slice, "Paladín","Soldado","Dragontino")
Podemos fusionar slices ya sea por variable o porque se ha instanciado directamente uno en el propio append
slice := []string{"Mago del Tiempo", "Monje"}
slice2 := []string{"Paladín", "Soldado", "Dragontino"}
slice = append(slice, slice2...) // Fusión por variable
slice = append(slice, []string{"Domador", "Cazador"}...) // fusión por slice instanciado
Se pueden elemininar elementos de un slice de distintas maneras
Con esta técnica crearíamos un nuevo array omitiendo valores del slice original
Cortar por el principio
slice := []string{"Mago del Tiempo", "Monje"}
// Crearíamos un nuevo slice omitiendo el primer elemento de "slice"
slice2 := slice[1:]
Cortar por el final
slice := []string{"Mago del Tiempo", "Monje"}
// Crearíamos un nuevo slice omitiendo el último elemento del original
slice2 := slice[:len(slice)-1]
Aqui las cosas se ponen un poco espinosas teniendo que hacer nuevos slices de las partes del slice original, se ve mejor visualmente con un ejemplo:
slice := []string{"Mago del Tiempo", "Monje", "Paladín", "Elementalista", "Esgrimista"}
// Crearíamos un nuevo slice omitiendo el último elemento del original
// slice[:2] -> crea un array con: Mago del Tiempo Monje
// slice[4:]... -> añade a ese array creado los elementos resultantes de descartar 4 elementos del slice original
slice2 := append(slice[:2], slice[4:]...)
// imprime: [Mago del Tiempo Monje Esgrimista]
// se han eliminado los elementos centrales: Paladín y Elementalista
fmt.Print(slice2)
Consiste en un par de valores primero la key y luego el value ambos pueden tener cualquier tipo
A tener en cuenta los mapas apuntan siempre a la misma referencia asique si lo pasamos a una función y manipulamosese mapa fuera de la función ese objeto tambien se verá afectado por los cambios
// map[tipo de la key] tipo del value
profesiones := map[int]string{
1: "Soldado",
2: "Paladín",
3: "Dragontino",
4: "Lancero",
}
fmt.Println(profesiones)
profesiones2 := make(map[int]string)
fmt.Println(profesiones2)
Se suelen hacer por key para obtener el value de tal manera:
profesiones := map[int]string{
675: "Soldado",
432: "Paladín",
321: "Dragontino",
968: "Lancero",
}
// no imprimiría nada ya que no existe ninguna key que valga 3
fmt.Println(profesiones[3])
// En cambio esto si que imprimiría Dragontino porque el valor 321 existe de key
fmt.Println(profesiones[321])
Tenemos otra forma de leer entradas de un mapa leyendo directamente el par o la key del mismo
profesiones := map[int]string{
675: "Soldado",
432: "Paladín",
321: "Dragontino",
968: "Lancero",
}
trabajo, ok := profesiones[675]
// Soldado true
fmt.Println(trabajo, ok)
trabajo2, ok2 := profesiones[999]
// false
fmt.Println(trabajo2, ok2)
trabajo -> value del mapa
ok -> booleano indicando la existencia en el mapa de esa key (profesiones[key]
)
En el caso de que solo pongamos uno de los 2 sacaríamos el value de esa key
trabajo := profesiones[675]
fmt.Println(trabajo)
Tambien podemos descartar el valor y quedarnos con el booleano de si existe o no:
_ , ok := profesiones[675]
fmt.Println(ok)
A tener en cuenta si sacamos algo que no existe: key -> el valor por defecto (String = " " y numeros = 0) value -> false
como en los arrays aqui tambien tenemos la función len() para ver el tamaño del mapa
Podemos añadir valores como se puede observar aqui:
profesiones := map[int]string{
675: "Soldado",
432: "Paladín",
321: "Dragontino",
968: "Lancero",
}
// No sacaría nada
fmt.Println(profesiones[6])
profesiones[6] = "Samurai"
// Imprimiría Samurai ya que ahora si está en el mapa
fmt.Println(profesiones[6])
Una cosa al tener en cuenta es que cuando añadimos un elemento el mapa puede cambiar totalmente la ordenación
Podemos usar la funcion delete para eliminar entradas del mapa por id, un ejemplo:
profesiones := map[int]string{
675: "Soldado",
432: "Paladín",
321: "Dragontino",
968: "Lancero",
}
// mapa , id de la entrada a eliminar
delete(profesiones, 432)
fmt.Println(profesiones)
Viene a referirse a objetos en la vida cotidiana, lo que vendría siendo una clase en java por ejemplo
Copias de ese struct no apuntan a la misma referencia por lo que si cambias una la otra no se ve afectada para que sean iguales debemos usar "&" para indicar que esas 2 variables aputan a la misma referencia, es tal cual se hace en Arrays
Debemos tener los struct a nivel de paquete
type Tecnica struct {
nivelRequerido int
nombre string
mana int
}
Se puede declarar un struct directamente en una función
tecnica := struct{ nivelRequerido int }{nivelRequerido: 50}
fmt.Print(tecnica)
tecnica := Tecnica{
nivelRequerido: 10,
nombre: "Última Pesadilla",
mana: 40,
}
fmt.Print(tecnica) // Si queremos consultar todos los valores del struct
fmt.Print("\n" + tecnica.nombre) // Si queremos consultar un valor en específico
Hay otra manera de instanciar un struct eliminando los nombres de los campos en este cada atributo corresponde exactamente a la posicion de los campos del struct a esta técnica se le llama "Positional Syntax"
no es recomendable su uso por razones de mantenibilidad ya que si cambias el orden de los campos o añades otro lo vas a tener dificil para mantenerlo
un ejemplo:
tecnica := Tecnica{
10, // Corresponde con la 1º posición del struct nivelRequerido
"Última Pesadilla", // nombre
40, //mana
}
En go no existe la herencia pero tenemos algo parecido usaremos el "embedding"
type Magia struct {
mana int
}
type MagiaNegra struct {
Magia
danno int
}
func main() {
magiaNegra := MagiaNegra{}
magiaNegra.mana = 10
magiaNegra.danno = 500
// {{10} 500}
fmt.Println(magiaNegra)
// 10
fmt.Println(magiaNegra.mana)
// 500
fmt.Println(magiaNegra.danno)
}
En otros lenguajes tradicionales tendríamos una herencia de magia negra con magia, pero en Go tendríamos que embeber Magia dentro de Magia negra
Los tags son usados para añadir constraints o limitaciones esto no actua directamente sobre el struct sino que es usado por frameworks de terceros o uno custom que haga uso de esa información para cierto fin usando el paquete de reflection
Básicamente es añadir metadatos a los campos de un struct para un framework ya sea de terceros o custom use esa información para validar algo o saber como des-serializar un json xml etc
type Tecnica struct {
nivelRequerido int
nombre string `required,max:"10"`
mana int
}
func main() {
tecnicaReflection := reflect.TypeOf(Tecnica{})
campo, _ := tecnicaReflection.FieldByName("nombre")
// required,max:"10"
fmt.Print(campo.Tag)
}
Al igual que las variables si ponemos mayúscula la primera letra querrá decir que están expuestas a toda la aplicación pero en cambio si está en minúscula estará disponible a nivel de paquete solamente, usaremos pascal naming
Van precedidas de la palabra const
const constante string = "constante"
-
const constante = "constante"
Como dato curioso en otros lenguajes tipo java puedes tener asignacioens de constantes que dependan en la ejecución de una función por ejemplo es decir que se resuelvan en tiempo de ejecución , en Golang eso no se puede pero sin embargo las operaciones matemáticas entre literales o constantes si que están permitidas
A nivel de compilador:
- Son inmutables puede aplicarse el "shadow"
- Son reemplazadas por el compilador en tiempo de compilación
- El valor debe poder ser calculado en tiempo de compilación
const(
numero1 = 1
numero2 = 2
numero3 = numero1 + numero2 // valdría 3
)
Y como último dato importante no se puede hacer una constante de una colección, estos siempre serán variables
Suelen ir a nivel de paquete y mas raramente a nivel de función (aunque no deberías)
puedes usar a nivel de paquete la siguiente forma de declarar una enumeracion de constantes
const (
constanteEnumerada = iota // valdría 0
constanteEnumerada2 = iota // valdría 1
constanteEnumerada3 = iota // valdría 2
)
En este caos al usar iota este actua como un contador que empieza desde 0 y va incrementando su valor en 1
Si quitamos el iota de la constante 2 y 3 tendríamos el mismo resultado ya que el compilador de go asigna a las constantes que no están inicializadas explicitamente el ultimo valor seteado de tal manera
const (
constanteEnumerada = iota // valdría 0
constanteEnumerada2 // valdría 1
constanteEnumerada3 // valdría 2
)
Ahora si en vez de usar iota usamos un literal todas las constantes por debajo de esa tendrán ese valor hasta que se setee otro literal de tal manera
const (
constanteEnumerada = 1 // Valdría 1
constanteEnumerada2 // Valdría 1
constanteEnumerada3 = 2 // Valdría 2
constanteEnumerada4 // Valdría 2
)
Ahora bien si tenemos por ejemplo 2 bloques de constantes y usamos iota en los 2 iota el valor se reiniciara para cada bloque (Osea que el scope de iota esta ligado a cada bloque de constantes ) de tal manera
const (
constanteEnumerada = iota // valdría 0
constanteEnumerada2 // valdría 1
)
const (
constanteEnumerada3 = iota // valdría 0
)
Recuerda que las operacioens matemáticas entre constantes o literales están permitidas asique podríamos hacer algo parecido a esto
const (
constanteEnumerada = iota + 5 // valdría 5
constanteEnumerada2 // valdría 6
)
o algo un poco mas útil sería aprobechar esa funcionlidad para hacer potencias a traves del operador bitwise (<<) como dijimos antes no se pueden llamar a funciones y en Go para hacer el tema de potencias necesitas usar la librería de math por lo tanto para poder hacer potencias tendríamos algo como esto:
const (
_ = iota
KB = 1 << (10 * iota) // Con esto conseguimos una potencia 10^1
MB // Con esto conseguimos una potencia 10^2 etc
GB
TB
PB
EB
ZB
YB
)
func main() {
// Aqui tenemos un decimal que representa el peso de un fichero
fileSize := 4000000000.
/*
Con el Printf aplicamos cierto formato
%.2f -> decimal de precisión 2
concatenado con " GB"
*/
fmt.Printf("%.2f GB", fileSize/GB) // Imprime 3.73 GB
}
Tambien podrías usar los operadores Shift (>>)
Un buen caso para usar las constantes enumeradas sería:
const (
especialistaError = iota
especialistaGato
especialistaPerro
especialistaHamster
)
func main() {
var tipoEspecialista int
fmt.Println(tipoEspecialista == especialistaError) // True
tipoEspecialista = 1
fmt.Println(tipoEspecialista == especialistaGato) // True
}
podemos descartar un valor generado por iota usando _
un ejemplo:
const (
_ = iota // Descartamos el valor 0
especialistaGato // Valdría 1
especialistaPerro // Valdría 2
especialistaHamster // Valdría 3
)
func main() {
tipoEspecialista := 1
fmt.Println(tipoEspecialista == especialistaGato) // True
}
Podemos descartar multiples valores
const (
_ = iota // Descartamos el valor 0
_ = iota // Descartamos el valor 1
especialistaGato // Valdría 2
)
En el que para verificar que una constante no está inicializada usamos la constante especialistaError
que vale 0
En otros lengajes verías nombres parecido a esto: CONSTANTE_PI sin embargo en go la convención es la misma que con las variables para el mismo caso en go sería constantePi si queremos que solo sea accesible desde ese paquete o ConstantePi para que sea accesible globalmente (esto aplicado a variables a nivel de paquete las de nivel de función serían con minúscula siempre)
- Pascal case -> Export variables (Variables Globales)
- Camel Case -> internal variables (Variables de paquete)
En go las variables que estén a nivel de paquete
- Si empiezan por mayúscula ->
var Numero int 5
serán accesibles por todos - Si empiezan por minúscula ->
var numero int 5
será accesible solo en ese paquete - Capitalización de acrónimos -> si usamos algún acrónimo este debe estar en mayúscula var direccionHTTP string = "http://localhost:25565"
En go las variables de función solo estarán disponibles para los miembros de la misma función y si una variable se llama de la misma forma que una a nivel de paquete, la que está a nivel de función será la que se use (a esto se le llama Shadow que explicaremos en mas detalle en el siguiente apartado)
Esto no solo aplica a los nombres sino tambien al tipo
En go se puede tener distintas variables con el mismo nombre siempre que estas estén en distintos scope el orden de preferencia que tiene Go sobre esto cuando por ejemplo lees el valor de una variable es que en este caso concreto la variable mas cercana coge precendencia
Imagínate que tienes:
- Una variable numero con valor 0 a nivel de paquete
- Otra que se llame igual con valor 2 a nivel de función
si por ejemplo en esa función lees el valor de numero el orden de preferencia hace que leas el valor 2 ya que está mas cercana (de hecho está en el mismo bloque) a esto se le llama "variable shadowning"
Por resumir y que quede claro el shadowning es cuando tenemos la misma variable en distintos scopes entonces la forma de determinar con cual vamos a interactuar es la que mas cercana esté a donde se está usando por ejemplo un fmt.Println(numero)
Con esta técnica tambien se puede hacer "shadow" a un tipo de las variables de distintos scopes de la misma forma que de los valores
if condicion {
fmt.Print("La condicion es afirmativa")
} else if condicion && otraCondicion {
fmt.Print("Las 2 condiciones son afirmativas")
} else {
fmt.Print("La condicion es negativa")
}
Podemos usar un return de varios valores en la condición y usarlo tanto dentro del if como en la condición como por ejemplo la variable ok que dictamina si la key con valor 675 -> osea la variable id
está en el mapa profesiones
Luego dento del if usamos la variable trabajo que lleva el valor de la key con valor 675 en el mapa
profesiones := map[int]string{
675: "Soldado",
432: "Paladín",
321: "Dragontino",
968: "Lancero",
}
id := 675
if trabajo, ok := profesiones[id]; ok {
fmt.Printf("Trabajo con id: %v y value: %v", id, trabajo)
}
En Go se pueden usar todos operadores habituales de la programación & | < > ==
En go las condiciones tambien pueden ser el return de una función
func main() {
if createCondition() == false {
fmt.Print("La condición es falsa")
}
}
func createCondition() bool {
fmt.Print("Retornando condición")
return true
}
Los statements que pueden ir dentro y por lo tanto se ejecutarian en cada case pueden ser de 0 a N de tal manera podríamos tener mas de 1 statement en un case un ejemplo muy simple
Tambien podemos hacer uso de break si por cualquier razón en un caso específico no queremos ejecutar cierta parte del switch poniendo un break
en el ejemplo no va a tener mucho sentido pero esto junto a un if por ejemplo si que podría llegar a tener sentido
switch {
case 1:
fmt.Print("Puedo tener mas de 1 statement entre cases")
fmt.Print("Y pertenecerían al mismo case en este caso del valor numero > 0")
break
fmt.Print("Esto o se ejecuta ya que esta debajo de un break")
numero := 1
switch numero {
case 1:
fmt.Print("Uno")
case 2:
fmt.Print("Dos")
default:
fmt.Print("Ni 1 ni 2")
}
Podemos concatenar condiciones en un mismo case
numero := 1
switch numero {
case 1, 2:
fmt.Print("Uno o Dos")
default:
fmt.Print("Ni 1 ni 2")
}
A tener en cuenta que en los switch no puede haber valores duplicados es decir que 2 cases distintos tengan el mismo valor te daría un syntax error
Al igual que con los if podemos definir una variable dentro del switch directamente y evaluar ese resultado
// Definimos una variable directamente en el switch y calculamos su valor -> 2 + 1 = 3
switch suma := 2 + 1; suma {
case 1, 2:
fmt.Print("Uno o Dos")
default:
fmt.Print("Ni 1 ni 2")
}
Podemos usar condiciones complejas en nuestros cases un ejemplo:
numero := 0
switch {
case numero > 0:
fmt.Print("Mayor que cero")
case numero < 0:
fmt.Print("Menor que cero")
default:
fmt.Print("Es cero")
}
en este tipo de switch los cases pueden hacer overlapping ya que puede que 2 cases sean true para cierta condición en estos casos el que antes de evalúe tiene preferencia sobre el resto
A diferencia de java no hay falltrought de manera por defecto, tenemos que indicarlo a traves de fallthrough
al final de los statement del case
numero := 1
switch {
case numero > 0:
fmt.Println("Uno")
// Esto haría que se ejecutase el siguiente case aunque el mismo sea false
fallthrough
case numero > 5: // Esto es false ya que numero es = 1
fmt.Println("Cinco")
default:
fmt.Println("Ni 1 ni 5")
}
Pero sin embargo la consola nos muestra:
Uno
Cinco
Podemos usar interfaces para por ejemplo saber de que tipo es un valor
var condicion interface{} = 1 // podemos asignar cualquier valor de los aprendidos en este tutorial
switch condicion.(type) { // Conseguiríamos el type de la interfaz
case int, int8, int16, int32, int64:
fmt.Print("Es un entero") // Se ejecutaría este case
case float32, float64:
fmt.Print("Es un decimal")
case string:
fmt.Print("Es una cadena de texto")
}
for i := 1; i < 6; i++ {
// Imprimiría en consola 1 2 3 4 5
fmt.Print(i)
fmt.Print(" ")
}
podemos manejar individualmente los valores de un slice o array por ejemplo:
slice := []int{1, 2, 3}
for index, valor := range slice {
fmt.Print(index, valor)
}
Si queremos omitir el index usaremos el _
slice := []int{1, 2, 3}
for _, value := range slice {
fmt.Print(value)
}
para mapas podremos obtener tanto el value como la key del mapa aunque siempre podremos omitir alguno de los mismos con _
profesiones := map[int]string{
675: "Soldado",
432: "Paladín",
321: "Dragontino",
968: "Lancero",
}
for index, value := range profesiones {
fmt.Println(index, value)
}
tambien podremos usar strings
cadena := "HolaMundo"
fmt.Println("Índice | Valor Unicode | Valor en String")
for index, value := range cadena {
fmt.Println(index, value, string(value))
}
en este caso tendremos la siguiente salida (Lo ordeno en una tabla para que se vea mejor)
Indice | Valor unicode | Valor en String |
---|---|---|
0 | 72 | H |
1 | 111 | o |
2 | 108 | l |
3 | 97 | a |
4 | 77 | M |
5 | 117 | u |
6 | 110 | n |
7 | 100 | d |
8 | 111 | o |
Al igual que java se pueden hacer uso de las label para por ejemplo en el caso de que tengamos 2 fors
for i := 1; i < 6; i++ {
for j := 0; j < 6; j++ {
break // esto rompería la ejecución del for con la variable j
}
}
label2:
for i := 1; i < 6; i++ {
for j := 0; j < 6; j++ {
// en cambio este rompería la ejecución de todo
// ya que la label "label2" está por encima de los 2 for
break label2
}
}
defer es la técnica por la cual el statement que vaya precedido de él se ejecutará el último inmediatamente antes del return de la función (en el caso de que no haya return será el ultimo statement en ejecutarse) un ejemplo:
fmt.Println(1)
defer fmt.Println(2) // statement deferido
fmt.Println(3)
Resultado:
1
3
2
Ahora que pasa si tenemos varios statements deferidos
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
Resultado:
3
2
1
El comportamiento que nos vendría a la cabeza que tendría que tener este código debería de ser 1 2 3 ya que se ejecuta todo al final del método, pero en cambio obtenemos justo lo contrario
Esto es debido a que los statements deferidos funcionan por FILO (First In Last Out) imagina una pila de libros y ten presente la regla de que solo puedes coger 1 a la vez si apilas 3 libros libro1 libro2 y libro3 en ese orden concreto cuando quieras coger esos libros el primero que recogerás es el libro3
Ocurre lo mismo con los statements deferidos el ultimo que entra es el primero en salir
Los usos de esto podría ser por ejemplo el cierre de recursos , podrías tener al final del método el cierre de los mismos pero podrías olvidarte de todos los recursos que tienes que cerrar haciendo defer asociarías el abrir del recurso con el cerrar del mismo inmediatamente despues (aunque realmente lo ejecutarías al final de la función)
Una cosa a tener en cuenta es que por ejemplo si estás en un for loop y el usar el defer para cerrar un recurso que esté en ese for cuando realmente se va a ejecutar ese statement es al final de la función lo del for loop podrías tener miles de recursos abiertos a la vez así (que luego se cerrarían pero no deberías ya que podría dar lugar a fallos o problemas de rendimiento)
Una opción sería delegar ese cierre de recurso a una función y hacer ahí el defer
Según lo que vimos anteriormente podrías pensar que este codigo imprimiría final
variable := "comienzo"
defer fmt.Print(variable)
variable = "final"
Pero la realidad es que imprimiría comienzo
esto es debido a que en las llamadas a funciones deferidas lo que se va a ejecutar coge los valores de los argumentos que tuviera en ese momento esa variable de tal manera que en nuestro ejemplo cogería el valor comienzo
Esto es por ejemplo un erro grave en Go , no hay muchos casos por lo que se pueda dar un error grave del que la aplicación no sepa como seguir por ejemplo el típico caso sería una petición a un servidor remoto en el que no se recibe respuesta alguna , esto no es causa de un panic ya que tendremos un valor de error para interpretar en este caso por ejemplo un 500 o un 404 por decir algunos
Un ejemplo de panic sería que estás intentand leer una plantilla para generar una página web pues bien si no se puede leer esa plantilla no se puede seguir porque sería un error grave del que no puedes recuperarte
por otro lado si vas a abrir un fichero de log y no puedes abrirle ese no es un panic ya que la ejecución puede seguir
En go un error grave es lo que se le llama un panic ya que la aplicación no sabe que hacer despues de eso:
numero1, numero2 := 1, 0
division := numero1 / numero2
fmt.Print(division)
panic: runtime error: integer divide by zero
goroutine 1 [running]:
main.main()
C:/Users/MaQuiNa1995/workspace/Go_Workspace/HolaMundo/src/maquina1995/hola.mundo/Main.go:11 +0x11
exit status 2
Podemos probecar errores manualmente gracias a la funcion panic()
fmt.Println("Comienzo")
panic("Algo malo ha pasado")
// el IDE nos avisará de que este statement no va a ejecutarse
fmt.Println("Fin")
Resultado:
Comienzo
panic: Algo malo ha pasado
goroutine 1 [running]:
main.main()
C:/Users/MaQuiNa1995/workspace/Go_Workspace/HolaMundo/src/maquina1995/hola.mundo/Main.go:14 +0x65
exit status 2
Un tema importante es que el panic será ejecutado justo despues de los statements que hayan sido deferidos asique nuestros posibles cierres de recursos que tengamos deferidos podrán ejecutarse normalmente
Es usado para salvar un error grave (panic) , solo es útil en statements que estén deferidos ya que como hablamos anteriormente estos se ejecutan antes del panic por lo tanto podrán arreglar el error
Cuando se produce un error la función que se estuviera ejecutando en ese momento para su ejecución pero si salvas ese error las funciones padre que hayan llamado a esta podrían seguir la ejecución
un ejemplo de un manejo de excepción:
func main() {
fmt.Print("Empieza el main")
numero := -1
start(numero)
fmt.Printf("Acaba el main")
}
func start(numero int) {
// Requiere que esté deferida para
// que pueda manejar la excepción
// tambien puede ser una función anónima
defer manejarExcepcion()
if numero == 0 {
panic("Error: el valor de número no puede ser cero")
}
fmt.Printf("El número es %v\n", numero)
}
func manejarExcepcion() {
if exception := recover(); exception != nil {
fmt.Println("Manejando la excepción: ", exception)
}
}
numero1 := 1
// aunque se asigne a numero2 apuntan
// a distintas direcciones de memoria
numero2 := numero1
fmt.Println(numero1, numero2) // 1 1
// al no apuntar a la misma dirección de memoria
// aunque asignemos el valor 0 numero2 no se ve afectada
numero1 = 8
fmt.Println(numero1, numero2) // 8 1
var numero1 int = 1
// Hemos creado un puntero para indicar
// que numero2 va a tener un puntero de tipo integer
// a la variable numero 2
var numero2 *int = &numero1
fmt.Println(numero1, numero2) // 1 0xc0000aa058
// Ahora al apuntar a la misma referencia de memoria
// Al cambiar uno el otro tambien se ve reflejado
numero1 = 8
fmt.Println(numero1, numero2) // 8 0xc0000aa058
Ahora el 0xc0000aa058 representaría la dirección de memoria donde estaría almacenado el valor 8 podemos probar esto convirtiendo la variable numero1 a un puntero añadiendo el siguiente statement al código anterior fmt.Println(&numero1) // 0xc0000aa058 0xc0000aa058
Podemos recuperar un valor que esté almacenado en un puntero gracias a *
var numero1 int = 1
var numero2 *int = &numero1
fmt.Println(numero1, numero2) // 1 0xc0000aa058
numero1 = 8
// Al usar el asterisco estamos reemplazando el
// valor de la memoria donde está almacenada por su valor real
fmt.Println(numero1, *numero2) // 8 8
Una aplicación siempre debe tener un punto de entrada:
// siempre en el paquete main
package main
import "fmt"
// Sin parámetros y llamada main
func main() {
fmt.Print("Empieza el main")
}
Se sigue las reglas normales ya descritas en la guía pascalCase o camelCase con la primera minuscula (solo disponible para ese fichero) y con la primera mayúscula para que pueda ser accedido desde toda la aplicación
func main() {
pintarEnConsola("mensaje")
}
// func nombre (nombreVariable tipo, ...)
func pintarEnConsola(mensaje string) {
fmt.Print(mensaje)
}
Si tienes mas de 1 variable del mismo tipo podrías agruparlas
func pintarEnConsola(mensaje, mensaje2 string, numero int) {
fmt.Print(mensaje, mensaje2, numero)
}
func main() {
saludo := "Hola "
nombre := "MaQuiNa"
saludarPorConsola(saludo, nombre)
println("Hemos saludado a", nombre)
}
func saludarPorConsola(saludo, nombre string) {
fmt.Println(saludo, nombre)
nombre = "nombreCambiado"
}
Resultado:
Hola MaQuiNa
Hemos saludado a MaQuiNa
func main() {
saludo := "Hola "
nombre := "MaQuiNa"
saludarPorConsola(&saludo, &nombre)
println("Hemos saludado a", nombre)
}
func saludarPorConsola(saludo, nombre *string) {
fmt.Println(*saludo, *nombre)
*nombre = "nombreCambiado"
}
Resultado:
Hola MaQuiNa
Hemos saludado a nombreCambiado
Podemos usar esta notación para wrapear un conjunto de variables en un slice y trabajar con una serie de posibles valores indeterminado
func main() {
saludo := "Hola"
nombre := "MaQuiNa"
nombre2 := "MaQuina2"
saludarPorConsola(&saludo, nombre, nombre2)
}
func saludarPorConsola(saludo *string, nombres ...string) {
for _, nombre := range nombres {
fmt.Println(*saludo, nombre)
}
}
Puedes incluso no pasar ningun argumento en el parámetro del var arg de modo que en la funcion de antes sería plausible esto: saludarPorConsola(&saludo)
De tal manera que entonces el forEach no se ejecutaría ya que nombres estaría vacío
Podemos retornar mas de 1 valor en go en las funciones esto es util para por ejemplo evitar el uso de panic y controlar el flujo de errores con objetos secundarios un ejemplo:
func main() {
division, error := dividir(5.0, 0.0)
// Si error no es nulo quiere decir que hubo excepciones
if error != nil {
// imprimimos por consola el error
fmt.Print("Hubo un error mas info: ", error)
// paramos la ejecución del método
return
}
// Imprimimos el valor de la división
fmt.Print("El valor de la división es:", division)
}
// En una función podemos retornar mas de un valor
// normalmente el 1º es el resultado de la función
// y los segundos y sucesivos son errores por ejemplo
func dividir(dividendo, divisor float64) (float64, error) {
// En go usamos las ward clausules en la medida
// de lo posible para evitar piramides infernales de ifs
if divisor == 0.0 {
// En vez de controlar el flujo con panic
// usamos la String creada a partir de fmt.Errorf
return 0.0, fmt.Errorf("No se puede dividir entre 0")
}
// retornamos el valor de la división y nil
// que indica que no hubo error
return dividendo / divisor, nil
}
En go podemos crear funciones dentro de funciones
func main() {
// Declaramos la funcion
func() {
fmt.Print("Hola")
// y con los `()` hacemos la llamada a
// la misma inmediatamente
}()
}
func main() {
// podemos usar variables de fuera de la
// función ya que está en el mismo scope
// Aqui el problema es que en cuanto vayamos
// a la programación asincrona vamos a tener problemas
for i := 0; i < 5; i++ {
func() {
fmt.Print(i)
}()
}
// Esto en cambio tiene garantizado el correcto
//funcionamiento en entornos asincronos
// ya que se le pasa el valor vada ciclo
for i := 0; i < 5; i++ {
func(i int) {
fmt.Print(i)
}(i)
}
}
En go podemos tener funciones como variables un ejemplo
func main() {
// Aqui declararíamos la función
funcion := func() {
fmt.Print("saludos desde dentro de una función")
}
// Aqui tenemos otra forma de declarar una función
var funcion2 func() = func() {
fmt.Print("saludos desde otra función nueva")
}
// aqui las ejecutaríamos
funcion()
funcion2()
}
Son básicamente los mismo que una función solo que le damos un contexto donde ejecutarse para asi por ejemplo de 1 struct podemos definir "métodos" como si por ejemplo en java en una clase metiéramos un método un ejemplo
type hechicero struct {
mana int
nombre string
}
func main() {
mago := hechicero{
100,
"MaQuiNa",
}
fmt.Println("Maná actual del mago: ", mago.mana)
// Aunque el struct no tenga este metodo hemos anclado esa función a ese tipo
mago.lanzarHechizo()
fmt.Println("Maná actual del mago: ", mago.mana)
}
// en este caso estamos "añadiendo" una función para poder ser invocada desde hechicero
// de tal manera aunque los struct no puedan tener una función dentro como si de una clase
// y un método en java serían podemos vincular una función a cierto struct
// notese que si queremos que los cambios que hagamos a este struct se hagan
// efectivos para en este caso la función main desde donde hemos pasado el struct
// necesitamos marcar el tipo del parámetro con un *
func (mago *hechicero) lanzarHechizo() {
mago.mana = mago.mana - 10
fmt.Println(mago.nombre, " Lanza Bola de fuego maná restante: ", mago.mana)
}
Resultado:
Maná actual del mago: 100
MaQuiNa Lanza Bola de fuego maná restante: 90
Maná actual del mago: 90
Las interfaces no describen datos como los structs , sino comportamientos un ejemplo del uso de las mismas:
/*
Se define un comportamiento es decir en este caso se va a
escribir bytes en algun lugar ya sea un fichero una conexión TCP
solo nos interesa lo que entra y lo que sale de la función
esto es lo que denominamos "comportamiento"
Lo que devuelva será (nº de bytes escritos y el posible
error que pueda producirse) respectivamente
*/
type Escritor interface {
Write([]byte) (int, error)
}
/*
Ahora solo necesitamos un struct que implemente esa interfaz
si vienes de otros lenguajes tipo java esperarías algo tipo
implements para implementar la interfaz pero en go , eso se hace
creando un método con la misma firma que el método de la interfaz
que acepte este struct
*/
type EscritorConsola struct{}
/*
Aqui definimos la implementación de la interfaz
*/
func (escritorConsola EscritorConsola) Write(bytesEscribir []byte) (int, error) {
bytesEscritos, error := fmt.Print(string(bytesEscribir))
return bytesEscritos, error
}
func main() {
// De tal manera que puedo sustituir EscritorConsola{} por por ejemplo
// EscritorArchivo{} ya que ese tambien implementaría la interfaz Escritor
// porque cumple el contrato(método) de la misma
var escritor Escritor = EscritorConsola{}
escritor.Write([]byte("Hola mundo"))
}
- El nombre de la interfaz debe ser descriptivo de lo que hace
- El nombre de la interfaz debe describir una accion (escribir)
- Si tienes una interfaz con 1 solo método el nombre de la interfaz tendría que ser el nombre del método tambien
- Si tienes mas de un método en la interfaz el nombre debe indicar en conjunto es decir si tienes sumar dividir restar y multiplicar la interfaz se debería de llamar Calcular
Las interfaces de interfaces se usan principalmente para recolectar una serie de utilidades que son a su vez interfaces como podría ser la conexión a una base de datos tendrías por ejemplo un método para abrir la conexión a la misma y otro para cerrarlo