Go (GoLang)

Go es un lenguaje de programación desarrollado por Google pensado para la simplicidad y la eficiencia siendo confiable, con una velocidad de respuesta semejante a lenguajes como C o C++, logrando acogida para la gestión de concurrencia en la Web. Además de estar disponible en varios sistemas (para Windows, macOS, Linux), posibilita la programación para la web con WebAssembly. También puede ser interesante usar Go como complemento con otros lenguajes (interoperabilidad), logrando así un tiempo de respuesta favorable.

Aunque es un lenguaje compilado, se pueden crear scripts para el sistema usando Go, no sólo para acceder al sistema de archivos, sino que también se puede conectar a bases de datos, y por qué no, con almacenamiento en la nube. Por esto es frecuente encontrarlo tanto en software de Backend como en DevOps.

Esta es una referencia ágil para quién tenga nociones de programación o codifique con algún otro lenguaje, también a modo de repaso y uso frecuente. Siendo así, tener esta información como memoria te resultará simple de acceder a los fundamentos, incluso como prueba de concepto sobre el lenguaje.

Tips esenciales del lenguaje

Para quien tenga habilidades, experticia en programación y/o requiera agilidad en conceptos técnicos, pueden resumirse los siguientes tips esenciales del lenguaje:

  1. Tipos de datos básicos: int, uint, float32, byte, bool, string, struct. Las variables se definen con var, el nombre y el tipo de datos. Se puede asignar el valor directamente sin necesidad de especificar el tipo de datos usando el operador :=.
  2. Las funciones se definen con func, luego los parámetros van entre paréntesis (...) continuando con el tipo de datos del parámetro (y separando los parámetros con coma ,). El tipo de datos a retornar va después del cierre del paréntesis. Además, se usa return para retornar un valor. La función principal de un programa se denomina main.
  3. Para el bloque de la función o el flujo de control se usan llaves {}. Las sentencias terminan con la línea sin el uso de punto y coma.
  4. El flujo de control es semejante a lenguajes como C, Java, Javascript, Kotlin, es decir que se cuenta con una anatomía cercana para el uso de if, for, while pero sin el uso de paréntesis y no existe try ... exception (pero tiene otro modo de controlar errores).
  5. El lenguaje presenta una manera de homologar clases siendo en principio funcional (mediante el uso de struct y funciones asociadas).

Para información detallada puede consultarse: https://github.com/jltabara/Libro-Golang/blob/master/LibroV1.md

Ejemplo esencial

package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hi there!")
}

En un programa inicial, siempre encuentras package, import y el bloque para func main(). Revisa la instalación del lenguaje y el proyecto de inicio rápido avanzando el documento

Una breve comparación con Javascript

Si tienes conocimiento en Javascript u otro lenguaje, puede ser útil un sencillo escenario de comparación que nos da una referencia inmediata de Go.

Concepto Javascript Go
Variable let variable = ‘Ana’ variable := “Ana”
Función function () { } func main() { …
Condición if (i == 1) { } if i == 1 { …
Ciclo For for (let i = 0; i < 10; i++) { } for i := 0; i < 10; i++ { …
Excepción try { } catch(err) { } if err != nil { …

En Go no existen excepciones propiamente pero se pueden gestionar los errores usando una variable y una condición como se verá avanzando el texto.

Tipos de datos básicos

Declaración de variables

Se puede declarar variables con la palabra reservada var y el tipo de datos porterior al nombre, o simplemente usando la declaracion corta que asigna un valor con el operador :=. Si se declara a nivel global, suele usarse var ( ... ), aunque en ese caso debe revisarse si se trata de constantes usando const ( ... ).

Ejemplo:

name := "Ana"
var n int
var a [10]int
v := [3]int{1,2,3}

En el caso de arreglos (últimas dos líneas del ejemplo), los elementos deben corresponder a un solo tipo de datos y si se usa el operador := se asignan los valores iniciales usando llaves {}.

Definición de funciones

func sum(a int, b int) int {
    return a + b
}

Basicamente se usa la palabra servada func. Si devuelve un valor se indica el tipo de dato y se usa internamente la palabra reservada return. Veamos un ejemplo.

Condicionamiento if / else (if)

if i == 1 {
    fmt.Println("one")
} else if i == 2 {
    fmt.Println!("two")
} else {
    fmt.Println!("aha")
}

Nótese que se usa, if ... {, } else if ... { o } else {, es decir, en tanto no existe caracter indicador de fin de línea (; en otros lenguajes), el lenguaje se expresa de modo estricto con estas expresiones sin dar lugar a variación en la organización del código. Así mismo, una condición simple siempre será expresada entre llaves {}, puesto que se maneja el concepto de bloques de código y el alcance de variables entre dichas llaves, del mismo modo que en las funciones.

El Ciclo For

for i := 0; i < 5; i++ {
    fmt.Println(i)
}

En Go sólo existe el ciclo for. Nótese que se usa ; para separar las partes de inicialización, condición para terminar y el incremento. Además, se usa := para establecer la variable con un valor inicial.

También podemos expresarlo de modo simple, es decir, aplicando sólo la condición (semejante a while en otros lenguajes), pero se debe controlar la inicialización y el incremento de la iteración. Veamos:

i := 0
for i < 5 {
    i++
    fmt.Println(i)
}
fmt.Println("Programa terminado")

Dado que en Go sólo existe el ciclo for, para recorrer un arreglo puede usarse la palabra reservada range que devuelve dos valores y el ciclo termina automaticametne al finalizar el rango. Por ejemplo:

a := []string{"a","b","c"}
for index, value := range a {
    fmt.Println(index, value)
}

Lo mismo ocurre cuando se trata de mapas o tuplas (que se inicializan con la palabra reservada make). Por ejemplo:

m := make(map[string]string)
m["a"] = "apple"
m["b"] = "banana"
for index, value := range m {
    fmt.Println(index, value)
}

Manejo de Errores

En Go no aplica el concepto de excepciones de otros lenguajes. Básicamente, si una función puede producir un error se devuelven dos valores, uno para el resultado natural que retorna la función y otro para el error, y se debe validar el resultado de la variable de error usada (if err != nil {...). Veamos un ejemplo:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    s := "5"
    n, err := strconv.Atoi(s)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    n = n + 10
    fmt.Println(n)
}

En este caso se usa el paquete strconv para intentar convertir una cadena a número y luego incremtarlo en 10 unidades, si la variable err es diferente de nil (nulo) entonces se identifica un error en la conversion.

Punteros

Como en lenguaje C y C++, el concepto de punteros se puede entender como variables que se pasan como referencia, es decir, en tanto se usan como parámetro en otra función se apunta a la misma posición de memoria y por tanto se altera el resultado de la variable original. En últimas, lo que se espera es reflejar el cambio de un valor en otra parte del programa, sólo que en Go se simplifican aspectos. Veamos un ejemplo:

package main

import (
    "fmt"
)

func main() {
    i := 5
    increment(&i)
    fmt.Println(i)
}

func increment(x *int) {
    *x++
}

Se usa el signo & en la invocacion para denotar la referencia y el signo * en la funcion (tanto en la declaración del tipo recibido como en el uso de la variable interna).

Estructuras de datos (struct)

Con la palabra reservada struct (combinada con type) se definen estructuras de datos u objetos. Veamos un ejemplo.

package main

import (
    "fmt"
)

type Person struct {
    name string
    lastname string
    age int
}

func main() {
    person := Person{name: "John", lastname: "Doe", age: 22}
    fmt.Println(person)
}

También se puede iniciar vacío usando new(Person). Además, se puede recorrer un arreglo de datos usando la variable y la función range. Por ejemplo:

func main() {
    persons := []Person{name: "John", lastname: "Doe", age: 22}
    for _,person := range persons {
        fmt.Println(person)
    }
}

Métodos en Estructuras (Homologando Clases)

En Go no hay clases pero se puede homologar métodos que corresponderían a funciones que utilizan un identificador para un struct. Veamos esto con la estructura anterior y un ejemplo:

func (this Person) fullname() string {
    return this.name + " " + this.lastname
}

func main() {
    p := Person{name: "John", lastname: "Doe", age: 22}
    fmt.Println(p.fullname())
}

Nótese que antes del nombre de la función se establece un identficador que refiere el struct asociado a la función: (this Person). Este modo de implementación se aplica en cada método asociado a un struct. En el ejemplo se ha usado this como identificador pero puede tener otro nombre distinto, dado que no se trata de una palabra reservada del lenguaje. También se puede recibir el struct como puntero (agregando el * como se ha visto antes).

Además, se pueden implementar interfaces usando en lugar de struct la palabra reservada interface que se trata de otro type. Dentro de estas se reportan las funciones.

Interfaces

Veamos un ejemplo:

package main
import (
    "fmt"
    "strconv"
)

type UserInput interface {
	Add(rune)
	GetValue() string
}

type NumericInput struct {
	input string
}

func (this NumericInput) Add(rune string) {
    n, err := strconv.ParseFloat(rune, 64)
    if err != nill {
        this.input += rune
    }
}

func (this NumericInput) GetValue() string {
    return this.input    
}

func main() {
	var input UserInput = &NumericInput{}
	input.Add('1')
	input.Add('a')
	input.Add('0')
	fmt.Println(input.GetValue())
}

Concurrencia a modo de ejemplo (Goroutines)

package main

import (
    "fmt" 
    "time"
)

func ahaGo(index int) {
    fmt.Println(index)
}

func tryGo(n int){	
    for i := 0; i < n; i++ {
        go ahaGo(i)
    }
}

func main(){
    go tryGo(20)
    time.Sleep(2000 * time.Millisecond)
}

Con la palabra reservada go se define una Goroutine liberando la lógica de una secuencia estricta pero atendiendo ágilmente la función correspondiente (en este caso, imprimir un número). Este programa puede arrojar un resultado como:

0
1
6
2
3
4
5
9
7
8
11
10
12
13
14
15
19
16
17
18

Veamos otro ejemplo:

package main
import (
    "fmt"
    "math"
)

func weeks(days int) int {
    var result float64
    n, _ := math.Modf(days / 7)
    return int(n)
}

func main() {
    m := map[string]int{ "Robert" : 30, "John" : 475, "Elly" : 1022, "Bob" : 99 }
    go func() {
        for p, days := range m {
            w := weeks(days)
            fmt.Println(p, " ha trabajado en la empresa ", w, " semanas")
        }
    }()
}

Matriz de palabras reservadas

Dada la simplicidad de Go podemos citar tan solo 25 palabras reservadas en sus primeras versiones.

. 1 2 3 4 5
1 break default func interface select
2 case defer go map struct
3 chan else goto package switch
4 const fallthrough if range type
5 continue for import return var

Instalación de Golang

Para instalar Go te diriges al sitio oficial y descargas el instalador (en Linux puede usarse el comando apt-get), luego se ejecuta el archivo descargado.

El instalador sugiere que se instale en la carpeta C:\Go que correspondería a la variable de entorno GOROOT (raíz del lenguaje que contiene la subcarpeta bin). Para la variable GOPATH bajo Windows, automaticamente asigna el valor %USERPROFILE%/go (dónde %USERPROFILE% es la carpeta de inicio del usuario que lo instala), y apunta a las carpetas src, pkg y bin. Si se cambia la ruta de instalacion de Go, es preciso reportar la variable de entorno GOROOT con la nueva ruta correspondiente.

Considere verificar que las variables de entorno GOROOT y GOPATH se encuentren debidamente configuradas y agregarlas en PATH, por ejemplo: PATH=%GOROOT%\bin;%GOPATH%

También puede usarse scoop en Windows (si esta previamente instalado) usando PowerShell con la sentencia:

scoop install go

Para Chocolatey se usa: choco install golang

Si el sistema que utilizas para desarrollar es Linux Ubuntu/Debian, puedes ejecutar la siguiente instrucción:

sudo apt install -y golang

Para Linux Enterprise como CentOS use: sudo yum install -y golang
Para macOS se usa Homebrew ejecutando: brew install go

Versión reciente de Go usando el paquete para Linux Ubuntu

Una opción para instalar la versión mas reciente Go es usar el paquete para Linux Ubuntu. Basta con descargar desdel el sitio oficial el archivo con extensión .tar.gz. Veamos un ejemplo usando los comandos necesarios para instalar la versión mas reciente en Linux.

sudo rm -rf /usr/lib/go-*
wget https://go.dev/dl/go1.22.1.linux-amd64.tar.gz
tar -xvzf go1.22.1.linux-amd64.tar.gz
sudo mv go /usr/lib/go-1.22.1
cd /usr/lib
sudo rm -rf go
sudo ln -s go-1.22.1 go
cd ../bin
sudo rm -rf go gofmt
sudo ln -s ../lib/go-1.22.1/bin/go go
sudo ln -s ../lib/go-1.22.1/bin/gofmt gofmt
go version
cd ../share
sudo rm -rf go
sudo mkdir go

Puedes ejecutar estos comandos desde una terminal uno por uno. Incluso, se pude haber instalado antes la version por defecto de Go para Ubuntu y luego usar estos comandos para dejar la versión reciente funcionando.

Módulos de Librerías

Para organizar nuestros paquetes modularmente podemos usar una sentencia como la siguiente:

go mod init mymodule

mymodule se refiere al nombre asginado a nuestro módulo

Para instalar módulos de librerías se usa go get ... (indicando la librería o url), por ejemplo, para usar el controlador de MongoDB ejecutas:

go get go.mongodb.org/mongo-driver/mongo

Proyecto de inicio rápido

Creamos una carpeta nueva en una ubicación destinada para el desarrollo de nuestro proyecto e incluimos un archivo con extension .go (por ejemplo, server.go). Esto puede hacerse desde un editor como Visual Studio Code (VSCode). Podemos incluir el siguiente código.

package main

import (
	"fmt"
	"log"
	"net/http"
)

const (
	HOST = "localhost"
	PORT = "8080"
)

func start(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "It works!")
}

func main() {
	fmt.Println("Listen on", HOST, "[", PORT, "]")
	http.HandleFunc("/", start)
	err := http.ListenAndServe(HOST+":"+PORT, nil)
	if err != nil {
		log.Fatal("Error listening server: ", err)
		return
	}
}

Este programa deja a disposición un servidor lógico para la Web que muestra un mensaje en el navegador. Para compilar el programa y ejecutarlo se procede de la siguiente manera:

go build server.go
server.exe

Dado que se trata de un servidor web, usamos un navegador para consultar la ruta respectiva (localhost:8080) y probar el programa.

Programando un Servidor Web con Fiber

Veamos ahora como lograr un servidor web en simples pasos con la librería Fiber. Primero descargamos la librería.

go get github.com/gofiber/fiber

Abrimos el editor y nombramos nuestro programa, por ejemplo server.go, y colocamos el siguiente contenido:

package main

import "fmt"
import "github.com/gofiber/fiber"

func main() {
  app := fiber.New()

  app.Get("/", func(c *fiber.Ctx) error {
    return c.SendString("Hi there!")
  })

  app.Listen(":8000")
}

Ejecutamos el programa con el comando:

go run server.go

Puedes revisar en un navegador consultando la dirección: localhost:8000

Experimentando con Go y Javascript (goja)

Podemos embeber algunos lenguajes de “script” en Go. Tal es el caso del proyecto goja que permite ejecutar Javascript. Veamos un ejemplo:

package gojs

import (
	"fmt"

	"github.com/dop251/goja"
	"github.com/dop251/goja_nodejs/console"
	"github.com/dop251/goja_nodejs/require"
)

func plainJS() {
	vm := goja.New()

	new(require.Registry).Enable(vm)
	console.Enable(vm)

	script := `
		console.log("Hello world - from Javascript inside Go! ")
	`
	fmt.Println("Compiling ... ")
	runnable, err := goja.Compile("", script, true)
	if err != nil {
		fmt.Printf("Error compiling the script %v ", err)
		return
	}
	fmt.Println("Running ... \n ")
	_, err = vm.RunProgram(runnable)

}

func main() {
    plainJS()
}

Go & AWS Lambda

Veamos a continuación un ejemplo de código con la estructura orientada para AWS Lambda. Pero antes de ello definimos nuestro proyecto ubicándonos en una carpeta usado como espacio de trabajo en dónde estableceremos la carpeta para nuestro proyecto (con un archivo main.go) y lo iniciaremos con las siguientes sentencias:

mkdir golambda
cd golambda
go mod init golambda
go get -u github.com/aws/aws-lambda-go
vim main.go

golambda corresponde al nombre de nuestro proyecto. Usamos go mod init para preparar el proyecto y go get para incorporar módulos (AWS).
La última sentencia representa la edición de un nuevo archivo main.go, pero puede usarse un editor como VSCode en lugar de vim.

El código de ejemplo tendrá un contenido como el siguiente:

package main

import "log"
import "github.com/aws/aws-lambda-go/events"
import "github.com/aws/aws-lambda-go/lambda"

func main() {
  lambda.Start(handler)
}

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
  response := events.APIGatewayProxyResponse{
    StatusCode: 200,
  }
  log.Println("Hi there!")
  return response, nil
}

Antes de compilar, debemos garantizar un binario para Linux. Si tienes macOS se ejecuta GOOS=linux, para Windows se usa en PowerShell $Env:GOOS="linux".
Para probar la Lambda se compila con go build main.go y se comprime el archivo binario obtenido, para luego subirlo manualmente a AWS Lambda (indicando que el Handler se denomina main)