1. Code
  2. Coding Fundamentals

3 Cosas Que Hacen Diferente a Go

Scroll to top

Spanish (Español) translation by Rafael Chavarría (you can also view the original English article)

Go es un lenguaje especial. Es muy refrescante en su aproximación a la programación y los principios que promueve. Ayuda que algunos de los inventores fueran pioneros de C. La sensación en general de Go es C del siglo XXI. 

En este tutorial, aprenderás sobre tres de las características que hacen único a Go: su simplicidad, el modelo de concurrencia vía gorutines y el mecanismo de manejo de errores.

1. Simplicidad

Muchos lenguajes modernos exitosos como Scala y Rust son muy ricos y proporcionan tipos de sistemas avanzados y sistemas de administración de memoria. Esos lenguajes tomaron los lenguajes principales de su tiempo como C++, Java y C# y agregaron o mejoraron capacidades. Go tomó una ruta diferente y eliminó muchas características y capacidades.

Sin Genéricos

Los genéricos o plantillas son un pilar de muchos lenguajes de programación. Estos frecuentemente agregan complejidad, y los mensajes de error asociados con genéricos pueden ser oscuros algunas veces. Los diseñadores de Go decidieron solo omitirlo.

Esta es discutiblemente la decisión de diseño más controversial de Go. Personalmente, encuentro mucho valor en los genéricos y creo que puede hacerse adecuadamente (mira C# para un ejemplo grandioso de genéricos bien ejecutados). Ojalá, los genéricos serán incorporados a Go en el futuro.

Sin Excepciones

El manejo de errores de Golang depende de códigos explícitos de estado. Para separar el estado del resultado real de una función, Go soporta múltiples valores de retorno de una función. Esto es bastante inusual. Lo cubriré a mucho más detalle después, pero aquí está un ejemplo rápido:

1
package main
2
3
import (
4
  "fmt"
5
  "errors"
6
)
7
8
func div(a, b float64) (float64, error) {
9
  if b == 0 {
10
    return 0, errors.New(fmt.Sprintf("Can't divide %f by zero", a))
11
  }
12
  return a / b, nil  
13
}
14
15
16
func main() {
17
  result, err := div(8, 4)
18
  if err != nil {
19
    fmt.Println("Oh-oh, something went wrong. " + err.Error())
20
  } else {
21
    fmt.Println(result)
22
  }
23
  
24
  result, err = div(5, 0)
25
  if err != nil {
26
    fmt.Println("Oh-oh, something iswrong. "+err.Error())
27
  } else {
28
    fmt.Println(result)
29
  }
30
}
31
32
2
33
Oh-oh, something is wrong. Can't divide 5.000000 by zero

Ejecutable Simple

Go no tiene librería de tiempo de ejecución separada. Este genera un solo ejecutable, el cuál puedes desplegar solo copiando (a.k.a despliegue XCOPY). Esto es tan sencillo como suena. No hay necesidad de preocuparse por desajustes de dependencias o versiones. También es una gran bendición para despliegues basados en contenedor (Docker, Kubernetes y amigos). El ejecutable independiente hace Dockerfiles muy sencillo.

Sin Librerías Dinámicas

OK. Esto apenas cambió recientemente en Go 1.8. Ahora puedes realmente cargar librerías dinámicas a través del paquete de complemento. Pero, ya que esta capacidad no fue introducida desde el inicio, aun lo considero una extensión para situaciones especiales. El espíritu de Go es aún un ejecutable compilado de manera estática. También está disponible en Linux solamente.

2. Goroutines

Las Goroutines son probablemente el aspecto más atractivo de Go desde un punto de vista práctico. Las Goroutines te permiten aprovechar el poder de máquinas multi-núcleo de una manera muy amigable con el usuario. Está basado en fundamentos teóricos sólidos, y la sintaxis para soportarlo es bastante placentera.

CSP

El cimiento del modelo de concurrencia de Go es C.A.R. Los Procesos Secuenciales de Comunicación de Hoare. La idea es evitar sincronización sobre memoria compartida entre múltiples hilos de ejecución, lo que es propenso a errores y de labor intensiva. En su lugar, comunica a través de canales que evitan contención.

Invoca una Función como una Goroutine

Cualquier función puede ser invocada como una goroutine llamándola vía la palabra clave go. Considera primero el siguiente programa lineal. La función foo() duerme por varios segundos e imprime cuántos segundos durmió. En esta versión, cada llamada a foo() bloquea antes de la siguiente llamada.

1
package main
2
3
import (
4
  "fmt"
5
  "time"
6
)
7
8
func foo(d time.Duration) {
9
  d *= 1000000000
10
  time.Sleep(d)
11
  fmt.Println(d)
12
}
13
14
15
func main() {
16
  foo(3)
17
  foo(2)
18
  foo(1)
19
  foo(4)
20
}

La salida sigue el orden de llamadas en el código:

1
3s
2
2s
3
1s
4
4s

Ahora, haré un ligero cambio y agregaré la palabra clave "go" antes de las primeras tres invocaciones:

1
package main
2
3
import (
4
  "fmt"
5
  //"errors"
6
  "time"
7
)
8
9
func foo(d time.Duration) {
10
  d *= 1000000000
11
  time.Sleep(d)
12
  fmt.Println(d)
13
}
14
15
16
func main() {
17
  go foo(3)
18
  go foo(2)
19
  go foo(1)
20
  foo(4)
21
}

La salida es diferente ahora. La llamada de 1 segundo terminó primero e imprimió "1s", seguido por "2s" y "3s".

1
1s
2
2s
3
3s
4
4s

Nota que la llamada del segundo 4 no es una goroutine. Esto es por diseño, así que el programa espera y deja terminar a las goroutines. Sin esto, el programa completará automáticamente después de lanzar las goroutines. Hay varias maneras además de dormir para esperar que una goroutine termine.

Sincroniza Goroutines

Otra manera de esperar que terminen las goroutines es usar grupos sincronizados. Declaras un objeto de grupo de espera y lo pasas a cada goroutine, que es responsable de llamar su método Done() cuando está terminado. Después, esperas al grupo sincronizado. Aquí está el código que adapta el ejemplo anterior para usar un grupo de espera.

1
package main
2
3
import (
4
  "fmt"
5
  "sync"
6
  "time"
7
)
8
9
func foo(d time.Duration, wg *sync.WaitGroup) {
10
  d *= 1000000000
11
  time.Sleep(d)
12
  fmt.Println(d)
13
  wg.Done()
14
}
15
16
func main() {
17
  var wg sync.WaitGroup
18
  wg.Add(3)
19
  go foo(3, &wg)
20
  go foo(2, &wg)
21
  go foo(1, &wg)
22
  wg.Wait()
23
}

Canales

Los canales permiten a las goroutines (y a tu programa principal) intercambiar información. Puedes crear un canal y pasarlo a una goroutine. El creador puede escribir al canal, y la goroutine puede leer desde el canal.

La dirección opuesta funciona también. Go también proporciona dulce sintaxis para canales con flechas para indicar el flujo de la información. Aquí está otra adaptación de nuestro programa, en el cuál las goroutines reciben un canal al que escriben cuando terminan, y el programa principal espera a recibir mensajes de todas las goroutines antes de terminar.

1
package main
2
3
import (
4
  "fmt"
5
  "time"
6
)
7
8
func foo(d time.Duration, c chan int) {
9
  d *= 1000000000
10
  time.Sleep(d)
11
  fmt.Println(d)
12
  c <- 1
13
}
14
15
func main() {
16
  c := make(chan int)
17
  go foo(3, c)
18
  go foo(2, c)
19
  go foo(1, c)
20
  <- c
21
  <- c
22
  <- c
23
}

Escribe una Goroutine

Eso es una especie de truco. Escribir una goroutine es lo mismo que escribir cualquier función. Revisa la función foo() de arriba, la cuál es llamada en el mismo programa como una goroutine así como también como una función regular.

3. Manejo de Errores

Como mencioné anteriormente, el manejo de errores de Go es diferente. Las funciones puedes devolver múltiples valores, y por convención las funciones que pueden fallar devuelven un objeto de error como su último valor de retorno.

También hay  un mecanismo que se parece a las excepciones vía las funciones panic() y recover(),  pero es más adecuado para situaciones especiales. Aquí está un escenario típico de manejo de errores en donde la función bar() devuelve un error, y la función main() revisa si hubo un error y lo imprime.

1
package main
2
3
import (
4
  "fmt"
5
  "errors"
6
)
7
8
func bar() error {
9
  return errors.New("something is wrong")
10
11
}
12
13
func main() {
14
  e := bar()
15
  if e != nil {
16
    fmt.Println(e.Error())
17
  }
18
}

Revisión Obligatoria

Si asignas el valor a una variable y no la revisas, Go se molestará.

1
func main() {
2
  e := bar()
3
}
4
5
main.go:15: e declared and not used

Hay maneras de solucionarlo. Puedes solo no asignar el valor en absoluto:

1
func main() {
2
  bar()
3
}

O puedes asignarlo al guión bajo:

1
func main() {
2
  _ = bar()
3
}

Soporte de Lenguaje

Los errores son solo valores que puedes pasar libremente. Go proporciona soporte de pequeño error declarando la interfaz de error que solo requiere un método llamado Error() que devuelve una cadena:

1
type error interface {
2
    Error() string
3
}

También está el paquete errors que te permite crear nuevos objetos de error. El paquete fmt proporciona una función Errorf() para crear objetos formateados de error. Eso es todo.

Interacción con Goroutines

No puedes devolver errores (o cualquier otro objeto) desde una goroutine. Las Goroutines pueden comunicar errores al mundo exterior a través de algún otro medio. Pasar un canal de error a una goroutine es considerado una buena práctica. Las Goroutines también pueden escribir errores a archivos de registro o la basa de datos o llamar servicios remotos.

Conclusión

Go ha visto tremendo éxito y momento en los últimos años. Es el lenguaje al que recurrir para sistemas distribuidos modernos y bases de datos. Convirtió a muchos desarrolladores Python.

Una gran parte de esto es sin duda debido al respaldo de Google. Pero Go definitivamente se sostiene sobre sus propios méritos. Su aproximación a diseño básico de lenguaje es muy diferente de otros lenguajes de programación contemporáneos. Pruébalo. Es sencillo de tomar y divertido para programar.