Golang de 0 a API.

Yoandy Rodríguez Martínez

1 Primeros pasos

1.1 Instalación

  1. Descargar la versión para tu OS desde https://golang.org/dl (algunas distribuciones de Linux incluyen el paquete en sus repositorios).
  2. Abrir el instalador y seguir las instrucciones.

       (Unix| Linux): /usr/local/go (varía según el sistema)
       Windows: C:\Go
    
  3. Para chequear que la instalación está correcta escribir en la consola

       $ go version
    

1.2 Configuración inicial

  1. Activar variables de entorno para el trabajo con módulos

       $ go env -w GO111MODULE=on GOPROXY=https://goproxy.io,direct
    
  2. Para chequear que todas las variables de entorno están correctas

escribir en la consola

   $ go env
   GOROOT = [Directorio de instalación]
   GOPROXY=https://goproxy.io,direct
   GO111MODULE=on

1.3 Completamiento en editores

  1. Instalar la herramienta gopls.
$ go get golang.org/x/tools/gopls@latest

1.4 Otras Herramientas

2 Hello World

Vamos a programar el tradicional Hello World.

2.1 Pasos

  1. Crear un directorio llamado hello-world.
  2. Crear un archivo de texto llamado main.go dentro del directorio creado en el paso 1.
  3. Agregar el siguiente código al archivo main.go
  package main

  import "fmt"

  func main() {
      fmt.Println("Hello World")
  }
  • Ejecutar el programa con el comando go run main.go

2.2 ¿Qué sucede exactamente?

En esta sección vamos a revisar cada línea del programa Hello World.

2.2.1 Packages e imports

Linea 1 > =package main

Los packages son la forma de agrupar funcionalidad común en Golang. El package default es main y usualmente significa que el archivo que lo contiene es el punto de entrada.

Linea 3 > =import "fmt"

En esta línea especificamos que el package fmt (que se incluye como parte de Golang), es requerido para ejecutar este programa.

2.2.2 Funciones

Linea 5 > =func main() {

Las funciones en Golang se definen con el keyword func. En esta línea estamos definiendo una función llamada main sin argumentos. El código de la función se escribe entre { }.

Para crear un ejecutable en Golang tiene que existir una funcion llamada main dentro de un package también llamado main. Solo puede haber una funcion main, incluso si tenemos muchos archivos .go (solo puede existir un entrypoint por ejecutable)

2.2.3 Bibliotecas

Línea 6 > =fmt.Println("Hello World")

Esta línea imprime Hello World en la consola. Println es una función dentro del package fmt que recibe como argumento el string "Hello World".

La función Println hace dos cosas::

  • Imprime la entrada que el usuario específica en el argumento.
  • Agrega una nueva línea después de imprimir la entrada especificada.

3 Variables

3.1 Definiendo variables

Una variable es un nombre que se le asigna a una porción de memoria para almacenar un valor de un determinado tipo.

El keyword var se usa para declarar variables. La sintaxis es var <name> <type> seguido (opcionalmente) por un valor inicial.

3.1.1 Ejemplos de variables

  var a int // Inicializado por defecto

  var b = 10 // Se infiere el tipo

  var c, d = 20, 30 // Asignando valores múltiples

  e, f := 40, 50 // Crear y asignar

3.1.2 Valor inicial

Si no se especifica un valor inicial Golang asigna uno por defecto dependiento del tipo de dato de la variable.

Para imprimir el valor de una variable se puede usar Println.

fmt.Println("a = ", a)

4 Tipos

4.1 Tipos simples

Los siguientes tipos de datos están disponibles en Golang:

  • bool
  • Tipos numéricos:
    • int8, int16, int32, int64, int
    • uint8, uint16, uint32, uint64, uint
    • float32, float64
    • complex64, complex128
    • byte
    • rune
  • string
  • error
  • chan

4.1.1 Booleanos

bool representa valores boleanos true o false.

4.1.2 Tipos numéricos

Para tipos numéricos, el número que sigue al tipo indica la cantidad de bits que se usa para representar el valor en memoria. El número de bits determina qué tan grande el número puede ser.

int8: 8 bit signed integers

int16: 16 bit entero con signo

int32: 32 bit entero con signo

int: 32 or 64 bit entero con signo (dependiende de la plataforma).

uint : 32 or 64 bit entero sin signo (dependiende de la plataforma).

float32: 32 bit número flotante.

float64: 64 bit número flotante.

byte alias de uint8

rune alias de int32

4.1.3 Números complejos

complex64: números complejos con parte real e imaginaria float32

complex128: números complejos con parte real e imaginaria float64

La función complex se utiliza para construir un número complejo con partes reales e imaginarias:

  func complex(r, i FloatType) ComplexType

4.1.4 El tipo string

string es una colección de caracteres encerrados entre comillas.

  first := "Allen"
  last := "Varghese"
  name := first +" "+ last
  fmt.Println("My name is",name)

4.1.5 Errores y canales.

error es un tipo especial para indicar condiciones de error.

chan es un tipo especial que representa un canal de comunicación.

4.2 Conversion de tipos

No hay conversión automática de tipos en Golang. Se utilizan funciones de tipo para hacer una conversión.

  int(<float value>)
  float64(<integer value>)

4.3 Constantes

Las constantes no cambian sus valores una vez asignadas. Se usa el keyword const en vez de var para declarar una constante.

  const a bool = true
  const b int32 = 32890
  const c string = "Something"

5 Funciones

5.1 Funciones en Golang

Una función es un grupo de instrucciones que se ejecutan todas juntas como un bloque. Una función puede o no tener argumentos de entrada, y retornar valores.

En Golang una función se define con el keyword func. El siguiente es un ejemplo de una función que suma dos enteros:

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

5.2 Retornando valores

El keyword return es usado para indicar qué valor va a retornar la función.

Golang soporta que una función devuelva múltiples valores:

  func SumDifference(a int, b int) (int, int) {
      return a + b, a - b
  }

5.3 Identificador en blanco

Se utiliza el identificador en blanco en el lugar de un valor que se quiere descartar al llamar a una función:

  var _, diff = SumDifference(10, 20)

  fmt.Println("Difference is ", diff)

5.4 Valores de retorno con nombres

Cuando se define una función se le puede asignar un nombre al tipo de dato de retorno para luego referenciarlo en el código de la función.

  func Product(a int, b int) (prod int) {
      prod = a * b
      return
  }

Al asignar un valor de retorno con nombre no hace falta incluirlo en la sentencia return.

5.5 Funciones anónimas y clausuras

Golang soporta funciones anónimas y de segundo orden. Tomemos por ejemplo la función sort.Slice

func Slice(slice interface{}, less func(i, j int) bool)

El parámetro less describe una función que toma dos enteros y retorna un valor bool

5.5.1 Funciones anónimas.

Podemos asignar una función a una variable o definirla en el momento de su uso. Las funciones anónimas forman clausuras

c := []int{20, 11, 12, 1, 5}
less := func(i int, j int) bool {
    return c[i] < c[j]
}
sort.Slice(c, less)
fmt.Println("Despues del sort", c)
//Output: [1 5 11 12 20]
c := []int{20, 11, 12, 1, 5}
sort.Slice(c, func(i int, j int) bool {
    return c[i] < c[j]
})
fmt.Println("Despues del sort", c)
//Output: [1 5 11 12 20]

6 Instrucciones de control

Golang soporta múltiples instrucciones de control e iteradores.

6.1 if=/=else

Se pueden tener muchas instrucciones else if. El bloque else es opcional:

  if condition {
      // codigo a ejecutar
  } else if condition {
      // codigo a ejecutar
  } else {
      // codigo a ejecutar
  }

El bloque else debe estar en la misma línea que la llave de cierre (}).

6.1.1 Inicializar la condición.

La instruccion if soporta indicar una instrucción opcional que se ejecuta antes de la condición:

  if statement; condition {
      // código a ejecutar
  }

6.2 for

Hay una sola instrucción de iteración en Golang, el iterador for.

  for initialization; condition; post {
      // codigo a ejecutar
  }

Los tres componentes initialization, condition y post son opcionales en Golang.

6.2.1 for en detalle

  1. La inicialización se ejecuta una sola vez.
  2. La condición se prueba antes de ejecutar el ciclo.
  3. El post se ejecuta después de cada iteración del ciclo.

6.2.2 Ejemplo de for

El siguiente ejemplo imprime números del 1 al 10 usando un iterador for:

  for i := 1; i <= 10; i = i + 1 {
      fmt.Println(i)
  }

6.3 switch

La instrucción de control switch evalúa una expresión y la compara contra una lista de posibles valores o expresiones que puedan coincidir. Es una forma abreviada de escribir muchas cláusulas if else.

  switch expression {
      case expression or value | (, expression or value)*:
          // código a ejecutar
          (break | fallthrough)
      case expression or value | (, expression or value)*:
          // código a ejecutar
      default:
          // código a ejecutar
  }

6.3.1 break, fallthrough y default

  1. break detiene el flujo y sale del switch
  2. fallthrough continúa al próximo case.
  3. Si no hay coincidencias, se ejecuta el código en el bloque default.

7 Arreglos

7.1 Arreglos en Golang

Una colección de elementos de un mismo tipo es un arreglo. Como Golang es un lenguaje fuertemente tipado no es posible mezclar valores de diferentes tipos en un arreglo.

Un arreglo se define como [size]type:

  var a [3]int
  fmt.Println(a)

// Output: [0 0 0]

7.1.1 Comenzando en 0.

El primer índice de un arreglo es 0 en vez de 1. Se accede a los valores de un arreglo usando el número del índice, y se asignan valores con el operador =.

  var a [3]int
  a[0] = 1
  a[1] = 2
  a[2] = 3
  fmt.Println(a)
// Output: [1 2 3]

7.1.2 Inicializando arreglos.

Un arreglo se puede inicializar en la declaración:

  a := [3]int{1, 2, 3}

Se puede omitir el número de elementos si se inicializa con valores en la declaración:

  a := [...]int{1, 2, 3}

7.1.3 Tipos valor

Un detalle importante a tener en cuenta es que un arreglo es un tipo valor, esto quiere decir que cada vez que un arreglo es asignado a una nueva variable, se hace una copia del arreglo original. Si se cambian valores en la nueva variable, esto no se ve reflejado en la variable original.

  a := [...]string{"IRL", "IND", "US", "CAN"}
  b := a

  b[1] = "CHN"

  fmt.Println("Original:", a)
  fmt.Println("Copy    :", b)

Esto afecta el modo en que los arreglos son pasados por parámetros.

7.2 Funciones para operar con arreglos.

7.2.1 len

La función len se usa para conocer el tamaño de un arreglo:

  a := [...]int{1, 2, 3}
  fmt.Println(len(a))

// Output: 3

7.2.2 range

Una forma de interactuar con un arreglo es utilizar el keyword range. Este retorna el índice del arreglo y el valor.

  a := [...]int{1, 2, 3, 4, 5}
  sum := 0
  for i, v := range a {
      fmt.Println("Index:", i, " Value is:", v)
      sum += v
  }
  fmt.Println("Sum:", sum)

// Output: 15

7.2.3 range y el blank identifier

Se puede usar el blank identifier (_) si no nos interesa alguno de los valores que retorna el keyword range.

  _, v := range a

7.3 Arreglos multidimensionales

Se pueden crear arreglos de más de una dimensión de la siguiente manera:

  a := [3][2]int

7.3.1 Inicializando arreglos multidimensionales

  a := [3][2]string{
      {"lion", "tiger"},
      {"cat", "dog"},
      {"pigeon", "peacock"},
  }
  fmt.Println(a)
// Output: [[lion tiger] [cat dog] [pigeon peacock]]

7.4 Slices

Los arreglos tienen tamaño fijo, reservan memoria, y son tipos valor. Un slice es una forma flexible de acceder a una arreglo. Un slice no tiene datos, solo apunta a un arreglo.

7.4.1 Creando slices

Un slice vacío se crea así:

  var sa []int

El valor de un slice vacío es nil

Un slice también se puede crear apuntando a un subconjunto de valores de un arreglo:

  a := [...]int{1, 2, 3, 4, 5}
  sa := a[1: 4]
  fmt.Println(sa)
// Output: [2 3 4]

7.4.2 Tipos referencia

Como un slice es un tipo referencia, modificar un valor en un elemento del slice modifica el arreglo original.

  a := [...]int{1, 2, 3, 4, 5}
  sa := a[1: 4]

  fmt.Println("Before:", a)
  sa[0] = 22

  fmt.Println("After:", a)

// Output: Before: [1 2 3 4 5]
// Output: After: [1 22 3 4 5]

7.4.3 La función make

Un slice también se puede crear utilizando la funcion make, especificando el tipo, y el tamaño, y opcionalmente la capacidad (que indica el máximo tamaño que el slice puede crecer):

#+begin_src go
  i := make([]int, 5, 5)
  fmt.Println(i)
// Output: [0 0 0 0 0]

Crear un slice con make inicializa todos sus valores con los valores por defecto del tipo del slice.

7.4.4 Cambiando el tamaño de un slice

El tamaño de un slice se puede incrementar utilizando la función append.

  sa := []int{1, 2, 3}
  newSa := append([]int{}, sa...)
  fmt.Println(newSa)
// Output: [1, 2, 3]

En vez de valores se puede indicar otro slice. El operador ... se usa para expandir el slice en sus valores.

8 Maps

8.1 Tipos asociativos, map

Un map es un tipo de dato incluido en Golang que asocia una clave con un valor. El valor puede ser recuperado a partir de la clave.

Un map vacío se crea con la función make:

  make(map[type of key]type of value)

  eg: make(map[string]int)

8.2 Agregando valores

Los valores en un map se referencian igual que en un arreglo. Un valor se agrega a un map asignándole una clave, si la clave ya existe el valor para esa clave se sobrescribe.

  m := make(map[string]int)
  m["a"] = 1
  m["b"] = 2

  fmt.Println(m)

// Output: map[a:1 b:2]

8.2.1 Inicializando un map

Un map se puede inicializar con valores en su declaración igual que un arreglo:

  m := map[string]int {
      "a": 1,
      "b": 2,
  }

8.3 Obteniendo valores

Una manera de chequear si una clave existe es la siguiente:

  data, ok := m["some key"]

Si ok es true, la clave existe y la variable data contendrá la información recuperada, si ok es false la clave no existe.

Acceder a una clave inexistente en un map causa un panic.

8.4 Iteraciones

El iterador for se utiliza para recorrer las claves y valores de un map

  m := map[string]int {
      "a": 1,
      "b": 2,
      "c": 3,
  }
  for key, value := range m {
      fmt.Println("Key:", key, " Value:", value)
  }
// Output: Key: a  Value 1
//         Key: b  Value 2
//         Key: c  Value 3

Se puede utilizar el identificador en blanco (_) para descartar cualquiera de los elementos del par.

El orden en que se recorre el map no esta garantizado, y cada ejecución puede tener un order diferente.

8.5 Eliminacion

Los valores se eliminan de un map con la función delete. La función no retorna nada. La sintaxis es la siguiente:

  delete(m, key)

8.6 Tamaño

El tamaño de un map es retornado por la funcion len. Esta funcion retorna la cantidad de claves que hay en un map. La sintaxis es la siguiente:

  len(m)

8.7 Tipo

Un map es un tipo referencia. Por lo tanto si un map se asigna a otra variable, ambas variable apuntan a los mismos pares (clave, valor). Lo mismo sucede si se pasa como argumento en una funcion.

9 Paquetes y módulos

9.1 Paquetes

En Golang, los paquetes corresponden a los archivos en un mismo directorio. Por convención, los paquetes llevan el nombre del directorio que los contiene. Aunque esto no es requerido es considerado una buena práctica.

Supongamos que creamos el proyecto github.com/ourcompany/superproduct

+-- LICENSE
+-- README.md
+-- config.go  # package superproduct
+-- go.mod
+-- go.sum
+-- client
       +-- lib.go             # package client
       +-- lib_test.go        # package client
+-- cmd
       +-- super-client
           +-- main.go        # package main
       +-- super-server       # package main
           +-- main.go
+-- internal
       +-- auth
           +-- auth.go        # package auth
           +-- auth_test.go   # package auth
+-- server
       +-- lib.go             # package server

El paquete main tiene significado especial ya que representa el punto de entrada de la aplicación. Como se ve en el ejemplo anterior podemos tener más de una aplicación en un mismo proyecto Golang (super-client y super-server).

Al menos uno de los archivos en el paquete main debe tener una función main.

9.2 Estructura de un proyecto

  1. En la raíz se situan los archivos con funcionalidades comunes que se desean exportar.
  2. Las funcionalidades específicas van dentro de subpaquetes (client y server en el ejemplo). Pueden existir varios niveles de subpaquetes.
  3. Las funcionalidades que no son parte de nuestra API van dentro del paquete internal (Golang no permite importar internal o ninguno de sus subpaquetes desde módulos de terceros)
  4. Las aplicaciones ejecutables se ubican en subpaquetes de cmd.

9.3 Importando paquetes.

Para importar paquetes utilizamos la ruta completa del paquete dentro del módulo

package main

import "fmt"
import "github.com/ourcompany/superproduct"
import "github.com/ourcompany/superproduct/client"

func main() {
  fmt.Println(superproduct.Config())
  fmt.Println(client.Hello())
}

9.3.1 Alias con import

Es posible usar alias para los paquetes importados

import s "github.com/ourcompany/superproduct"

9.3.2 Importando varios paquetes

Cuando existen varios imports, la convención es agruparlos.

import (
    "fmt"
    s "github.com/ourcompany/superproduct"
    "github.com/ourcompany/superproduct/client"
)

9.3.3 Casos especiales

Existen dos casos especiales de alias para import

  1. El . incorpora todos los elementos del paquete que se importa a nuestro namespace.
  2. El _ no incorpora ninguno de los elementos del paquete importado a nuestro namespace.

9.4 La función init

La función init nos permite ejecutar código de inicialización para nuestros paquetes. A diferencia de la función main, puede existir más de una función init por paquete.

// tomado de /github.com/lib/pq/conn.go

func init() {
    sql.Register("postgres", &Driver{})
}

9.5 Reglas de visibilidad

Las reglas de visibilidad en Golang siguen un patrón sencillo.

  1. Todo elemento con letra inicial mayúscula es exportado.
  2. Todo elemento con letra inicial minúscula no es exportado.

En el ejemplo el tipo square no es exportado la función NewSquare si.

package geometry
type square struct {
    a int
    b int
}

func NewSquare(a, b int) *square {
    return &square{a, b}
}

Es posible para un tipo no exportado tener campos o métodos que sean exportados.

// Area of a square
func (s square) Area() int {
    return s.a * s.b
}

En este caso es posible acceder a los elementos exportados aunque no sea posible declarar explicitamente que se accede al tipo.

// Inválido porque square no es exportado
//var s *geometry.square = geometry.NewSquare(length, breadth)
s := geometry.NewSquare(length, breadth)
fmt.Println("Area is", s.Area())

9.6 Módulos

A partir de la versión 1.13, Golang incluye un sistema nativo de manejo de dependencias utilizando módulos. En versiones anteriores el código de nuestros proyectos tenía que ubicarse en $GOPATH/src. Ese enfoque es ahora considerado obsoleto

Para crear un módulo ejecutamos el siguiente comando:

go mod init <nombre del módulo>

Por convención el nombre del módulo es la URL del repositorio de control de versiones que alberga el código.

El sistema de módulos depende de dos archivos.

  1. go.mod que incluye la definición y las dependencias directas.
  2. go.sum que incluye las dependencias directas e indirectas con versiones exactas y suma de verificación.

Golang incluye el subcomando mod para ejecutar diferentes tareas relacionadas con módulos. Para más detalles ejecutar

go help mod

9.7 Manejando dependencias

Existen varias formas de adicionar dependencias a nuestro módulo. La más simple es importar la dependencia en el código y ejecutar el siguiente comando.

go mod tidy

Los entornos de desarrollo modernos y el Go Language Server hacen este proceso de forma automática

9.8 Referencias

10 Línea de comandos

10.1 Argumentos

Interactuar con la línea de comandos es muy útil y muy común al ejecutar scripts y programas que no tienen GUI. El paquete os se usa para tener acceso a los argumentos de la línea de comandos al ejecutar un programa de Golang.

Los argumentos de la línea de comandos están disponibles en el arreglo os.Args:

  import "os"

  os.Args // Lista completa incluyendo el nombre del programa
  os.Args[1:] // Solo los argumentos

10.2 Flags

Especificar los argumentos como valores separados por espacios en la terminal es muy básico y difícil de extender. El uso de flags provee mas flexibilidad en cómo los argumentos se especifican, sobre todo los opcionales. Golang cuenta con un paquete llamado flag para soportarlos.

  import "flag"

  flag.<type>(nombre del flag, valor default, descripcion)

Un flag se define con la sintaxis anterior. El <type> puede ser string, int o bool.

Un flag opcional se puede indicar controlando si el valor es el mismo que el valor especificado por defecto.

Un flag se puede leer independientemente de la posición en que el usuario los hubiera especificado.

10.3 Environment Variables

Las variables de entorno se usan para configurar aspectos de sistema en la mayoría de los *NIX y también en Windows. Son pares de clave-valor usualmente disponibles para todos los programas que corren en la terminal/shell. Se pueden definir variables de entorno custom solo disponibles durante la ejecución de un script de shell.

El paquete os proporciona dos funciones, Setenv y Getenv, para escribir y leer variables de entorno. Un string vacío se retorna si la clave no existe.

  import "os"

  os.Setenv("FOO", "1")
  fmt.Println("FOO:", os.Getenv("FOO"))
  fmt.Println("BAR:", os.Getenv("BAR"))

//Output: FOO: 1
//        BAR:

La función Environ retorna la lista completa de todas las variables de entorno existentes.

  import "strings"

  for _, e := range os.Environ() {
      pair := strings.Split(e, "=")
      fmt.Println(pair[0])
  }

11 Manejo de errores: error, defer, recover, panic

11.1 Manejo de errores en Golang

En lugar de excepciones como Python, Java o C#, Golang sigue un enfoque más cercano a los lenguajes funcionales donde el estado de error es representado por un tipo de datos,

  var DivisionByZero = errors.New("División por cero")

  func safedivide(a int, b int) (int, error) {
      if b == 0 {
          return 0, DivisionByZero
      }
      return a / b, nil
  }

11.2 El tipo error

El tipo error es una interfaz built-in del lenguaje

  type error interface {
      Error() string
  }

11.2.1 Errores personalizados

Esto nos permite crear nuestros propios tipos de errores

  type MyError struct {
      message: string
      status: int
  }

  func (m *MyError) Error() string {
      return fmt.Sprintf("Error - %s. Status %d", m.message, m.status)
  }

  func returnMyError() error {
      return &MyError{
       message: "None",
       status : -1,
      }
  }

11.2.2 Operando con errores

El paquete errors contiene funciones dedicadas a manejar tipos de error. Podemos usar la función errors.Is para verificar si un error es de un tipo específico.

  if _, err := os.Open("non-existing"); err != nil {
      if errors.Is(err, os.ErrNotExist) {
          fmt.Println("file does not exist")
      } else {
          fmt.Println(err)
      }
  }

11.3 Defer

La sentencia defer se utiliza para indicar que la función a continuación se debe ejecutar a la salida de la función actual

Es muy usual en Golang usar defer para destruir o liberar cualquier tipo de recurso temporal que se obtenga en la función.

  func doSomething() {
   a := getExternalResource();
   defer a.Release()
   /// Resto de la función
  }

defer se ejecuta sin importar las causas por las que la función actual haya terminado, lo que garantiza en el ejemplo anterior que a.Release() siempre se ejecute.

11.4 Panic y recover

Golang incluye una función especial panic para indicar un error que no puede ser manejado de forma correcta.

La contraparte de panic es la función recover, que verifica si ocurrió una llamada a panic en el contexto de la función actual.

Si una llamada a panic no es seguida por un recover, la función termina y el contexto pasa al invocador. Esto continúa hasta que se encuentre un recover o se llegue a la función main en cuyo caso el programa se detendrá con un mensaje de error.

  func f() {
      defer func() {
          if r := recover(); r != nil {
              fmt.Println("Recovered in f", r)
          }
      }()
      fmt.Println("Calling g.")
      g(0)
      fmt.Println("Returned normally from g.")
  }

  func g(i int) {
      if i > 3 {
          fmt.Println("Panicking!")
          panic(fmt.Sprintf("%v", i))
      }
      defer fmt.Println("Defer in g", i)
      fmt.Println("Printing in g", i)
      g(i + 1)
  }

11.5 Panic y recover no son try y catch

Uno de los errores más comunes para los que se inician en Golang es pensar en panic y recover como una alternativa a los bloques try-catch que aparecen en otros lenguajes. Esto es considerado una mala práctica.

La función panic se debe utilizar solo para indicar estados en el flujo de una aplicación para los cuales no hay solución efectiva. Por otro lado recover debería utilizarse para liberar o destruir recursos adquiridos por la aplicación antes de hacer una salida forzosa.

12 Pruebas unitarias

Golang incluye en el paquete testing las herramientas necesarias para hacer pruebas unitarias. Por convención las pruebas unitarias se incluyen en el mismo paquete de la funcionalidad a probar, adicionando a los archivos el sufijo _test.

Además de pruebas unitarias, Golang nos permite escribir pruebas de rendimiento o benchmarks y ejemplos de prueba.

12.1 Nuestra primera prueba unitaria.

Si tenemos una función para calcular los números de fibonacci.

  func Fibonacci(n int) int {
      if n < 2 {
          return n
      }
      return Fibonacci(n-1) + Fibonacci(n-2)
  }

12.1.1 Pruebas unitarias

  func TestFibonacci(t *testing.T) {
      var in int = 10
      var want int = 55

      got := Fibonacci(in)
      if got != want {
          t.Errorf("Fibonacci(%d) == %d, want %d", in, got, want)
      }
  }

12.1.2 Pruebas de ejemplo

Los ejemplos además de ejecutar pruebas, son una forma de documentar el uso de funcionalidades.

  func ExampleFibonacci() {
      fmt.Println(Fibonacci(10))
      // Output: 55
  }

Tanto las pruebas unitarias como los ejemplos se ejecutan utilizando el comando go test.

12.2 Benchmarks

Un benchmark o prueba de rendimiento nos permite examinar el desempeño de nuestras funcionalidades.

  func BenchmarkFibonacci(b *testing.B) {
          for n := 0; n < b.N; n++ {
                  Fibonacci(10)
          }
  }

12.3 Datos de pruebas o fixtures

Por convención, Golang ignora cualquier directorio que empiece con ., _ o se llame testdata. Dado que las pruebas siempre se ejecutan en el directorio donde se encuentra el archivo _test podemos usar la función Join del paquete path/filepath para acceder a los datos de prueba.

  func TestBytesInFile(t *testing.T) {
      l, err := BytesInFile(filepath.Join("testdata", "hello_world"))
      if err != nil {
          t.Errorf("Error %s", err)
          t.FailNow()
      }
      if l != 12 { // New line at end of file
          t.Errorf("Got wrong number of bytes %d", l)
      }
  }

13 Structs y punteros

13.1 Tipos compuestos, struct

Un struct es un tipo de dato definido por el usuario que representa una colección de campos.

  type Employee struct {
      firstName string
      lastName string
      age int
  }

Los campos individuales de un struct se acceden con ..

  var emp := Employee {
      firstName: "Something"
  }

  fmt.Println(emp.firstName)

//Output: Something

13.2 Campos struct

Golang permite que los campos de un struct puedan ser de tipo struct.

  type Address struct {
      city, state string
  }

  type Employee struct {
      firstName, lastName string
      age int
      address Address
  }

13.2.1 Inicializando un struct

Para inicializar un campo struct simplemente nos referimos al mismo en el bloque de inicialización

  var emp Employee

  emp := Employee{
      firstName: "Peter",
      lastName:  "Parker",
      age:       22,
      address: Address{
          city:  "New York",
          state: "New York",
      },
  }
  fmt.Println(emp)
//Output: {Peter  {New York New York}}

Los campos anidados se acceden con múltiples niveles de notación de punto.

  fmt.Println(emp.address.city)

//Output: New York

13.3 Etiquetas

Los campos en las estructuras pueden opcionalmente tener etiquetas. Las etiquetas funcionan como metadatos y no afectan la forma en que se representan los datos de la estructura.

Las etiquetas pueden opcionalmente ser pares llave:"valor".

type Employee struct {
    firstName string `json:"nombre"`
    lastName  string `json:"apellido"`
    age       int    `json:"edad"`
}

Por convención la llave corresponde al nombre del paquete encargado de procesar la anotación.

13.4 Composición

Un struct puede también "heredar" todos los campos de otro usando composición. Para esto existe una notación especial

  type Address struct {
      city, state string
  }

  type Employee struct {
      firstName, lastName string
      age int
      Address
  }

13.4.1 Inicializar struct compuesto

Al crear una estructura compuesta tenemos que especificar todos los campos

  emp := Employee{
      firstName: "Peter",
      lastName:  "Parker",
      age:       22,
      Address: Address{
          city:  "New York",
          state: "New York",
      },
  }
  fmt.Println(emp.city)
//Output: New York

13.5 Igualdad

Un struct es un tipo por valor. Dos variables struct se consideran iguales si todos los valores de sus campos son iguales. Esto quiere decir que si un struct tiene campos que no se pueden comparar, como un map, la operación (==) va a fallar.

13.6 Métodos/Receptores

Los métodos (también llamados receptores) son funciones asociadas a un tipo especifico. Son similares al concepto de métodos de clase en el mundo OOP. La sintaxis es la siguiente:

  func (t Type) MethodName(parameter list) {
      // codigo del metodo
  }

13.6.1 Definiendo métodos

Usualmente se define el código del método en el mismo archivo que el tipo que lo contiene.

  // Receptor por valor
  func (e Employee) Print() {
      fmt.Println("Employee Record:")
      fmt.Println("Name:", e.firstName, e.lastName)
      fmt.Println("Address:", e.address)
  }
  // en main
  var emp Employee
  emp.Print()
// Outpput  Employee Record:
//          Name: Allen Varghese
//          Address: {AA CO}

13.7 Punteros

Golang soporta punteros para actualizar valores pero no admite aritmética de punteros como en C. * se usa como prefijo para definir un puntero para un tipo dado. El operador & se usa para crear punteros a tipos.

13.7.1 Inicializando punteros

El valor por defecto de los punteros en Golang es nil, este valor también se utiliza para indicar que un puntero es nulo.

Tener en cuenta que un puntero solo permite recibir punteros de su tipo y no otros.

var emp *Employee // puntero nil

emp = &Employee{...} // puntero a Employee

13.7.2 Tipos referencias

Un puntero es una referencia a un tipo, por lo que podemos utilizarlo para modificar el valor original. Los receptores por puntero o por referencia son una aplicación directa de este concepto.

  // Receptor por puntero
  func (e *Employee) updateAge(newAge int) {
      e.age = newAge
  }
  // En main
  emp := Employee{
      age: 33,
  }
  fmt.Println("Before:", emp.age)
  emp.updateAge(34)
  fmt.Println("After:", emp.age)

//Output: Before: 33
//        After: 34

13.8 Receptores: puntero o valor

  • Elección del tipo de receptor.
    1. Usar solo una variante de receptor para un mismo tipo.
    2. Ante la duda, usar receptores por puntero.
  • Usar receptores por puntero.
    1. Cuando el método es inmutable.
    2. Para estructuras que contienen campos que no se deben copiar (ej. sync.Mutex).
    3. Para arreglos o estructuras de gran tamaño.
  • Usar receptores por valor.
    1. Para tipos map, func o chan
    2. Para tipos básicos como int o string
    3. Cuando el tipo del receptor no contiene valores mutables

14 Interfaces

14.1 Interfaces en Golang

En Golang, una interfaz es un conjunto de firmas de métodos. Si un tipo tiene una definición para esos métodos, se dice que implementa la interfaz. A diferencia de otros lenguajes, la asociación de un tipo con una interfaz es implicita.

Un tipo puede implementar más de una interfaz.

  type Permanent struct {
      empID int
      basicPay int
      pension int
  }

  type Contract struct {
      empID int
      basicPay int
  }

  func (p Permanent) calculateSalary() int {
      return p.basicPay + p.pension
  }

  func (p Contract) calculateSalary() int {
      return p.basicPay
  }

  type SalaryCalculator interface {
      calculateSalary() int
  }

  // In main
  sc := [...]SalaryCalculator{Permanent{1, 5000, 50}, Contract{3, 7000}}
  totalPayout := 0
  for _, v := range sc {
      totalPayout += v.calculateSalary()
  }
  fmt.Println(totalPayout)

14.2 Interfaces anidadas.

Las interfaces al igual que las estructuras pueden anidarse.

  type Reader interface {
      Read(p []byte) (n int, err error)
  }

  type Seeker interface {
      Seek(offset int64, whence int) (int64, error)
  }

  type ReadSeeker interface {
      Reader
      Seeker
  }

Para que un tipo implemente la interfaz ReadSeeker tiene que implementar Read y Seeker a la vez.

14.3 Interfaces y composición.

De la misma forma que una estructura "hereda" los métodos de otra en la composición, implementa las interfaces de aquellas que la componen.

  type Animal interface {
      Name() string
  }

  type Dog struct{}

  func (d *Dog) Name() string {
      return "Dog"
  }

  func Bark(d *Dog) {
      fmt.Println("Woof!")
  }

  type GuideDog struct {
      *Dog
  }

En el ejemplo anterior el tipo GuidedDog implementa la interfaz Animal

14.4 La interfaz vacía

El tipo interface{} representa un valor comodín al estilo del tipo object de C# o void * en C (técnicamente todos los tipos implementan una interfaz sin métodos)

  func describe(i interface{}) {
      fmt.Printf("(%v, %T)\n", i, i)
  }

14.5 Type Assertions.

A veces nos es necesario saber de qué tipo es el valor guardado en la interfaz.

    var i interface{} = "hello"
    s := i.(string)
    fmt.Println(s)

    s, ok := i.(string)
    fmt.Println(s, ok)

    f, ok := i.(float64)
    fmt.Println(f, ok)

    f = i.(float64) // panic
    fmt.Println(f)

La sintaxis variable.(Tipo) es un type assertion, concepto muy parecido al type casting de otros lenguajes.

14.5.1 Type assertions seguras e inseguras

Los type assertions se ejecutan con alguna de las siguientes variantes

// variante segura
   v, ok := i.(Tipo)
// variante insegura
   v := i.(Tipo)

La variante segura retorna un par (Tipo,bool) donde el segundo valor representa el estado de la operación. Un estado de true significa que se pudo efectuar la conversión, false que la conversión no es posible y el primer valor de la tupla estará con el valor nulo.

En la variante insegura si no se puede efectuar la conversión el runtime de Golang lanzará un panic.

14.6 Type switches

Los type switches son una construcción especial que nos permite determinar el tipo de una variable y actuar en consecuencia

switch v := v.(type) {
    case string:
        fmt.Printf("%v is a string\n", v)
    case int:
        fmt.Printf("%v is an int\n", v)
    default:
        fmt.Printf("The type of v is unknown\n")
}

En lugar de usar la sintaxis v.(Tipo) para la conversión se utiliza v.(type).

15 Concurrencia en Go.

15.1 Gorutinas

Las gorutinas son funciones o métodos que se ejecutan de modo concurrente. Para los efectos se pueden considerar como hilos o threads ligeros.

Las gorutinas se crean usando la palabre clave go antes de la llamada a una función.

// llamar name como una gorutina
go name()

// gorutina  anónima como
go func() {
// código
}()

15.1.1 Sincronizando gorutinas

Las gorutinas se ejecutan de modo concurrente. Para sincronizar gorutinas es común utilizar elementos de sincronización como sync.WaitGroup.

func main() {
    wg := &sync.WaitGroup{}
    wg.Add(4)
    for i:=0; i < 4; i++ {
        go worker(wg)
    }
    // Sin esto es posible que main termine
    // antes de las gorutinas.
    wg.Wait()
}

func worker(w *sync.WaitGroup) {
    defer wg.Done()
    DoWork()
}

15.2 Canales

Los canales son un mecanismo para sincronización y comunicación entre gorutinas. Los usuarios de sistemas basados en Unix pueden pensar en canales como pipes.

Para crear un canal usamos la función make.

c := make(chan string) // canal sin buffer
cb := make(chan string, 200) // canal con buffer de 200

15.2.1 Tipos de canales

La comunicación vía canales en síncrona. Cuando se envían datos a un canal la gorutina se bloquea hasta que estos datos sean recibidos. Lo mismo ocurre si se intenta recibir sin haber datos disponibles.

Los canales con buffer permiten acumular datos enviados y solo bloquean si

  1. Se han enviado n datos, donde n es el tamaño del buffer.
  2. Se intenta recibir desde el canal y el buffer no contiene datos.

15.2.2 Terminando la comunicación.

Para cerrar un canal se utiliza close. Un canal cerrado no puede usarse para enviar datos. Recibir datos de un canal cerrado siempre retornará el valor cero del tipo de datos del canal.

// Enviar
c <- "Hola"
// Recibir
v := <- c
// Recibir mientras el canal esté abierto
for v := range c {
    processValue(v)
}

15.2.3 Multiplexing

La sentencia select nos permite esperar por el resultado de más de un canal.

// Esperamos por mensaje o timeout
for {
    select {
    case v := <-input:
        doOperation(v)
    // Canal de tipo <- chan Time
    case <- time.After(time.Duration(80) * time.Millisecond):
        fmt.Println("Timeout!")
        return results
    }
}

15.3 Referencias

  1. Concurrency is Not Parallelism
  2. Gorutines - A Tour of Go
  3. Channels - A Tour of Go
  4. Buffered Channels - A Tour of Go

16 Formatos de datos: JSON y XML

16.1 El paquete encoding.

El paquete encoding define interfaces para convertir datos desde/hacia bytes o representaciones de texto.

  • ascii85 Formato utilizado por Adobe PDF y Postcript.
  • asn1 Estructuras ASN.1 en formato DER.
  • base32 Codificación Base32, RFC 4648.
  • base64 Codificación Base64, RFC 4648.
  • binary Traducción entre formatos numéricos y codificación a bytes de los mismos.
  • csv Archivos con valores separados por coma (CSV).
  • gob Transmisión de flujos de datos entre emisor y receptor ()
  • hex Valores hexadecimales.
  • json JSON como se define en la RFC 7159.
  • pem Formato PEM usado actualmente para representar llaves TLS y certificados.
  • xml Parser XML 1.0 con soporte para espacios de nombres.

16.2 Serializando a JSON (Marshal).

json.Marshal codifica tipos Golang en JSON

  • bool como JSON Boolean.
  • valores numéricos como JSON Number.
  • string como JSON String, eliminando etiquetas HTML.
  • arreglos y slices como JSON Array.
  • struct como JSON Object.
  • nil como null
  • chan, complex y literales de funciones provocan error.

16.2.1 Serializando estructuras

Las etiquetas definen como se serializan las estructuras.

type Address struct {
    // omitempty hace que los campos vacíos no
    // no se serialice.
    City  string `json:"ciudad,omitempty"`
    State string `json:"estado,omitempty"`
    // Campos marcados con - no se serializan
    Zip   strung `json:"-"`
}

type Employee struct {
    FirstName string `json:"nombre"`
    LastName  string `json:"apellido"`
    Age       int    `json:"edad"`
    // no se serializa porque no es exportado
    id        string
    Address
}
emp1 := Employee{"Peter", "Parker", 22, Address{"Manhattan", "New York"}}
d, _ := json.Marshal(&emp1)
fmt.Println(string(d))

16.3 Deserializando desde JSON (Unmarshal).

json.Unmarshal es la contraparte de json.Marshal.

tony := `{"nombre":"Tony","apellido":"Stark","edad":44}`
var emp3 Employee
err = json.Unmarshal([]byte(tony), &emp3)
if err != nil {
    log.Fatal("No se pudo deserializar")
}

16.3.1 Consideraciones.

  1. Unmarshal a un slice elimina los valores del slice.
  2. Unmarshal a un array descarta los valores extra si el array destino es muy pequeño.
  3. Unmarshal a un map conserva las llaves existentes.
  4. null se traduce a nil o al valor que tenga el tipo sin inicializar

17 Creando APIs REST

17.1 Servidor HTTP.

El paquete net/http contiene implementaciones de servidor y cliente para este protocolo. Crear un programa que responda a peticiones HTTP es una tarea sencilla.

  func main() {
      http.HandleFunc("/hello", func(w http.ResponseWWriter, r *http.Request) {
          fmt.Fprintf(w, "Hello world")
      })
      log.Fatal(http.ListenAndServe(":8080", nil))
  }

Las estructuras Server y ServeMux son las encargadas de operar el servidor y el ciclo de petición y respuesta. La variable DefaultServeMux es utilizada por el servidor por defecto

17.2 Creando nuestro propio ServeMux

Usar DefaultServeMux tiene sentido para proyectos pequeños, pero lo usual es que utilicemos nuestra propio ServeMux

  mux := http.NewServeMux()
  mux.HandleFunc("/hello", func(w http.ResponseWriter, req *http.Request) {
      fmt.Fprintf(w, "Hello world")
  })
  log.Fatal(http.ListenAndServe(":8080", mux))

Alternativamente podemos crear nuestro propio Server.

    // Create a server listening on port 8000
    s := &http.Server{
        Addr:    ":8000",
        Handler: mux,
    }

    // Continue to process new requests until an error occurs
    log.Fatal(s.ListenAndServe())

17.3 APIS REST con ServeMux.

Tanto Server como ServeMux proveen el nivel mínimo de abstracción necesario para manejar peticiones HTTP.

Tomemos por ejemplo el código para hacer un API REST sencilla

type H = func(w http.ResponseWriter, r *http.Request)

func dispatchResource(get, post, put, delete H) H {
    return func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        switch r.Method {
        case "GET":
            get(w, r)
        case "POST":
            post(w, r)
        case "PUT":
            put(w, r)
        case "DELETE":
            delete(w, r)
        default:
            w.WriteHeader(http.StatusNotFound)
            w.Write([]byte(`{"message": "not found"}`))
        }
    }
}

func main() {
    // initialize mux
    getFunc := func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte(`{"message": "get called"}`))
    }
    postFunc := func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusCreated)
        w.Write([]byte(`{"message": "post called"}`))
    }
    m.HandleFunc("/resource", dispatchResource(getFunc, postFunc, nil, nil))
    // use mux
}

17.4 Referencias

18 HTTP Client

El paquete http contiene una implementacion de un cliente http que permite emular las acciones que realiza un web browser.

Para este ejercicio se utiliza el tester HTTP https://httpbin.org (codigo fuente, imagen de Docker para uso local).

18.1 Ejecutando un GET

  resp, _ := http.Get("https://httpbin.org/get")
  defer resp.Body.Close()

  data, _ := ioutil.ReadAll(resp.Body)
  fmt.Println(string(data))

18.2 Ejecutando un POST

  payload := "Hello world!"
  resp, _ := http.Post("https://httpbin.org/post", "text/plain", strings.NewReader(payload))
  defer resp.Body.Close()

  data, _ := ioutil.ReadAll(resp.Body)
  fmt.Println(string(data))

Notar que si obtenemos un error de http.Get o http.Post debemos cerrar el body (utilizando defer).

18.3 HTTP request timeouts

Podemos especificar un tiempo maximo de espera para que un request que tarda mucho termine automaticamente.

Crear un nuevo objeto request:

  req, _ := http.NewRequest(http.MethodGet, url, nil)

Crear un context con timeout:

  ctx, cancel := context.WithTimeout(context.Background(), timeout)
  defer cancel()

Ejecutar el request pasando por parametro el context con el timeout:

  resp, _ := http.DefaultClient.Do(req.WithContext(ctx))
  defer resp.Body.Close()

19 Challenge 1.1

Crear una funcion Reverse que devuelva un string pasado por parametro.

20 Challenge 1.2

Modificar la funcion Reverse para invertir el casing de las vocales (solamente).

21 Challenge 1.3

Crear un paquete stringutil que contega la funcion Reverse y se utilice en funcion la main.

22 Challenge 1.4

Cambiar el programa para que el parametro string de la funcion Reverse se obtenga desde la linea de comandos con un flag llamado text.

23 Challenge 1.5

Crear un test, un benchmark, y un example para la funcion Reverse.

24 Challenge 2.1

Modelar la funcionalidad de un sistema de precios para una aerolinea que calcule los ingresos netos de un vuelo en base a los pasajeros y el precio base del ticket.

El precio base del ticket es el mismo para todos los pasajeros.

Existen 3 tipos de pasajeros: * Base: Paga el 100% del precio base. * De ultimo minuto: Paga el 50% del precio base. * Empleado de la aerolinea: No paga el ticket.

25 Challenge 2.2

Crear un test para verificar el codigo creado en el challenge 2.1 anterior.

26 Challenge 2.3

Agregar un nuevo tipo de pasajero 'empleado de aerolinea de ultimo minuto' cuyo descuento sea la suma de los descuentos de los tipos de pasajero 'empleado de aerolinea' y 'ultimo minuto'.

27 Challenge 3

  • Convertir las siguientes funciones para que utilicen gorutines y channels.
  • Crear dos benchmarks para comparar la velocidad de ejecucion entre las versiones con y sin uso de gorutines.
  func Squares(number int) (sum int) {
      sum = 0
      for number != 0 {
          digit := number % 10
          sum += digit * digit
          number /= 10
      }
      return
  }

  func Cubes(number int) (sum int) {
      sum = 0
      for number != 0 {
          digit := number % 10
          sum += digit * digit * digit
          number /= 10
      }
      return
  }

  func SquaresPlusCubes(number int) int {
      return Squares(number) + Cubes(number)
  }

28 Challenge 4.1

Crear una API REST para una aplicacion de recordatorios (ToDo) que contenga todas las funciones CRUD para la siguiente entidad:

  ID     int
  Title  string
  IsDone bool

Hints:

  • Los datos se pueden almacenar en un arreglo.
  • La funcion strconv.Atoi del paquete strconv permite convertir un string a un integer.

29 Challenge 4.2

Modificar la API Rest del challenge 4.1 para que pueda acceder a diferentes fuentes de datos (en memoria y MongoDB) usando el patron Repository.

Hints:

  • Se pueden copiar las funciones necesarias para operar con MongoDB desde el Ejercicio 19.

30 Challenge 4.3

Modificar la API Rest del challenge 4.2 para usar Gin.

31 Acceso a bases de datos

31.1 Bases de datos SQL

El paquete database/sql contiene interfaces genéricas para el acceso a bases de datos SQL. El concepto es similar a ADO.NET en .NET Framework o JDBC en Java.

31.1.1 Definiendo la base de datos.

Para el resto de los ejemplo asumamos que tenemos una base de datos con una tabla única.

CREATE TABLE `userinfo` (
       `uid` INTEGER PRIMARY KEY AUTOINCREMENT,
       `username` VARCHAR(64) NULL,
       `departname` VARCHAR(64) NULL,
       `created` DATE NULL
);

31.1.2 Estableciendo conexión

Para acceder a una base de datos usamos la función sql.Open. La función recibe el nombre del driver y el DSN (ver documentación del driver)

import _ "github.com/mattn/go-sqlite3"

// En una función
db, err := sql.Open("sqlite3", "./data.db")
if err != nil {
    panic("Error accediendo a la base de datos")
}
defer db.Close() // Cerrar la base de datos siempre

31.1.3 Consultas

La función db.Query nos permite consultar la base de datos. Las llamadas a db.Query retornan una estructura db.Rows que nos permite iterar sobre las filas recibidas e inspeccionar su valor.

// Consultar
rows, err := db.Query("SELECT * FROM userinfo")
var uid int
var username string
var department string
var created time.Time

// Verdadero si existen más filas
for rows.Next() {
    // Tomar los valores de la fila
    err = rows.Scan(&uid, &username, &department, &created)
    checkErr(err)
    fmt.Println(uid)
    fmt.Println(username)
    fmt.Println(department)
    fmt.Println(created)
}
rows.Close() // Close libera recursos del iterador

31.1.4 Manipulación de datos

Para ejecutar consultas que manipulen datos debemos crear una estructura sql.Stmt.

// Insertar
stmt, err := db.Prepare("INSERT INTO userinfo(username, departname, created) values(?,?,?)")
res, err := stmt.Exec("pparker", "Avengers", "2021-01-01")
// Actualizar
stmt, err = db.Prepare("update userinfo set username=? where uid=?")
res, err = stmt.Exec("spiderman", id)
// Eliminar
stmt, err = db.Prepare("delete from userinfo where uid=?")
res, err = stmt.Exec(id)
// Llamar Close para liberar recursos
err := stmt.Close()

31.2 Bases de datos NOSQL: MongoDB

La mayoriá de los drivers para bases de dato NOSQL de Golang no implementan las interfaces en database/sql por lo que cada biblioteca maneja sus propios tipos e interfaces.

31.2.1 Obteniendo los paqutes necesarios

// Necesitamos estos imports
import (
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

31.2.2 Definiendo los datos

  1. Las etiquetas bson para serializar y deserializar hacia la base de datos.
  2. Las etiquetas json para comunición entre servicios.
type Item struct {
    ID     int    `json:"id,omitempty" bson:"id,omitempty"`
    Title  string `json:"title,omitempty" bson:"title,omitempty"`
    IsDone bool   `json:"isdone,omitempty" bson:"isdone,omitempty"`
}

31.2.3 Conectar y desconectar.

type MongoDB struct {
    *mongo.Client
}

func NewMongoDB(ctx context.Context) (*MongoDB, error) {
    client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
    if err != nil {
        return nil, err
    }
    return &MongoDB{client}, nil
}

func (m *MongoDB) Disconnect(ctx context.Context) {
    defer m.Client.Disconnect(ctx)
}

31.2.4 Crear y actualizar

func (m *MongoDB) CreateItem(ctx context.Context, newItem Item) string {

    collection := m.Database("todo").Collection("items")
    result, _ := collection.InsertOne(ctx, newItem)

    return result.InsertedID.(primitive.ObjectID).Hex()
}

func (m *MongoDB) UpdateItem(ctx context.Context, item Item) {
    update := bson.M{"$set": bson.M{"title": item.Title, "isdone": item.IsDone}}

    collection := m.Database("todo").Collection("items")
    collection.UpdateOne(ctx, Item{ID: item.ID}, update)
}

31.2.5 Obtener elementos

func (m *MongoDB) GetItems(ctx context.Context) (items []Item) {
    collection := m.Database("todo").Collection("items")
    cursor, _ := collection.Find(ctx, bson.M{})

    defer cursor.Close(ctx)
    for cursor.Next(ctx) {
        var oneItem Item
        cursor.Decode(&oneItem)
        items = append(items, oneItem)
    }

    return
}
func (m *MongoDB) GetItem(ctx context.Context, id int) (item Item) {

    collection := m.Database("todo").Collection("items")
    collection.FindOne(ctx, Item{ID: id}).Decode(&item)
    return
}

31.2.6 Eliminar.

func (m *MongoDB) DeleteItem(ctx context.Context, id int) {
    collection := m.Database("todo").Collection("items")
    collection.DeleteMany(ctx, Item{ID: id})
    return
}

31.3 Referencias

  1. Documentación de database/sql
  2. Documentación del driver de MongoDB

32 Labstack Echo

32.1 Introducción

Echo es un microframework Golang para crear servicios web.

32.2 Rutas.

:CUSTOMID: rutas-middleware

32.2.1 Handlers

echo.Context representa el acceso al estado de la solicitud (ruta, parámetros, handlers, etc) y contiene los métodos para generar las respuesta

func updateUser (c echo.Context) (err error) {
    // ...
}

32.2.2 Rutas REST

import "github.com/labstack/echo/v4"

func main(){
    e := echo.New()
    e.POST("/users", createUser)
    e.GET("/users/:id", findUser)
    e.PUT("/users/:id", updateUser)
    e.DELETE("/users/:id", deleteUser)
    e.Any("/",home)
}

32.2.3 Grupos

Establecer opciones similares para varias rutas.

//Todas las URLs con /v2/*
g := e.Group("/v2")
e.POST("/users", createUserV2)
e.GET("/users/:id", findUserV2)
e.PUT("/users/:id", updateUserV2)
e.DELETE("/users/:id", deleteUserV2)

32.3 Middleware

Funciones que se procesan antes de un handler.

func MyMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        //Hacer algo
        return next(c)
    }
}

32.3.1 Incluyendo middleware

//Antes de ejecutarse el router
e.Pre(MyMiddleware)
//Después de ejecutar el router
e.Use(MyMiddleware)
// A nivel de grupo
admin := e.Group("/admin", MyMiddleware)
//A nivel de ruta
e.GET("/", <Handler>, <Middleware...>)

32.4 Obteniendo datos

32.4.1 Desde el contexto

func(c echo.Context) error {
    name := c.FormValue("name") // valor de formulario
    printFull := c.QueryParam("full") // valor de query
    return c.String(http.StatusOK, name)
}

32.4.2 Usando Bind

type User struct {
  Name  string `json:"name" form:"name" query:"name"`
  Email string `json:"email" form:"email" query:"email"`
}
func handle(c echo.Context) (err error) {
    u := new(User)
    if err = c.Bind(u); err != nil {
        return
    }
    // Hacer algo con el usuario
}

32.5 Respuestas

echo.Context es también utilizada para generar respuestas.

// Retornar una cadena
c.String(http.StatusOK, "Hello, World!")
// Retornar HTML
c.HTML(http.StatusOK, "<p>Hello, World!</p>")
// Retorna JSON, serializa el valor de u
c.JSON(http.StatusOK, u)
// Retorna XML, serializa el valor de u
c.XML(http.StatusOK, u)
// Retorna el contenido del fichero
c.File("<PATH_TO_YOUR_FILE>")
// Retorna el contenido del fichero como flujo de datos
c.Stream(http.StatusOK, "<CONTENT_TYPE>", file)
// Redirige
c.Redirect(http.StatusMovedPermanently, "<URL>")

33 Golang y Kafka

33.1 Enviando mensajes con sarama

sarama es una biblioteca desarrollada por Shopify para la comunicación con Apache Kafka

Para utilizar sarama solo tenemos que importar el módulo

import  "github.com/Shopify/sarama"

33.1.1 Configuración

config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll // Wait for all in-sync replicas to ack the message
config.Producer.Retry.Max = 10                   // Retry up to 10 times to produce the message
config.Producer.Return.Successes = true
tlsConfig := createTlsConfiguration()
if tlsConfig != nil {
    config.Net.TLS.Config = tlsConfig // de crypto/tls
    config.Net.TLS.Enable = true
}

33.1.2 Enviando mensajes síncronos

sarama.SyncProducer bloquea la gorutina en espera de confirmación

brokerlist := [...]string{"host1:por1", "host2:port2"}
producer := sarama.NewSyncProducer(brokerList, config)
partition, offset, err := s.DataCollector.SendMessage(&sarama.ProducerMessage{
    Key:   sarama.StringEncoder("llave"),
    Topic: "topic",
    Value: sarama.StringEncoder("Mensaje"),
})
producer.Close() //Liberar recursos

33.1.3 Enviando mensajes asíncronos.

sarama.AsyncProducer envía de forma asíncrona.

brokerlist := [...]string{"host1:por1", "host2:port2"}
producer, err := sarama.NewAsyncProducer(brokerList, config)
producer.Input() <- &sarama.ProducerMessage{
    Key:   sarama.StringEncoder("llave"),
    Topic: "topic",
    Value: sarama.StringEncoder("Mensaje"),,
}
producer.AsyncClose() // Liberar recursos

33.2 Recibiendo mensajes con sarama

sarama permite dos modos de recepción de mensajes.

  1. Obtener mensajes desde una sola partición utilizando sarama.Consumer
  2. Obtener mensajes de un clúster con sarama.ConsumerGroup

En la carpeta day-07/21-sarama/consumergroup se incluye un ejemplo de consumidor usando sarama.ConsumerGroup

33.3 Interceptors

Los interceptors permiten procesar un mensaje antes de que sea enviado o recibido.

33.3.1 Interceptando mensajes enviados

//
func (m *myinterp) OnSend(p *sarama.ProducerMessage) {
    // se procesa el mensaje
}
// Configuración del productor
conf.Producer.Interceptors = []sarama.ProducerInterceptor{&myinterp{}}

33.3.2 Interceptando mensajes recibidos.

// Implementar sarama.ConsumerInterceptor
func (m *myinterp) OnConsume(p *sarama.ConsumerMessage) {
    // se procesa el mensaje
}
// Configuración del consumidor
conf.Consumer.Interceptors = []sarama.ConsumerInterceptor{&myinterp{}}

34 Patrones de concurrencia

34.1 Futures

Las futures representan el resultado de un cálculo que se ejecuta de forma concurrente. Este patrón es nos permite ejecutar una operación costosa sin que impacte la ejecución del proceso principal.

34.1.1 Interfaz para Future

Definamos una interfaz que exprese el comportamiento que queremos en un future

type Value interface{}

type Future interface {
    Get(c context.Context) (Value, error)
}

Usamos context.Context para dar la posibilidad de cancelar la espera.

34.1.2 Implementando Futures

Una implementación simple para future es usar un canal que comunique el estado de completamiento o error.

type result struct {
    value Value
    err   error
}

type futureImpl struct {
    result chan *result
}

Por conveniencia las estructuras son privadas.

El método Get bloquea la gorutina actual en espera del resultado del future o la señal Done del contexto.

func (f *futureImpl) Get(c context.Context) (Value, error) {
    select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case result := <-f.result:
            return result.value, result.err
    }
}

Finalmente una función para crear nuevas futures.

func NewFuture(f func() (Value, error)) Future {
    fut := &futureImpl {
        result: make(chan *result)
    }
    go func(){
        defer close(fut.result)
        value, err := f()
        f.result <- &result{value, err}
    }()
    return fut
}