Go es el lenguaje de programación de Google lanzado en 2009, disponible para diferentes tipos de sistemas Unix-like, incluidos FreeBSD, Linux, Mac OS X y Plan 9 además de para distintas arquitecturas como i386, amd64 y ARM.
Las principales ventajas respecto a otros lenguajes de programacón son:
- Mayor rendimiento que Python, Ruby o JavaScript.
- Mejor soporte para trabajos concurrentes.
- Go runtime incluido en los binarios lo que lo hace altamente distribuible entre sistemas.
- LibrerÃas descentralizadas, cada programador hospeda su código donde desee, para instalar el software se accede a dicha plataforma directamente.
-
Instalación
-
Hello world
-
Variables y auto-asignación de tipo de datos
-
Variable scope
-
Estructuras de control de flujo
-
Funciones
-
Arrays y slices
-
Maps
-
Recursos adicionales
Instalación:
Hello world:
package main
import "fmt"
func main() {
fmt.Println("Hello world!")
}
Hello world!
00-HelloWorld: ELF 64-bit LSB executable, x86-64, version 1 (FreeBSD), statically linked, for FreeBSD 12.3, FreeBSD-style, Go BuildID=Y4MQLyWJvSerAP4BVk2v/9P2KHgW0r9hY_WmEb9uy/zyzxSbQ1ETRoacqPL2Kq/-SzOfM95EQBcBz8mFnZr, with debug_info, not stripped
Hello world!
Variables y auto-asignación de tipo de datos:
package main
import "fmt"
func main() {
// Variable declaration
var x int
x = 5
// Sugar syntax: Short declaration method, auto variable type detection
y := 10
// Only first value assigment requires ":"
y = 15
// Print variable values
fmt.Println("x:", x)
fmt.Println("y:", y)
}
x: 5
y: 15
Variable scope:
El scope de las variables en Go, es muy reducido, es decir, si una variable se crea dentro de un if
esta solo será accesible dentro de ese if
, si se crea dentro de un bucle for
solo estará disponible dentro del bucle y del mismo modo en cualquier otro tipo de estructura de control de flujo.
Pongamos unos ejemplos:
package main
import "fmt"
func testFunction() error {
return fmt.Errorf("Error")
}
func main() {
i := 1
if i > 0 {
j := 2
if j > 0 {
j ++
}
}
// i is in scope
fmt.Println("i: ", i)
// j is out of the scope
// fmt.Println("j: ", j) would render an error
if err := testFunction(); err != nil {
fmt.Println("Error returned")
}
// err is out of the scope
// fmt.Println("err: ", err) would render an error
}
i: 1
Error returned
Estructuras de control de flujo:
La estructura mas básica de control es el if
.
package main
import "fmt"
func main() {
// Conditionals
age := 25
if age >= 18 {
fmt.Println("You are on legal age")
} else {
fmt.Println("You are NOT on legal age")
}
// For loop
for i := 0; i < 5; i++ {
fmt.Println(i)
}
}
You are on legal age
0
1
2
3
4
Una forma de agrupar los if
y obtener un código mas legible es utilizar un switch
.
package main
import "fmt"
func checkOption(option string){
switch option {
case "A":
fmt.Println("A")
case "B":
fmt.Println("B")
default:
fmt.Println("OTHER")
}
}
func main() {
checkOption("A")
checkOption("B")
checkOption("X")
}
A
B
OTHER
Si tenemos un bucle anidado podemos hacer un break
desde el segundo bucle rompiendo la iteración del primero, para ello debemos etiquetar el bucle que deseamos romper:
package main
import "fmt"
func main() {
FirstLoop:
for i := 0; i < 5; i++ {
fmt.Println("i: ", i)
for j := 0; j < 5; j++ {
fmt.Println("j: ", j)
if j == 0 {
break FirstLoop
}
}
}
}
i: 0
j: 0
Funciones:
En este ejemplo vamos a ver una función que suma dos enteros siempre y cuando sean menores o igual a 100. Además haremos uso de dicha función dos veces, una comprobando el valor de retorno de error y una segunda vez sin comprobarlo asignándo dicho valor retornado al operador blank identifier
que es utilizado para asignar valores de retorno que queremos ignorar.
package main
import "fmt"
// Function that adds two numbers
func add(a, b int) (int, error) {
if (a > 100 || b > 100) {
return 0, fmt.Errorf("Error, operators too big")
}
return a + b, nil
}
func main() {
result, err := add(3, 4)
if err != nil{
fmt.Println("Error: ", err)
return
}
fmt.Println("Result:", result)
// Ignoring returned error
result, _ = add(300, 400)
fmt.Println("Result:", result)
}
Result: 7
Result: 0
Arrays y slices:
La mayor diferencia entre un array y un slice es que el array es de un tamaño fijo mientras que el slice es dinámico.
En realidad un slice consiste en un puntero a un array subyacente con una longitud y una capacidad(máxima longitud) deterrminada.
Debido a esto cuando trabajamos con slices siempre estamos trabajando con punteros, en el siguiente ejemplo esto queda patente:
package main
import "fmt"
func main() {
// Sugar syntaz array
array := [5]string{"a", "b", "c", "d", "e"}
fmt.Println("Array:", array)
// Get array elements from 0 to N-1
slice1 := array[0:3]
fmt.Println("Slice1:", slice1)
// Get array elements from 2 to N-1
slice2 := array[2:5]
fmt.Println("Slice2:", slice2)
fmt.Println("---------")
// Modify underlying array value:
array[2] = "X"
fmt.Println("Array:", array)
fmt.Println("Slice1:", slice1)
fmt.Println("Slice2:", slice2)
}
Array: [a b c d e]
Slice1: [a b c]
Slice2: [c d e]
---------
Array: [a b X d e]
Slice1: [a b X]
Slice2: [X d e]
Hemos dicho que los slices son dinámicos, entonces que significa la “capacidad” de un slice?
Los slices conforme van creciendo en logitud, van migrando los datos de un array de un tamaño X a otro array de tamaño 2X, la longitud es el número de elementos almacenados en el slice y la capacidad el tamaño máximo del array subyacente.
Con un ejemplo lo veremos todo mas claro:
package main
import "fmt"
func main() {
// Create slice
slice := []string{"a"}
fmt.Printf("Slice length: %v\t Slice capacity: %v\t Slice content: %v\n", len(slice), cap(slice), slice)
slice = append(slice, "b")
fmt.Printf("Slice length: %v\t Slice capacity: %v\t Slice content: %v\n", len(slice), cap(slice), slice)
slice = append(slice, "c")
fmt.Printf("Slice length: %v\t Slice capacity: %v\t Slice content: %v\n", len(slice), cap(slice), slice)
slice = append(slice, "d")
fmt.Printf("Slice length: %v\t Slice capacity: %v\t Slice content: %v\n", len(slice), cap(slice), slice)
slice = append(slice, "e")
fmt.Printf("Slice length: %v\t Slice capacity: %v\t Slice content: %v\n", len(slice), cap(slice), slice)
}
Slice length: 1 Slice capacity: 1 Slice content: [a]
Slice length: 2 Slice capacity: 2 Slice content: [a b]
Slice length: 3 Slice capacity: 4 Slice content: [a b c]
Slice length: 4 Slice capacity: 4 Slice content: [a b c d]
Slice length: 5 Slice capacity: 8 Slice content: [a b c d e]
Como podemos ver el slice ha partido de un array con capacidad 1, al añadir un segundo elemento ha migrado el contenido existente a otro array de capacidad 2X, en este caso 2*1:2.
En el siguiente append el array con capacidad 2 vuelve a quedarse corta por lo tanto repite el proceso de migración a un array de capacidad 2X: 2*2:4
El siguiente append no es necesario migrar los datos ya que vamos a añadir un cuarto elemento a un array de una capacidad de 4.
En el siguiente append el array con capacidad 4 vuelve a quedarse corta por lo tanto repite el proceso de migración a un array de capacidad 2X: 4*2:8
Este es el proceso que sigue cada vez que añadimos elementos a un array, pero esto mismo que parece funcionamiento interno sobre la gestión de slices de Go puede llegar a afectarnos a nosotros como programadores ya que en cada migración se está cambiando de array.
Veamos este ejemplo:
package main
import "fmt"
func change(s []string) {
s = append(s, "c")
fmt.Println(s)
}
func main() {
testSlice := []string{"a", "b"}
change(testSlice)
fmt.Println(testSlice)
}
[a b c]
[a b]
No olvidemos que con slices siempre se estamos trabajando con punteros, entonces como es posible que el slice tenga distintos valores según la parte del código desde donde se consulte?
Lo que ha ocurrido aquà es que se ha generado un array de len:2 y cap:2 en la función main
, se ha llamado a la función change
donde se ha añadido un elemento al slice, pero como su capacidad es de 2 y el tercer elemento no cabe se ha tenido que migrar de array, al haber realizado esta migración el array al que apunta el slice de la función main
no es el mismo al que termina apuntando la función change
.
La manera mas sencilla de resolver esto serÃa retornando un valor en la función y sobreescribiendo el valor de testSlice en la función principal:
package main
import "fmt"
func change(s []string) []string {
s = append(s, "c")
fmt.Println(s)
return s
}
func main() {
testSlice := []string{"a", "b"}
testSlice = change(testSlice)
fmt.Println(testSlice)
}
[a b c]
[a b c]
Otro problema relacionado con el origen basado en punteros de los slices es que si realizamos append sobre un slice distinto al original, podemos encontrarnos con resultados un tanto ilógicos a simple vista.
Veámoslo en un ejemplo:
package main
import "fmt"
func main() {
s1 := []int{1}
s2 := append(s1, 2)
s3 := append(s2, 3)
s4 := append(s3, 4)
fmt.Println(s1, s2, s3, s4)
s4[0] = 55
fmt.Println(s1, s2, s3, s4)
}
[1] [1 2] [1 2 3] [1 2 3 4]
[1] [1 2] [55 2 3] [55 2 3 4]
Pero que ha ocurrido? Simplemente el array subyacente que comparten s3 y s4 es el mismo ya que cap(s3) era 4, por lo tanto en el siguiente append no ha sido necesario migrar de array. Al alterar un elemento de uno de los slices que comparte array subyacente con otro slice, ha afectado a ambos slices.
La mejor manera de obtener resultados incorrectos es siempre sobreescribir el mismo slice cuando hagamos uso de append.
Maps:
Los maps es lo que en otros lenguajes de programación se llama array asociativo, se trata de un array con un Ãndice. Esto resulta muy útil para acceder a un elemento de un array sin tener que recorrer sus posiciones hasta encontrar el elemento deseado.
Veamos un simple ejemplo:
package main
import "fmt"
func main() {
// Sugar syntax: Map declaration
grades := map[string]int{
"John": 8,
"Sarah": 6,
"Bob": 9,
}
fmt.Println("John grade:", grades["John"])
fmt.Println("Sarah grade:", grades["Sarah"])
fmt.Println("Bob grade:", grades["Bob"])
// Add map field
grades["Mary"] = 7
fmt.Println("Mary grade:", grades["Mary"])
// Print the complete map
fmt.Println("Grades:", grades)
}
John grade: 8
Sarah grade: 6
Bob grade: 9
Mary grade: 7
Grades: map[Bob:9 John:8 Mary:7 Sarah:6]
Cuando trabajemos con maps debemos tener en cuenta que al acceder a un elemento inexistente, este retornará el valor nil
del tipo, es decir en nuestro ejemplo que es un map[string]int, devolverÃa 0.
package main
import "fmt"
func main() {
// Sugar syntax: Map declaration
grades := map[string]int{
"John": 8,
"Sarah": 6,
"Bob": 9,
}
fmt.Println("John grade:", grades["John"])
fmt.Println("Sarah grade:", grades["Sarah"])
fmt.Println("Bob grade:", grades["Bob"])
// Add map field
grades["Mary"] = 7
fmt.Println("Mary grade:", grades["Mary"])
// Print the complete map
fmt.Println("Grades:", grades)
// Print the nonexistent map
fmt.Println("Nonexistent grade:", grades["XX"])
}
John grade: 8
Sarah grade: 6
Bob grade: 9
Mary grade: 7
Grades: map[Bob:9 John:8 Mary:7 Sarah:6]
Nonexistent grade: 0
Para distinguir si el map es un grade con valor 0 o un Ãndice inexistente debemos obtener los valores con un segundo parámetro del siguiente modo:
if value, ok := grades["XX"]; ok {
fmt.Println("Exists, value: ", value)
} else {
fmt.Println("Key doesnt exists")
}
Siguiendo con el ejemplo anterior lo mejor será crear una función que haga dicha comprobación, quedando el código del siguiente modo:
package main
import "fmt"
func getGrade(grades map[string]int, name string) {
if value, ok := grades[name]; ok {
fmt.Printf("%v grade: %v\n", name, value)
} else {
fmt.Printf("Incorrect student: %v\n", name)
}
}
func main() {
// Sugar syntax: Map declaration
grades := map[string]int{
"John": 8,
"Sarah": 6,
"Bob": 9,
}
getGrade(grades, "John")
getGrade(grades, "Sarah")
getGrade(grades, "Bob")
getGrade(grades, "XX")
}
John grade: 8
Sarah grade: 6
Bob grade: 9
Incorrect student: XX
Los maps al igual que los slices no son mas que punteros, si hacemos una “copia” en realidad estaremos compartiendo los datos subyacentes.
Veamos este ejemplo:
package main
import "fmt"
func main() {
// Sugar syntax: Map declaration
grades := map[string]int{
"John": 8,
"Sarah": 6,
"Bob": 9,
}
fmt.Printf("grades: %v\n", grades)
grades2 := grades
grades2["Laura"] = 3
fmt.Println("----------------")
fmt.Printf("grades: %v\n", grades)
fmt.Printf("grades2: %v\n", grades2)
delete(grades, "John")
fmt.Println("----------------")
fmt.Printf("grades: %v\n", grades)
fmt.Printf("grades2: %v\n", grades2)
}
grades: map[Bob:9 John:8 Sarah:6]
----------------
grades: map[Bob:9 John:8 Laura:3 Sarah:6]
grades2: map[Bob:9 John:8 Laura:3 Sarah:6]
----------------
grades: map[Bob:9 Laura:3 Sarah:6]
grades2: map[Bob:9 Laura:3 Sarah:6]
Si queremos que sean independientes lo mejor es hacer una copia elemente a elementeo en otro map teniendo en cuenta que si generamos un map vacÃo, habrá que utilizar la función make() de Go.
package main
import "fmt"
func main() {
// Sugar syntax: Map declaration
grades := map[string]int{
"John": 8,
"Sarah": 6,
"Bob": 9,
}
grades2 := make(map[string]int)
for name, mark := range grades {
grades2[name] = mark
}
fmt.Printf("grades: %v\n", grades)
fmt.Printf("grades2: %v\n", grades2)
fmt.Println("----------------")
grades2["Laura"] = 3
fmt.Printf("grades: %v\n", grades)
fmt.Printf("grades2: %v\n", grades2)
fmt.Println("----------------")
delete(grades, "John")
fmt.Printf("grades: %v\n", grades)
fmt.Printf("grades2: %v\n", grades2)
}
grades: map[Bob:9 John:8 Sarah:6]
grades2: map[Bob:9 John:8 Sarah:6]
----------------
grades: map[Bob:9 John:8 Sarah:6]
grades2: map[Bob:9 John:8 Laura:3 Sarah:6]
----------------
grades: map[Bob:9 Sarah:6]
grades2: map[Bob:9 John:8 Laura:3 Sarah:6]
Los maps no tienen porque limitarse a tipos primitivos, también podemos utilizar estructuras complejas como en el siguiente ejemplo.
package main
import (
"fmt"
)
type currency struct {
name string
symbol string
}
func main() {
curUSD := currency{
name: "US Dollar",
symbol: "$",
}
curGBP := currency{
name: "Pound Sterling",
symbol: "£",
}
curEUR := currency{
name: "Euro",
symbol: "€",
}
currencyCode := map[string]currency{
"USD": curUSD,
"GBP": curGBP,
"EUR": curEUR,
}
for cyCode, cyInfo := range currencyCode {
fmt.Printf("Currency Code: %s, Name: %s, Symbol: %s\n", cyCode, cyInfo.name, cyInfo.symbol)
}
}
Currency Code: USD, Name: US Dollar, Symbol: $
Currency Code: GBP, Name: Pound Sterling, Symbol: £
Currency Code: EUR, Name: Euro, Symbol: €