jueves, 29 de noviembre de 2018

Vamos a por Go 2

Go 2 llegará con nuevas e importantes propuestas. Ha habido mucho trabajo desarrollado por un gran equipo que ha recopilado, revisado, categorizado y decidido acerca de casi 120 nuevas mejoras, tomando muy en cuenta el intentar garantizar la retrocompatibilidad.

De todas las ideas puestas en la mesa, hay dos que destacan por su relevancia: la mejor gestión de errores y los genéricos.

Para progresar hacia Go 2, se implementará un proceso de evaluación de propuestas, el cual consistirá en:

  1. Seleccionar algunas propuestas pequeñas para ser consideradas.
  2. Anunciar y explicar estas propuestas a la comunidad, a fin de recoger feedback.
  3. Implementar las propuestas en base a los feedbacks recogidos.
  4. Recoger feedback sobre la implementación, dentro del ciclo de vida del desarrollo de Go.
  5. Lanzar la decisión en base al feedback de la implementación, al final del tercer mes del ciclo de vida, justo antes de la release.

Los criterios que se tomarán para las propuestas serán:

  1. atender una característica importante para mucha gente.
  2. tener el mínimo impacto para todo el mundo.
  3. venir con una solución clara y bien entendida.

Para más información: Go 2 here we come

domingo, 25 de noviembre de 2018

Rangos en Go

De forma natural, una colección de datos se recorre mediante un bucle, desde el primer elemento hasta el último. Para conseguir esto, normalmente utilizamos la función len() para obtener el tamaño o longitud de la colección, que nos sirve como límite en dicho bucle.

package main

import "fmt"

func main() {
   var nombres = [5]string{"Rafael", "Eduardo", 
      "Nerea", "Cintia", "Clemen"}

   for i := 0; i < len(nombres); i++ {
      fmt.Printf("Indice: %d, Nombre: %s\n", i, nombres[i])
   }
}

Este método de recorrer una colección se puede simplificar mucho mediante rangos. Un rango permite iterar o recorrer, de forma automática y secuencial, desde el inicio hasta el fin, una colección de datos.

Si usamos un rango sobre el array quedaría de la siguiente manera:

for indice, valor := range nombres {
   fmt.Printf("Indice: %d, Nombre: %s\n", indice, valor)
}

El rango retorna siempre dos resultados en cada iteración: el índice (o posición) y el valor del elemento actual en curso.

Si no nos interesa (por ejemplo) el índice, podemos omitirlo mediante el operador underscore (o guión bajo, _):

for _, valor := range nombres {
   fmt.Printf("Nombre: %s\n", valor)
}

Un rango puede trabajar con arrays, slices, strings y mapas. En el caso de los mapas, retornará la clave y el valor de cada elemento del mismo.

   // Definicion del mapa
   centros := make(map[string]string)

   // Adicion de claves y valores
   centros["MADE"] = "Madrid Este"
   centros["MADN"] = "Madrid Norte"
   centros["MOS"] = "Mostoles"
   centros["POZ"] = "Pozuelo"
   centros["BCN"] = "Barcelona"

   // Recorrido del mapa con un rango
   for clave, valor := range centros {
      fmt.Printf("Clave: %s, Nombre: %s\n", clave, valor)
   }

Nota: Los rangos también puede operar con canales, que es un concepto relacionado con la concurrencia.

Enlaces de interés

Funciones recursivas en Go

La recursividad es la habilidad de una función de llamarse a sí misma. Esta característica permite reducir y simplificar operaciones complejas, tales como una navegación jerárquica en un árbol o el cálculo de operaciones matemáticas como el factorial.

package main

import "fmt"

func main() {
   // factorial(5) = 5 * 4 * 3 * 2 * 1 = 120
   fmt.Println("Factorial de 5: ", factorial(5))
}

// Funcion que calcula un factorial
func factorial(n int) int {
   if n == 0 {
      return 1
   }

   return n * factorial(n-1)
}

Hay que tener en cuenta que cada vez que se llama a sí misma se crea un nuevo nivel de ejecución, en el que, hasta que el nivel más profundo no termina, no se irá resolviendo el retorno de las llamadas hacia atrás.

También hay que tener en cuenta que hay que controlar el nivel de profundidad de llamadas, para evitar un colapso en la memoria, al entrar en una recursividad infinita.

En el ejemplo del factorial, se va llamando a sí misma con el cálculo del factorial del número decrementado, hasta llegar a 1, momento en el que deja de llamarse a sí misma, y, por tanto, en ese momento termina la recursividad (y la profundidad de la misma) y comienza el retorno hacia atrás del resultado, disminuyendo nuevamente la profundidad de las llamadas hasta el primer nivel.

El siguiente ejemplo permite calcular la secuencia Fibonacci. Para limitar la recursividad, el factor de la secuencia debe ser mayor de 1.

package main

import "fmt"

func main() {
   // fibonacci(7) = 0 1 2 3 5 8 13
   fmt.Println("fibonacci(7): ", fibonacci(7))
}


// Funcion que calcula la secuencia fibonacci
func fibonacci(i int) int {
   if i <= 1 {
      return i
   }

   return fibonacci(i-1) + fibonacci(i-2)
}

Enlaces de interés

Funciones anónimas y closures en Go

Una función anónima es una función definida internamente dentro de un bloque de código, y que no tiene identificador o nombre. Este tipo de funciones no son reutilizables como paquetes, siendo utilizadas únicamente dentro del bloque de código en el que son declaradas.

El siguiente código declara una función anónima dentro de la función main(), la cual sólo tiene visibilidad dentro de esta función.

package main

import "fmt"

func main() {

   // Ejemplo basico de funcion anonima
   var num int = 0

   incrementar := func() int {
      num++
      return num
   }

   fmt.Println("Num: ", num)  // 0
   fmt.Println("Num: ", incrementar())  // 1
   fmt.Println("Num: ", incrementar())  // 2
}

closure

Un closure es una implementación de una función anónima como retorno de una función principal. Esta técnica es muy útil, ya que permite que la función principal pueda ser asignada o delegada a una "variable" que es en sí el closure. En ese momento, las variables de la función principal se registran o inicializan y el control pasa al closure.

Cuando se invoca al closure, internamente llama a la sentencia return, que es la que tiene la función anónima con el bloque de código de la operativa.

Para ver más claro esto, vamos a implementar el código anterior mediante un closure:

package main

import "fmt"

func main() {

  // Ejemplo basico de closure
   incr := increment()

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

}

// Funcion que retorna una funcion anonima
func increment() func() int {
   var num int = 0

   return func() int {
      num++
      return num
   }
}

También podemos utilizar parámetros en nuestros closures. Para ello, los parámetros se definen en la función de retorno.

package main

import "fmt"

func main() {

   // Ejemplo de closure con parametro
   acumulador := acumular()

   fmt.Println("acumulador() > ", acumulador(4))  // 4
   fmt.Println("acumulador() > ", acumulador(6))  // 10
   fmt.Println("acumulador() > ", acumulador(20)) // 30

}

// Funcion anonima con parametro
func acumular() func(incremento int) int {
   var num int = 0

   return func(incremento int) int {
      num += incremento
      return num
   }
}

El código anterior es muy básico, pero podemos implementar operativas más complejas, como la secuencia Fibonacci:

package main

import "fmt"

func main() {

   // Implementacion de la secuencia Fibonacci
   fmt.Println("Secuencia Fibonacci")

   proximaSecuencia := fibonacci()

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

 }

// Secuencia Fibonacci
func fibonacci() func() int {
   var num1, num2 int = 0, 1

   return func() int {
      var result int = num1 + num2
      num1 = num2
      num2 = result
      return result
   }
}

Enlaces de interés

domingo, 18 de noviembre de 2018

Conversión de datos en Go

Los datos son indispensables en nuestros programas. Gracias a las variables y a las constantes podemos almacenar y consultar datos. Pero nuestros programas también realizarán cálculos y transformaciones sobre los datos.

Conversión entre tipos numéricos

Vamos a probar el siguiente programa, el cual suma dos variables de tipo entero:

package main

import "fmt"

func main() {
   var a byte = 64
   var b int32 = 1567890

   fmt.Printf("%v + %v = %v\n", a, b, a+b)
}

A pesar de que las dos variables son de tipo entero, cada una de ellas es de un tipo específico y distinto. Por este motivo, al ejecutar este programa se producirá el siguiente error:

invalid operation: a + b (mismatched types byte and int32)

Tiene todo su sentido: un tipo byte es un entero que solo puede almacenar valores entre 0 y 255, mientras que el tipo int32 tiene un tamaño de cuatro bytes.

Para poder llevar a cabo esta suma, los dos valores deben ser del mismo tipo. Por tanto, habrá que convertir la variable con el tipo más pequeño al tipo más grande.

   fmt.Printf("%v + %v = %v\n", a, b, int32(a)+b)

Para hacer la conversión de un valor a un tipo concreto, basta con escribir el tipo a convertir, seguido de unos paréntesis, los cuales contendrán el valor a convertir.

tipo(valor)

En el siguiente ejemplo vamos a multiplicar un número de tipo byte con otro número de tipo float32. Primero se convertirá el tipo entero a tipo real y se multiplicarán como números reales. Después, se convertirá el tipo real a tipo entero y se multiplicarán como números enteros:

   var a byte = 64
   var pi float32 = 3.141519

   fmt.Printf("%v x %v = %v\n", a, pi, float32(a)*pi)
   fmt.Printf("%v x %v = %v\n", a, pi, a*byte(pi))

El resultado, obviamente, será distinto:

64 x 3.141519 = 201.05722
64 x 3.141519 = 192

Conversión a string

En muchas ocasiones necesitaremos utilizar un valor numérico o un valor lógico en formato de cadena de texto, o bien concatenar un número a una cadena, por lo que será necesario convertir a formato string.

En estos casos necesitaremos usar el paquete strconv, que contiene multitud de funciones que facilitan las conversiones entre diferentes tipos.

Las siguientes funciones retornan una cadena texto, proveniente de la conversión de un tipo numérico o de un tipo lógico:

strconv.FormatBool(valorBool)
strconv.FormatFloat(valorFloat64, 'base', decimales, tamaño)
strconv.FormatInt(valorInt, tamaño)
strconv.FormatUint(valorUint, tamaño)
strconv.Itoa(valorInt)

El siguiente ejemplo muestra la conversión de un tipo byte y un tipo float32 a formato de cadena de texto:

var a byte = 64
var pi float32 = 3.141519

var mensaje string = "El valor de PI es " +
   strconv.FormatFloat(float64(pi), 'f', 4, 64)

fmt.Println(mensaje)

mensaje = "El valor de a es " +
   strconv.Itoa(int(a))

fmt.Println(mensaje)

Estos métodos son muy efectivos, aunque pueden ser algo confusos y complejos de utilizar. Por este motivo, existe una forma más sencilla de realizar conversiones a cadenas de texto, a través del método Sprintf() del paquete fmt.

El método Sprintf retorna una cadena formateada en base a los parámetros que se faciliten. El primer parámetro es una cadena de texto con la plantilla a formatear. Los siguientes parámetros se corresponden a los valores a ser utilizados dentro de la plantilla.

En la plantilla se especifica texto con caracteres especiales, los cuales serán sustituidos por los valores contenidos en el resto de parámetros, en el mismo orden en que se especifiquen. Estos caracteres especiales permiten indicar el formato en que se mostrará el valor.

He aquí algunos de los caracteres más utilizados:

%v El valor en el formato por defecto
%T El nombre del tipo del valor
%t La palabra true o false
%d Base 10 (formato decimal, del 0 al 9)
%b Base 2 (formato binario, 0 ó 1)
%o Base 8 (formato octal, del 0 al 7)
%e Notación científica, como -1.234456e+78
%E Notación científica, como -1.234456E+78
%f Punto decimal sin exponente, como 123.456
%F Punto decimal sin exponente, como 123.456

Cuando trabajamos con números reales, podemos especificar también el ancho y la precisión del valor:

%f     Ancho y precisión por defecto
%9f    Ancho 9 caracteres, precisión por defecto
%.2f   Ancho por defecto, precisión de 2
%9.2f  Ancho 9 caracteres, precisión de 2
%9.f   Ancho 9 caracteres, precisión de 0

El ejemplo anterior se podría simplificar de esta manera:

mensaje = fmt.Sprintf("El valor de PI es %.4f", pi)
fmt.Println(mensaje)

mensaje = fmt.Sprintf("El valor de a es %d", a)
fmt.Println(mensaje)

Conversión de string a número

Con una cadena de texto no es posible realizar operaciones matemáticas, aunque dicha cadena contenga un número.

var num1, num2 string = "20", "32"
fmt.Println("num1 + num2 = ", num1+num2)

El ejemplo anterior dará como resultado la concatenación de dos cadenas de texto, no la suma de dos números:

num1 + num2 =  2032

Para obtener la suma de los números, será preciso convertir primero las cadenas en números para poder realizar las operaciones matemáticas. Pero es imprescindible que las cadenas contengan números válidos y que la conversión se realice al formato numérico válido correspondiente.

Para realizar estas conversiones, el paquete strconv dispone de varias funciones, dependiendo del tipo de dato a ser convertido:

varBool, err := strconv.ParseBool("false")
varFloat, err := strconv.ParseFloat("3.141519", 64)
varInt, err := strconv.ParseInt("-365", 10, 64)
varInt, err := strconv.Atoi("-365")
varUint, err := strconv.ParseUint("47", 10, 64)

Estas funciones retornan dos valores: el valor convertido al tipo correspondiente y un error, en caso de que éste se produzca.

El siguiente ejemplo convierte los valores de cadena a entero, y después realiza la suma de ambos números:

conv1, err := strconv.Atoi(num1)

if err != nil {
   fmt.Println("Error al convertir num1: ", err)
} else {
   conv2, err := strconv.Atoi(num2)

   if err != nil {
      fmt.Println("Error al convertir num2: ", err)
   } else {
      fmt.Println("conv1 + conv2 = ", conv1+conv2)
   }
}

En cada conversión, verificamos (a través de la sentencia if) si hubo algún error en la conversión. Si la variable err no contiene el valor nil (valor nulo), significa que se ha producido un error, y en ese caso informará del error producido.

Si no hubo error en ninguna de las dos conversiones, procederá a visualizar en pantalla la suma de los números obtenidos de la conversión de las dos cadenas de texto.

Enlaces de interés

miércoles, 14 de noviembre de 2018

Estructuras de control: bucles for

Un bucle permite ejecutar un bloque de código de forma repetida y controlada. Cada ejecución del código del bloque del bucle se denomina iteración.

Un bucle comienza con la sentencia for, y para controlar las iteraciones del bucle utilizamos una variable a modo de iterador, la cual se declara y se inicializa con un valor inicial. Después, se indica una condición que, mientras se cumpla, se ejecutará el bloque de código del bucle. Para que la variable cambie de valor en cada iteración, debe haber una asignación diferente, normalmente un incremento o un decremento.

Después de definir los parámetros de control del bucle, se especifica el código que ejecutará el bucle, encerrado entre llaves.

El siguiente ejemplo visualiza en pantalla una tabla de multiplicar, cuyo multiplicador va del 1 al 10:

  var tabla int = 2

  for i := 1; i <= 10; i++ {
    fmt.Printf("%d x %d = %d\n", tabla, i, tabla*i)
  }

La variable i es el iterador del bucle. El iterador va cambiando de valor en cada iteración, mientras se cumpla la condición. Cuando deje de cumplirse la condición, se saldrá del bucle. En este caso, el iterador comienza con el valor 1. En cada iteración, el iterador se incrementará. Mientras el iterador sea menor o igual que 10, se irá ejecutando. Pero cuando llegue al valor 11, saldrá del bucle.

2 x 1 = 2
2 x 2 = 4
2 x 3 = 6
2 x 4 = 8
2 x 5 = 10
2 x 6 = 12
2 x 7 = 14
2 x 8 = 16
2 x 9 = 18
2 x 10 = 20

El siguiente ejemplo realizará una cuenta atrás, desde 100 hasta 0, de dos en dos:

  for i := 100; i >= 0; i-=2 {
    fmt.Printf("%d, ", i)
  }

También podemos anidar bucles, es decir, definir y ejecutar un bucle dentro de otro. En el siguiente ejemplo, vamos a visualizar las tablas de multiplicar del 1 al 10, cada una de las tablas tendrá un multiplicador del 1 al 10:

  for x := 1; x <= 10; x++ {
    for i := 1; i <= 10; i++ {
      fmt.Printf("%d x %d = %d\n", x, i, x*i)
    }
  }

Bucles for Vs bucles while

Go no contempla bucles while como en otros lenguajes de programación. Sin embargo, es posible utilizar un bucle for de la misma manera a como se utiliza un bucle while

  var tabla int = 2
  var i int = 1

  for i <= 10 {  // while i <= 10
    fmt.Printf("%d x %d = %d\n", tabla, i, tabla*i)
    i++
  }

El iterador (variable i se declara e inicializa antes del bucle. En la declaración del bucle definimos únicamente la condición del bucle. Mientras (while) se cumpla la condición, el bloque de código se repetirá. Por último, dentro del bloque debe haber algo que cambie el valor del iterador, para poder llegar a incumplir la condición y salir del bucle. En este caso, es el incrementador (i++)

Sentencias break y continue

En los ejemplos vistos anteriormente, los bucles siguen una lógica lineal, es decir, que su ciclo de vida está ya definido y se puede predecir. Sin embargo, puede haber ocasiones en las cuales no sabremos cuándo hay que salir del bucle (puede ser en la primera iteración o en la número mil millones), o bien, podemos querer dejar de ejecutar el código del bucle y empezar una nueva iteración.

La sentencia break permite romper la iteración del bucle, saliendo directamente de él.

La sentencia continue deja de ejecutar el código del bucle, saltando directamente al inicio del bucle e iniciando una nueva iteración.

En el siguiente ejemplo vamos a crear un bucle infinito (no hay condición o la condición no se cumple nunca). Dentro de cada iteración va a solicitar al usuario que introduzca una palabra. Si la longitud de la palabra es menor de cinco caracteres se irá al inicio del bucle, omitiendo el resto del código del bucle e iniciando una nueva iteración. Si la longitud es mayor o igual de cinco caracteres, mostrará el mensaje "Tu palabra es: ", y repetirá la palabra introducida. Si la palabra introducida es "ok" (minúsculas) o "OK" (mayúsculas), saldrá del bucle.

  var palabra string

  fmt.Println("Escribe una palabra que la repito")
  fmt.Println("Para salir, escribe 'OK'")

  // Bucle infinito
  for {
    fmt.Print("Dame palabra > ")
    fmt.Scanln(&palabra)

    if palabra == "OK" || palabra == "ok" {
      break   // Sale del bucle
    } else if len(palabra) < 5 {
      continue // Inicio del bucle
    } else {
      fmt.Println("   Tu palabra es: ", palabra)
    }
  }

  fmt.Println("*** Fin del bucle ***")

El resultado es el siguiente:

Escribe una palabra que la repito
Para salir, escribe 'OK'
Dame palabra >  ratón
   Tu palabra es:  ratón
Dame palabra >  hola
Dame palabra >  vaso
Dame palabra >  mano
Dame palabra >  ordenador
   Tu palabra es:  ordenador
Dame palabra >  teléfono
   Tu palabra es:  teléfono
Dame palabra >  ok
*** Fin del bucle ***

Enlaces de interés

Estructuras de control en Go: bloques switch

En determinadas situaciones, será necesario evaluar múltiples condiciones para un mismo dato.

En el siguiente ejemplo muestra las etapas de la vida, en base a la edad:

  var edad int = 47

  if edad >= 0 && edad < 3 {
    fmt.Println("Primera infancia")
  } else if edad >= 3 && edad < 6 {
    fmt.Println("Niñez temprana")
  } else if edad >= 6 && edad < 12 {
    fmt.Println("Niñez intermedia")
  } else if edad >= 12 && edad < 18 {
    fmt.Println("Adolescencia")
  } else if edad >= 18 && edad < 35 {
    fmt.Println("Juventud")
  } else if edad >= 35 && edad < 50 {
    fmt.Println("Madurez")
  } else if edad >= 50 && edad < 65 {
    fmt.Println("Adultez")
  } else if edad >= 65 {
    fmt.Println("Vejez o Tercera Edad")
  }

Este código puede ser algo confuso, debido a tanto if, else y a las llaves de cada bloque de código

El siguiente código adapta los bloques if y else de antes, a un formato más legible y más sencillo de entender:

  switch {
    case edad >= 0 && edad < 3:
      fmt.Println("Primera infancia")
    case edad >= 3 && edad < 6:
      fmt.Println("Niñez temprana")
    case edad >= 6 && edad < 12:
      fmt.Println("Niñez intermedia")
    case edad >= 12 && edad < 18:
      fmt.Println("Adolescencia")
    case edad >= 18 && edad < 35:
      fmt.Println("Juventud")
    case edad >= 35 && edad < 50:
      fmt.Println("Madurez")
    case edad >= 50 && edad < 65:
      fmt.Println("Adultez")
    case edad >= 65:
      fmt.Println("Vejez o Tercera Edad")
  }

Go ofrece un tipo de bloque condicional, llamado switch, el cual permite evaluar diferentes casos (o condiciones), mediante la sentencia case. Cada caso evalúa una condición y, en el caso de ser cierta (true), abrirá un bloque de código que se ejecutará en tal caso. Dicho bloque no requiere el uso de las llaves.

Una de las ventajas del bloque switch es que podemos especificar al principio la variable a evaluar, y en cada case solamente indicamos el valor a evaluar.

El siguiente ejemplo evalúa los posibles valores de la variable region, a fin de poder determinar de qué región se trata.

  var region string = "EU"

  switch region {
    case "CH":   // region == "CH"
      fmt.Println("China")
    case "LAT":
      fmt.Println("Latino América")
    case "RU":
      fmt.Println("Rusia")
    case "US":
      fmt.Println("Estados Unidos")
    default:
      fmt.Println("Europa")
  }

En este tipo de evaluación, la condición será siempre de igualdad, es decir, que cada caso evaluará un valor concreto (variable == valor).

La cláusula default permite definir un caso por defecto, es decir, un caso que aplica cuando el valor de la variable no se corresponde con ninguno de los anteriores case.

Para evaluar varios valores en un mismo case, éstos se especifican separados por comas.

  var diaSemana int = 8

  switch diaSemana {
    case 1, 2, 3, 4, 5:
      fmt.Println("Es día laboral")
    case 6, 7:
      fmt.Println("Es fin de semana")
    default:
      fmt.Printf("Día %d no es válido", diaSemana)
  }

Enlaces de interés

Estructuras de control en Go: Bloques if y else

Una estructura de control permite la ejecución controlada de código (acciones) acordes a diferentes situaciones (o condiciones), las cuales son evaluadas por expresiones de comparación y/o expresiones lógicas o relacionales.

Un bloque es, básicamente, un conjunto de sentencias de código. A través de la sentencia if podemos definir un bloque de código que sólo se ejecutará en el caso de que se cumpla una determinada condición. Esto nos ayudará a crear programas más inteligentes, comportándose de formas distintas y controladas. Para ello, se evalúa una condición lógica, que es una comparación de datos, lo que dará un resultado de tipo bool. Si el resultado es true se ejecutará el código que se encuentra dentro del bloque, el cual está definido entre llaves ({ y }).

El siguiente ejemplo, evalúa la variable edad. Si el valor de la variable es mayor o igual a 18, significa que es mayor de edad.

var edad int = 18

if edad >= 18 {
   fmt.Println("Eres mayor de edad")
}

Si queremos realizar otra operación, en caso contrario, podemos añadir la cláusula else a la sentencia if.

var edad int = 18

if edad >= 18 {
   fmt.Println("Eres mayor de edad")
} else {
   fmt.Println("Eres menor de edad")
}

Podemos utilizar los operadores relaciones para evaluar más de una condición. En el siguiente ejemplo, evaluamos si la edad está entre 16 y 67 años, lo que significa que está en edad de trabajar:

if edad >=16 && edad <= 67 {
   fmt.Println("Estás en edad de trabajar")
}

Para evaluar condiciones más complejas o más específicas, se pueden enlazar más sentencias if en cláusulas else. En el siguiente ejemplo evaluamos tres posibles casos sobre la edad: ser aún demasiado joven para trabajar, estar en edad de trabajar o estar jubilado.

edad = 68

if edad >=16 && edad <= 67 {
  fmt.Println("Estás en edad de trabajar")
} else if edad < 16 {
  fmt.Println("Eres aún muy joven para trabajar")
} else {
  fmt.Println("Estás ya jubilado")
}

Enlaces de interés

lunes, 12 de noviembre de 2018

Mapas en Go

Un mapa es un tipo de colección dinámico que está referenciado por una clave, en lugar de por un índice. Su concepto es muy similar al de un diccionario en otros lenguajes de programación.

Hay dos formas de declarar y crear un mapa en Go:

var variableMapa map[tipoClave]tipoValor

variableMapa := make(map[tipoClave]tipoValor

Para añadir o modificar elementos del mapa, basta con indicar la clave y el valor para dicha clave. Si la clave no existe en el mapa, la añade; si ya existe, le cambia el valor.

variableMapa[clave] = valor

Para eliminar un elemento del mapa, se utiliza la función delete():

delete(variableMapa, clave)

Para saber si existe un elemento en el mapa, accederemos a dicho elemento, el cual nos devolverá dos valores: la propia clave y un valor boolean con el valor true si existe, o false si no existe:

var1, var2 = variableMapa[clave]
_, var = variableMapa[clave]

Nota: El uso del carácter subrayado (_) permite omitir un valor retornado, en este caso el nombre de la propia clave.

Si queremos recorrer uno a uno los elementos utilizaremos un bucle for alimentado por un rango mediante range, al cual indicaremos el mapa.

for varClave, varValor := range variableMapa {
   ...
}

Nota: El bloque for se repetirá por cada uno de los elementos del mapa. El propio bucle obtendrá, de cada elemento, dos variables: la clave y el valor. También podemos hacer uso del carácter subrayado (_) para omitir uno de estos valores.

Ejemplo

El siguiente ejemplo muestra los conceptos vistos anteriormente sobre los mapas en Go.

package main

import "fmt"

func main() {
   // Definicion del mapa
   centros := make(map[string]string)

   // Adicion de claves y valores
   centros["MADE"] = "Madrid Este"
   centros["MADN"] = "Madrid Norte"
   centros["MOS"] = "Mostoles"
   centros["POZ"] = "Pozuelo"
   centros["BCN"] = "Barcelona"

   // Visualizacion de propiedades y valores
   fmt.Println("Centros: ", centros)
   fmt.Println("Total: ", len(centros))
   fmt.Println("MOS: ", centros["MOS"])

   // Cambio de un elemento por su clave
   centros["POZ"] = "Pozoblanco"

   // Eliminacion de un elemento por su clave
   delete(centros, "MADN")

   // Visualizacion de propiedades y valores
   fmt.Println("Centros: ", centros)
   fmt.Println("Total: ", len(centros))

   // Verificar si existe (hay) valor para un elemento
   clave, existe := centros["MOS"]
   fmt.Printf("Clave: %s, existe: %v\n", clave, existe)
   _, existe = centros["ZZZ"]
   fmt.Printf("existe: %v\n", existe)

   // Recorrido del mapa
   for key, value := range centros {
      fmt.Printf("Key: %s, Value: %s\n", key, value)
   }
}

Nota: Los elemetos del mapa no están ordenados. Cuando se accede al mapa, estos elementos pueden estar en diferentes posiciones.

El resultado del programa será el siguiente:

$ go run maps.go
Centros:  map[MADE:Madrid Este MADN:Madrid Norte 
MOS:Mostoles POZ:Pozuelo BCN:Barcelona]
Total:  5
MOS:  Mostoles
Centros:  map[MOS:Mostoles POZ:Pozoblanco BCN:Barcelona 
MADE:Madrid Este]
Total:  4
Clave: Mostoles, existe: true
existe: false
Key: BCN, Value: Barcelona
Key: MADE, Value: Madrid Este
Key: MOS, Value: Mostoles
Key: POZ, Value: Pozoblanco

Enlaces de interés

domingo, 11 de noviembre de 2018

Go cumple nueve años

El 10 de Noviembre de 2009 nació Go, un lenguaje de programación nuevo creado por Google, diseñado por Ken Thompson (creador de Unix y el lenguaje B (precursor del lenguaje C)), Rob Pike (colaborador de Unix) y Robert Griesemer.

Durante estos nueve años, Go ha sido ampliamente adoptado por cada vez más empresas. De hecho, en la última encuesta de StackOverflow aparece entre las cinco primeras posiciones.

La comunidad ha crecido mucho, y se están celebrando cada vez más meetups a lo largo de todo el planeta, y aparece, cada día, nuevos blogs, tutoriales y libros.

También ha crecido, de forma constante, las contribuciones de la comunidad al lenguaje, a través de mejoras, fixes, proyectos y librerías:

Go ha ido madurando, y ya se perfila la versión 2 de Go, la cual traerá nuevas novedades, como mejoras en el tratamiento de módulos o de la gestión de errores.

Fuente: Golang Blog

jueves, 8 de noviembre de 2018

Operadores lógicos en Go

Los operadores lógicos o relacionales, permiten evaluar uno o dos valores, pero, a diferencia de los operadores de comparación, ni realiza una comparación ni evalúa valores numéricos o de cadena, si no valores exclusivamente lógicos.

La utilidad de estos operadores es evaluar condiciones complejas, en las que existen varias circunstancias, estableciendo una relación entre ellas.

Los operadores lógicos o relacionales en Go son los siguientes:

valor1 && valor2   AND/Y
valor2 || valor2   OR/O
!valor             NOT/NO (negación)

Operador && (AND/Y)

El operador && retornará un valor true si los dos valores lógicos comparados valen true.

true  && true   = true
true  && false  = false
false && true   = false
false && false  = false

Operador || (OR/O)

El operador || retornará un valor true si, al menos uno de los dos valores lógicos comparados vale true.

true  || true   = true
true  || false  = true
false || true   = true
false || false  = false

Operador ! (NOT/NO)

El operador ! retornará el valor contrario al evaluado (niega el valor).

!true  = false
!false = true

Precedencia de los operadores

Cuando se evalúa más de una relación, el orden de precedencia será de izquierda a derecha. Se evalúan los dos primeros valores, y el resultado de éste, se evaluará con el tercero, y así, sucesivamente, hasta el final.

true || false && false || true

// Paso a paso
[true || false] && false || true
true && false || true
[true && false] || true
false || true
true
Nota: Se ha utilizado los corchetes para resaltar qué operación se va a realizar.

Si se utiliza los paréntesis, las evaluaciones englobadas entre ellos se priorizarán, también de izquierda a derecha.

true || (false && false) || true || (true && false)

// Paso a paso
true || (false && false) || true || (true && false)
true || [(false && false)] || true || (true && false)
true || false || true || (true && false)
true || false || true || [(true && false)]
true || false || true || false
[true || false] || true || false
true || true || false
[true || true] || false
true || false
true

Ejemplo

package main

import "fmt"

func main() {
   var (
      edadRafael int = 47
      edadNerea  int = 16
      edadGoyi   int = 69
   )

   fmt.Println("Rafael en edad de trabajar: ",
      edadRafael >= 16 && edadRafael <= 67)
   fmt.Println("Goyi en edad de trabajar: ",
      edadGoyi >= 16 && edadGoyi <= 67)
   fmt.Println("Nerea en edad de trabajar: ",
      edadNerea >= 16 && edadNerea <= 67)

   fmt.Println("Rafael mayor de edad: ", !(edadRafael < 18))
   fmt.Println("Goyi mayor de edad: ", !(edadGoyi < 18))
   fmt.Println("Nerea mayor de edad: ", !(edadNerea < 18))

   fmt.Println("true || false && false || true - ",
      true || false && false || true)
   fmt.Println("true || (false&&false) ||true|| (true&&false) - ",
      true || (false && false) || true || (true && false))
}

El resultado será el siguiente:

$ go run oper-logicos.go
Rafael en edad de trabajar:  true
Goyi en edad de trabajar:  false
Nerea en edad de trabajar:  true
Rafael mayor de edad:  true
Goyi mayor de edad:  true
Nerea mayor de edad:  false
true || false && false || true -  true
true || (false && false) || true || (true && false) -  true

Enlaces de interés

Operadores de comparación en Go

Los operadores de comparación permiten evaluar una condición o circunstancia, comparando dos valores. El resultado de esta evaluación es un valor lógico (true o false).

Los operadores de comparación en Go son los siguientes:

valor1 == valor2  igual a 
valor1 != valor2  distinto de (no igual a)
valor1 <  valor2  menor que
valor1 <= valor2  menor o igual que
valor1 >  valor2  mayor que
valor1 >= valor2  mayor o igual que

Ejemplo

El siguiente ejemplo ilustra todos estos operadores comparando la edad de una persona con el valor 18.

package main

import "fmt"

func main() {
   var edad int = 18

   fmt.Println("edad == 18 - ", edad == 18)
   fmt.Println("edad != 18 - ", edad != 18)
   fmt.Println("edad <  18 - ", edad < 18)
   fmt.Println("edad <= 18 - ", edad <= 18)
   fmt.Println("edad >  18 - ", edad > 18)
   fmt.Println("edad >= 18 - ", edad >= 18)
}

No hay que confundir los operadores de asignación (= y :=) con el operador de comparación de igualdad (==).

El resultado será el siguiente:

$ go run oper-comparacion.go
edad == 18 -  true
edad != 18 -  false
edad <  18 -  false
edad <= 18 -  true
edad >  18 -  false
edad >= 18 -  true

Enlaces de interés

martes, 6 de noviembre de 2018

Operadores de concatenación en Go

La concatenación es una operación de unión de varios strings o cadenas de texto.

Existen dos operadores de concatenación en Go:

string1 + string2   // Concatenación
variable += string  // Concatenación y asignación

He aquí un ejemplo:

package main

import "fmt"

func main() {
   var cad1 string = "En un lugar de la Mancha, "
   var cad2 string = "de cuyo nombre no quiero acordarme"

   frase := cad1 + cad2

   fmt.Println(frase)

   saludo := "Hola, "
   saludo += "amigo"

   fmt.Println(saludo)
}

El resultado es el siguiente:

$ go run concatenacion.go
En un lugar de la Mancha, de cuyo nombre no quiero acordarme
Hola, amigo

Enlaces de interés

Operadores de asignación, asignación aritmética e incremento en Go

Operadores de asignación

Un operador de asignación realiza la operación de asignar un valor a una variable o a una constante. Su sintaxis es la siguiente:

variable/constante operador valor

Mediante el operador = igualamos (o asignamos) la variable o constante con el valor especificado a continuación.

var op1 int
op1 = 1

El operador de asignación = sólo permite asignar valores a variables o constantes que ya han sido declaradas previamente especificando su tipo, tal y como se representa en el ejemplo anterior. El valor a asignar debe ser del mismo tipo que el de la variable declarada.

Hay otro operador de asignación en Go, que permite asignar, por inferencia, un valor a una variable que se está declarando o creando en ese mismo momento, sin especificar su tipo.

pi := 3.14159

En este ejemplo declaramos la variable pi y le asignamos el valor 3.14159. El compilador de Go detecta automáticamente el tipo del valor que se asigna, e internamente, crea la variable pi de tipo float64 para que tenga el mismo tipo que el dato. El compilador de Go, internamente realizaría la siguiente declaración y asignación:

var pi float64 = 3.14159

Operadores de incremento y decremento

Go posee dos operadores que permiten incrementar o decrementar el valor de una variable:

variable++
variable--

Estos operadores necesitan únicamente un operando, que sería la variable sobre la cual incrementar o decrementar en uno su valor.

Operadores de asignación aritmética

Un operador de asignación aritmética permite, en un único paso, realizar una operación aritmética sobre una variable y guardar el resultado en dicha variable.

variable operador valor

En la siguiente tabla se muestra la equivalencia de estos operadores:

variable += valor  -> variable = variable + valor
variable -= valor  -> variable = variable - valor
variable *= valor  -> variable = variable * valor
variable /= valor  -> variable = variable / valor
variable %= valor  -> variable = variable % valor

He aquí un ejemplo:

var op1 int = 3
var op2 int = 2
op1 *= op2   // op1 = op1 * op2 -> 6

Ejemplo

package main

import "fmt"

func main() {
   // Asignacion normal
   var op1 int
   op1 = 1

   // Asignacion por inferencia
   pi := 3.14159
   fmt.Printf("pi: %T %v\n", pi, pi)

   // Incremento
   op1++    // op1=op1+1 >> 2

   // Asignacion aritmetica
   op1 += 4 // op1=op1+4 >> 6
   op1 -= 2 // op1=op1-2 >> 4
   op1 *= 6 // op1=op1*6 >> 24
   op1 /= 2 // op1=op1/2 >> 12

   // Decremento
   op1--    // op1=op1-1 >> 11
   fmt.Println("op1 = ", op1)

   // Aritmetica con una constante
   const DUP int = 2
   op1 *= DUP  // op1=op1*DUP >> 22

   fmt.Println("op1 = ", op1)
}

El resultado es el siguiente:

pi: float64 3.14159
op1 =  11
op1 =  22

Enlaces de interés

Operadores aritméticos y expresiones en Go

Un operador permite realizar una operación (de ahí su nombre) sobre uno o varios valores (operandos), que pueden ser datos directos, variables o constantes.

Operadores aritméticos

Los operadores artiméticos en Go son:

+  Suma
-  Resta
*  Multiplicación
/  División
%  Resto (de la división)

Estos operadores permiten realizar operaciones aritméticas con valores numéricos.

Expresiones

Una expresión se refiere a la transcripción de una o varias operaciones mediante código, utilizando para ello operadores, variables, constantes, valores, etc.

Para que un operador aritmético pueda desempeñar su función, es necesario expresar la operación de la siguiente manera:

operando1 operador operando2

Los operandos son los valores que intervienen en la operación. Pueden ser valores directos, variables o constantes.

Ejemplo simple

El siguiente ejemplo muestra el uso de los operadores aritméticos y de las expresiones, las cuales realizarán el calculo determinado por el operador, y el resultado se asignará a una variable.

package main

import "fmt"

func main() {
   var op1, op2 int = 6, 4
   var suma int = op1 + op2
   var resta int = op1 - op2
   var multiplicacion int = op1 * op2
   var division int = op1 / op2
   var resto int = op1 % op2

   fmt.Printf("%v + %v = %v\n", op1, op2, suma)
   fmt.Printf("%v - %v = %v\n", op1, op2, resta)
   fmt.Printf("%v * %v = %v\n", op1, op2, multiplicacion)
   fmt.Printf("%v / %v = %v\n", op1, op2, division)
   fmt.Printf("%v resto %v = %v\n", op1, op2, resto)
}

El resultado será el siguiente:

$ go operadores-aritmeticos.go
6 + 4 = 10
6 - 4 = 2
6 * 4 = 24
6 / 4 = 1
6 resto 4 = 2

Precedencia de los operadores

Cuando escribimos expresiones más complejas, donde se desarrollan varias operaciones, el orden de precedencia será siempre de izquierda a derecha.

fmt.Println("4 + 3 - 1 = ", 4+3-1)

En este caso, primero se sumarán 4 y 3 (7), y después se restará 1, dando como resultado el valor 6.

Sin embargo, según el operador, la precedencia cambia. Por ejemplo, el siguiente ejemplo:

fmt.Println("4 + 2 * 6 - 8 / 2 + 4 % 2 = ", 4+2*6-8/2+4%2)

daría como resultado 12

En esta expresión tan larga, primero se calcularían las expresiones con el operador de mayor peso, y después, con los resultados, se calcularían las expresiones de menor peso, siempre de izquierda a derecha.

Los operadores de mayor peso son la multiplicación, la división y el resto. Los operadores de menor peso serían la suma y la resta.

Por tanto, en el ejemplo anterior se calcularían primero, de izquierda a derecha, las operaciones de mayor peso. Una vez calculadas, de izquierda a derecha, se calcularían las operaciones de menor peso.

4 + [2 * 6] - [8 / 2] + [4 % 2]
4 + 12 - 4 + 0
12

Nota: Se han enmarcado entre corchetes las operaciones de mayor peso para identificar claramente cómo se realizan los cálculos

La precedencia de los paréntesis

Los paréntesis permiten cambiar la precedencia de los operadores, obteniendo el mayor peso en la precedencia. Por tanto, primero se calculan los paréntesis, después la multiplicación, división o resto, y, por último, la suma o la resta.

fmt.Println("(4 + 2) * (6 - 8) / 2 + 4 = ", (4+2)*(6-8)/2+4)

El resultado de la expresión anterior será -2. El orden de las operaciones es el siguiente:

(4 + 2) * (6 - 8) / 2 + 4
(6) * (-2) / 2 + 4
-12 / 2 + 4
-6 + 4
-2

El paréntesis no tiene por qué englobar sólo una operación. Puede tener las operaciones que sean necesarias, e incluso englobar entre paréntesis operaciones dentro de otra operación englobada por paréntesis. Aquí el orden de precedencia serían desde los paréntesis más profundos hasta los más externos. Veamos un ejemplo:

fmt.Println("(4 + 2) * ((6 - 8 / 2) + 4) = ", (4+2)*((6-8/2)+4))

El orden de las operaciones es el siguiente:

(4 + 2) * ((6 - 8 / 2) + 4)
(4 + 2) * ((6 - [8 / 2]) + 4)
(4 + 2) * ((6 - 4) + 4)
(4 + 2) * (2 + 4)
(6) * (6)
36

Enlaces de interés

Slices en Go

Un slice ("rebanada" en español), viene a ser un segmento o una porción de un array. En la práctica, un slice nos ofrece nuevas posibilidades para tratar con colecciones de datos.

Asignando rebanadas

En el siguiente ejemplo vamos a crear un array con una serie de elementos, y, a continuación, vamos a extraer una porción o rebanada de este array y se la asignaremos a un slice, el cual crearemos por inferencia.

package main

import "fmt"

func main() {
   // Creacion de un slice por inferencia
   // a partir de un array
   miArray := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
   miSlice := miArray[3:7]

   fmt.Printf("miArray: %T %v - len:%v - cap:%v\n",
      miArray, miArray, len(miArray), cap(miArray))

   fmt.Printf("miSlice: %T %v - len:%v - cap:%v\n",
      miSlice, miSlice, len(miSlice), cap(miSlice))
}

Aparentemente, el slice podría parecer un array normal con la copia del fragmento extraido del array. Pero cuando ejecutamos el código, veremos algunas diferencias:

$ go run slices.go
miArray: [10]int [0 1 2 3 4 5 6 7 8 9] - len:10 - cap:10
miSlice: []int [3 4 5 6] - len:4 - cap:7
  • El slice no tiene un tamaño definido ([]int)
  • La capacidad del slice es mayor que la longitud del mismo: se han asignado cuatro elementos (longitud), pero su capacidad es de 7.

Esto significa que el slice puede redimensionarse dinámicamente, es decir, que puede variar de tamaño. Esto supone una ventaja muy grande con respecto a los arrays, que son estáticos y no pueden variar de tamaño.

Creación de un slice

Un slice se puede crear de varias maneras:

// Creacion de un slice por inferencia
// a partir de un array
miArray := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
miSlice := miArray[3:7]

// Declaracion de un slice y posterior asignacion
// Tendra longitud 3 y capacidad 3
var miSlice2 []int
miSlice2 = []int{1, 2, 3}

// *** FORMA RECOMENDADA ***
// Creacion de un slice con longitud 3 y capacidad 3
miSlice3 := make([]byte, 3)

// Creacion de un slice con longitud 3 y capacidad 6
miSlice4 := make([]float64, 3, 6)

A la hora de crear un slice es recomendable utilizar la función make(), ya que aporta una mejor legibilidad en el código, y porque permite controlar tanto su tamaño como su capacidad.

Asignación de datos a un slice

Existen varias maneras de asignar datos a un slice:

// Extrayendo una porcion a partir de un array
miSlice = miArray[3:7]

// Asignando un slice ad-hoc
miSlice2 = []int{1, 2, 3}

Agregando nuevos valores al slice

Un slice puede crecer dinámicamente, agregando valores según sean necesarios. Esto se consigue mediante la función append()

miSlice5 := []float64{12.2, 15.5, 18.9}

// Agregacion de nuevos valores
miSlice5 = append(miSlice5, 21.4, 23.7, 7.5)

// Ahora miSlice5 contiene [12.2 15.5 18.9 21.4 23.7 7.5]

Copiando valores a un slice

Existen diferentes maneras para copiar valores a un slice:

// Copia los elementos de un array
miSlice := miArray[:]

// Copia entre slices, mediante asignacion
miSlice2 = miSlice

// Metodo recomendado para copiar entre slices
miSlice5 := []float64{12.2, 15.5, 18.9}
miSlice7 := make([]float64, 3)
copy(miSlice7, miSlice5)

La función copy() es la forma recomendada para copiar todos los valores de un slice a otro.

Nota importante: Los datos a copiar deben ser del mismo tipo que el tipo del slice

Eliminando elementos de un slice

miSlice8 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// Eliminar los dos primeros elementos
// 3, 4, 5, 6, 7, 8, 9, 10
miSlice8 = miSlice8[2:]

// Eliminar los dos ultimos elementos
// 3, 4, 5, 6, 7, 8
miSlice8 = miSlice8[:6]

Unión de varios slices

La sintaxis de la función append() es la siguiente:

append(slice, valor1, valor2, valor_n)

Sin intentamos unir dos slices dará un error, pues a partir del segundo parámetro espera un valor del mismo tipo que el slice, no un tipo slice. Es decir, espera una lista de elementos y no un slice.

Para añadir un slice mediante la función append(), debemos utilizar la siguiente sintaxis:

append(slice1, slice2...)

Los puntos suspensivos después del nombre del slice, indican a Go que descomponga dicho slice por la lista de elementos que contiene.

Veamos un ejemplo:

// Adicion de varios slices
slice1 := []string{"Rafael", "Eduardo"}
slice2 := []string{"Cristina", "Antonio"}
slice3 := []string{"Lucía", "José", "Víctor", "Ana"}
slice3 = append(slice3, slice1...)
slice3 = append(slice3, slice2...)
fmt.Println("slice3: ", slice3)

El resultado será el siguiente:

slice3:  [Lucía José Víctor Ana Rafael Eduardo Cristina Antonio]

Slices como referencias a un array

Cuando trabajamos con slices, en realidad estamos trabajando con una referencia a un array. Por tanto, el slice esta apuntando a la dirección de memoria del array que contiene los datos, y, por tanto, los cambios en algún elemento afectarán a ambos.

En el siguiente ejemplo se crea un array con una lista de nombres. A continuación, se crean dos slices, cada uno de los cuales referencia a una porción del array. Después, se cambia el elemento "Cristina" por "Nerea" en el segundo slice. Al ser una referencia, en realidad se está modificando en el array, por lo que el elemento cambia tanto para el array como para los slices.

arr := [5]string{"Rafael", "Edu", "Cristina", "Antonio", "Lucía"}

// ["Rafael", "Edu", "Nerea"]
sl1 := arr[:3]

// ["Nerea", "Antonio", "Lucía"]
sl2 := arr[2:]

fmt.Printf("arr: %v\n", arr)
fmt.Printf("sl1: %v\n", sl1)
fmt.Printf("sl2: %v\n", sl2)

// Cambiar "Cristina" por "Nerea"
sl2[0] = "Nerea"

fmt.Printf("arr: %v\n", arr)
fmt.Printf("sl1: %v\n", sl1)
fmt.Printf("sl2: %v\n", sl2)

El resultado será el siguiente:

arr: [Rafael Edu Cristina Antonio Lucía]
sl1: [Rafael Edu Cristina]
sl2: [Cristina Antonio Lucía]
arr: [Rafael Edu Nerea Antonio Lucía]
sl1: [Rafael Edu Nerea]
sl2: [Nerea Antonio Lucía]

Enlaces de interés

lunes, 5 de noviembre de 2018

Concurrencia y paralelismo en Go: conceptos

Go puede realizar varias cosas al mismo tiempo. Esto significa que podemos lanzar un proceso y, mientras éste se está ejecutando, realizar otras cosas, sin tener que esperar a que el proceso termine. Esto permite optimizar mucho los tiempos de ejecución y el rendimiento de nuestras aplicaciones.

Concurrencia

Cuando el trabajo lo realiza una única CPU, podemos dividir el tiempo de procesamiento de esta CPU de forma que los diferentes procesos parezcan que se ejecutan de forma concurrente, cuando en realidad está haciendo una cosa cada vez. En este caso, la CPU está priorizando tiempos de ejecución de cada proceso o tarea, intercalando una fracción mínima de tiempo a cada proceso. La palabra clave en este caso, es la coordinación de los tiempos de ejecución de cada tarea.

Paralelismo

Los ordenadores de hoy en día tienen procesadores multi-core, por lo que disponen de varias CPUs (Unidades Centrales de Procesamiento), capaces de ejecutar procesos diferentes en paralelo y al mismo tiempo.

Diferencia entre concurrencia y paralelismo

La principal diferencia entre concurrencia y paralelismo es que la concurrencia está sincronizando o coordinando varias cosas a la vez, mientras que el paralelismo está haciendo varias cosas a la vez.

Hilos de ejecución

Todo programa comienza con un hilo de ejecución (hilo principal), y cuando este hilo principal acaba, el programa finaliza. El hilo ejecuta un proceso, el cual contiene código y datos en la memoria.

Dentro de un proceso podemos abrir nuevos hilos de ejecución, cada uno de los cuales ejecutará un proceso (o fragmento de código). Cada hilo tiene acceso a una zona de memoria determinada, y las variables creadas por cada hilo son exclusivas (no se comparten) para dicho proceso a través de un stack o pila. Por otra parte, también disponemos de un heap (o montón), que es una zona compartida de memoria que podemos compartir datos entre procesos.

Una aplicación con múltiples hilos de ejecución (multi-thread) permite ejecutar procesos en serie (concurrencia) o en paralelo.

gorutinas

Una gorutina (goroutine), es una función que se ejecuta de forma concurrente o en paralelo. A diferencia de un hilo estándar (o thread), una gorutina posee las siguientes características:

  • Es creada, ejecutada, sincronizada y destruida automáticamente por Go a través de su runtime (o controlador de ejecución).
  • Es gestionada por Go, no por el sistema.

  • No tiene dependencias del hardware.

  • Puede dimensionar dinámicamente el tamaño de su stack para almacenar datos en tiempo de ejecución.
  • Puede usar canales de comunicación con otras gorutinas con latencia baja.
  • Puede trabajar de forma sincronizada y colaborativa con otras gorutinas.

Básicamente, una gorutina puede ser cualquier función que definamos en nuestras aplicaciones. La diferencia radica en la forma en que se invoca o ejecuta, a través de la palabra clave go, la cual permite al controlador de ejecución de Go tomar el control de dicha función para que se ejecute de forma asíncrona.

go función(argumentos)

Ejemplo

El siguiente código de ejemplo ilustra, de forma sencilla, cómo funciona la ejecución concurrente (o asíncrona) mediante una gorutina.

package main

import (
   "fmt"
   "time"
)

func main() {
   // Ejecucion sincrona
   repetir(3, "Síncrono inicio")

   // Ejecucion asincrona
   go repetir(5, "Asíncrono")

   // Nueva ejecucion sincrona
   repetir(3, "Síncrono final")

   // Esperar a que el usuario pulse tecla Enter
   fmt.Scanln()
   fmt.Println("Fin del programa")
}

func repetir(veces int, texto string) {
   for i := 1; i <= veces; i++ {
      fmt.Printf("#%v > %v\n", i, texto)
      time.Sleep(10 * time.Millisecond)
   }
}

Hemos definido una función llamada repetir(), la cual se encarga de imprimir n veces un texto pasado como argumento. El bucle for se encargará de este trabajo, y entre cada iteración o impresión, realizamos un retardo de diez milisegundos. Ello se consigue mediante el paquete time, el cual incluye funciones muy útiles relacionadas con la gestión del tiempo. Entre ellas, encontramos la función Sleep(), la cual permite esperar el tiempo que le indiquemos.

Dentro de la función main(), invocamos a esta función de forma normal (síncrona). Esto es, se ejecuta paso a paso hasta que termine. Por este motivo, hasta que no termine su ejecución no continuará su ejecución.

Después, se invocará nuevamente mediante la palabra clave go, con lo cual, el controlador de ejecución de Go tomará esta ejecución como un hilo concurrente y lo ejecutará de forma asíncrona. Es decir, que este proceso se ejecutará en segundo plano sin esperar a que termine la ejecución, continuando la ejecución la siguiente línea de código de la función main().

A continuación se ejecutará nuevamente de forma síncrona (normal), pero, al mismo tiempo, el proceso anterior se seguirá ejecutando.

Una vez terminada la ejecución anterior (es decir, se ha impreso 3 veces el texto "Síncrono final), esperamos a que el usuario pulse la tecla Enter, mediante la función Scanln del paquete de entrada y salida fmt. Después, imprime el mensaje "Fin del programa" y finaliza la ejecución del hilo principal (función main().

El resultado será el siguiente:

$ go run concurrency-concept.go

#1 > Síncrono inicio
#2 > Síncrono inicio
#3 > Síncrono inicio
#1 > Síncrono final
#1 > Asíncrono
#2 > Asíncrono
#2 > Síncrono final
#3 > Síncrono final
#3 > Asíncrono
#4 > Asíncrono
#5 > Asíncrono

Fin del programa

Ejemplo 2

Para el siguiente ejemplo, simplemente comentaremos la línea en que el programa espera a que el usuario pulse la tecla Enter:

   // fmt.Scanln()

Ahora el resultado será el siguiente:

$ go run concurrency-concept.go
#1 > Síncrono inicio
#2 > Síncrono inicio
#3 > Síncrono inicio
#1 > Síncrono final
#1 > Asíncrono
#2 > Asíncrono
#2 > Síncrono final
#3 > Asíncrono
#3 > Síncrono final
Fin del programa

Aquí se puede apreciar que, una vez ha terminado la ejecución síncrona del texto "Síncrono final", se ha lanzado la impresión de "Fin del programa" y el programa finaliza, mientras aún quedaban iteraciones de la gorutina. El proceso main() es el hilo principal, el cual lanzó un hilo concurrente que aún no terminó cuando llegó al final de la ejecución. Por tanto, el hilo principal es el que manda, y, una vez finaliza éste, se corta y finalizan todos los sub-hilos en curso.

Enlaces de interés