Esta pagina se ve mejor con JavaScript habilitado

Go basics

 ·  🎃 kr0m

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:

pkg install go

Hello world:

vi 00-HelloWorld.go
package main

import "fmt"

func main() {
    fmt.Println("Hello world!")
}
go run 00-HelloWorld.go
Hello world!
go build 00-HelloWorld.go
file 00-HelloWorld
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
./00-HelloWorld
Hello world!

Variables y auto-asignación de tipo de datos:

vi 01-Variables.go
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)
}
go run 01-Variables.go
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:

vi 01b-Variables.go

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
}
go run 01b-Variables.go
i:  1
Error returned

Estructuras de control de flujo:

La estructura mas básica de control es el if.

vi 02-FlowControl.go

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)
    }
}
go run 02-FlowControl.go
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.

vi 02b-FlowControl.go

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")
}
go run 02b-FlowControl.go
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:

vi 02c-FlowControl.go
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
                }
            }
        }
}
go run 02c-FlowControl.go
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.

vi 03-Functions.go
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)
}
go run 03-Functions.go
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:

vi 04-Arrays_Slices.go

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)
}
go run 04-Arrays_Slices.go
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:

vi 04b-Arrays_Slices.go

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)
}
go run 04b-Arrays_Slices.go
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:

vi 04c-Arrays_Slices.go

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)
}
go run 04c-Arrays_Slices.go
[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:

vi 04d-Arrays_Slices.go

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)
}
go run 04d-Arrays_Slices.go
[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:

vi 04f-Arrays_Slices.go

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)
}
go run 04f-Arrays_Slices.go
[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:

vi 05-Maps.go

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)
}
go run 05-Maps.go
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.

vi 05b-Maps.go

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"])

}
go run 05b-Maps.go
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:

vi 05c-Maps.go

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")
}
go run 05c-Maps.go
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:

vi 05d-Maps.go

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)
}
go run 05d-Maps.go
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.

vi 05e-Maps.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)
}
go run 05e-Maps.go
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.

vi 05f-Maps.go

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)
    }
}
go run 05f-Maps.go
Currency Code: USD, Name: US Dollar, Symbol: $
Currency Code: GBP, Name: Pound Sterling, Symbol: £
Currency Code: EUR, Name: Euro, Symbol: €

Recursos adicionales:

Si te ha gustado el artículo puedes invitarme a un RedBull aquí