El lenguaje Go
En este artículo hacemos una introducción al lenguaje Go
El lenguaje Golang de Google también llamado Go. Vamos a ver las características principales y luego haremos un pequeño programa de prueba para ilustrar como funciona.
El lenguaje Go o también llamado Golang fue creado por Google a mediados del año 2009, y hasta entonces no ha parado de incrementar su popularidad, hasta situarse en el Top 10 de los lenguajes de programación más utilizados hoy en día.
Principalmente se creó para la parte del backend, donde era el reino de Java y PHP, y Ruby en menor medida. También lo han vendido como el lenguaje C del siglo XXI.
Características
Es un lenguaje de programación estructurado y compilado. Está disponible para Windows, Mac OS y Linux.
Dispone de un recolector de basura del mismo modo que Java, con lo que no hay que llevar una contabilidad de punteros en memoria.
La sintaxis es parecida a C y contiene algunos parecidos con Python. Aunque la manera de definir el tipo de las variables es precisamente poniendo su tipo al final de la variable. Además permite definir el tipo de varias variables al mismo tiempo.
Algo interesante es que se pueden devolver más de una variable de retorno de una función.
Esto es sin duda una de las marcas de la casa, la sencillez y facilidad a la hora de programar, facilitando la vida al programador.
En principio tiene la posibilidad de programación orientada a objetos aunque para los que conocen Java, su interpretación es algo especial. De hecho no permite la herencia.
Es un lenguaje de programación compilado. Se genera código nativo para cada plataforma.
A nivel de concurrencia lo borda gracias a las gorutines, haciendo que la programación concurrente sea todo un placer.
Se estructura en paquetes (algo similar a Java), y existe un paquete especial llamado "main" que contiene una función llamada main(), que es el punto de entrada de un programa. Algo parecido al main de Java y C.
Como siempre, es mejor aprender practicando, así que vamos a instalar primero el Golang y vamos a realizar un programa de ejemplo.
Instalar Go
Para Windows prodemos descargar el programa de aquí y para Linux podemos hacer un apt-get install golang.
Como editor de código vamos a usar el Microsoft Visual Code, que tiene un plugin muy bueno para Go.
Crear el programa
Para empezar creamos una carpeta llamada spider y creamos un fichero main.go dentro de ella.
La estructura inicial del programa será:
package main import "fmt" func main(){ fmt.Println("Run") }
Este es el esqueleto básico. Vemos el uso del paquete por defecto, la importación de fmt, que sirve para leer y escribir por terminal y el uso de la función main.
Todos los programas go, tienen que tener un punto de entrada y ese es precisamente el método main del package main.
Por cierto, el punto y coma no es necesario en go.
Para compilar el programa hay que escribir go build main.go y nos generará un fichero ejecutable.
Otro comando interesante es gofmt -w main.go, que se encarga de formatear el código y sobreescribirlo (por eso el flag -w).
Y otro más es goimports -w main.go, que se encarga de importar y corregir los paquetes que nos hayamos olvidado.
Ejecutar el programa
Para ejecutarlo en el terminal también se puede escribir go run main.go y nos sacará por pantalla Run. Así de simple. Es un modo fácil de testear los programas sin tener que compilarlos.
Tipos de datos
Como lo mayoría de lenguajes, en Go podemos encontrar cadenas de texto (string), enteros (int), números con coma (float), booleanos (true/false) y el tipo "null" que aquí es "nil".
Para definir variables se utiliza var. El propio compilador ya infiere el tipo de dato que es. Si queremos definir e inicializar una variable podemos usar :=
Las constantes se definen con const como en otros lenguajes. Se pueden definir constantes para char, string, bool y números.
Aunque el tipo de dato lo infiere, habitualmente se especifica el tipo de datos después de la variable.
Arrays
Podemos definir listas de elementos de un tipo de datos con una cierta longitud. Tenemos la función len para obtener el tamaño del array. Se pueden declarar e inicializar usando los corchetes.
ar := [3]int{2,4,6}
Slices
Un tipo parecido al array es el slice que permite ampliar el tamaño del mismo con la función append.
Se crea con
s := make([]int, 4)
Por supuesto también tiene la función len.
Adicionalmente se puede copiar un slice en otro con la función copy(slice1, slice2) y se pueden obtener slices de slices especificando la posición inicial y la final (o sólo la inicial o sólo la final).
Esto es parecido a Python
Los arrays se pueden ordenar con la función sort importando el paquete "sort".
Maps
Los maps son otro tipo de datos conocidos en Python como dict, o hashes en Ruby. Se crean con la palabra make(map[tipo llave]tipo valor). Tenemos la función len para obtener el número de pares llave/valor. También la función delete(mapa, "llave") elimina un par llave/valor del map.
Range
Esta sentencia sirve para iterar los elementos de ciertas estructuras, como los slices y arrays, o los maps o incluso cadenas de texto (string).
Bucles
Usa el for para iterar. No se utilizan paréntesis, sólo los corchetes. Un "while" se puede hacer con un for sin puntos y coma.
for sum<1000 {...}Y tenemos el break y el continue como otros lenguajes.
Funciones
Las funciones se definen con func y al igual que las variables se especifica el tipo de datos que devuelve. Algo curioso como Python es que puede devolver más de un valor, y es habitual devolver un dato, y un tipo de dato de error.
Las funciones pueden recibir varios parámetros no especificados determinados con ... y se pueden invocar pasando un slice de este modo mifun(params...)
Punteros
Una característica de Go es que permite usar punteros. Así si una función se define como func mifun(puntero *int) especifica que recibe un puntero a int.
Para obtener la dirección de memoria del puntero se especifica &puntero
Structs
Otro tipo de dato con similitudes del lenguaje C. Los struct definen tipos de datos que contienen campos de diferentes tipos de datos.
type coche struct { marca string modelo string km int }
Se crea un struct con c:= coche{"Seat", "Arona", 24000}
Como suele ser habitual se accede a los campos con el punto . y se obtiene el puntero a la estructura de datos con el &
Se pueden definir métodos sobre los structs, diría que es lo más parecido de programar OOP en Go. Estos métodos se pueden definir sobre el struct o el puntero al struct, dependiendo de la mutabilidad que queramos.
Es decir si queremos evitar la copia y modificar el valor del argumento debemos definir el método con un puntero.
func (self/this *coche) velocidad() nombre { return c.marca + c.modelo }
Podemos nombrar a la variable self (parecido a Python) o this (parecido a Java), pero es una ilusión ya que no es lenguaje puro para programar en OOP.
Interfaces
Se pueden crear tipos que son interfaces que definen varios métodos. Y estos interfaces los pueden implementar los structs. Entonces si tenemos dos structs que implementan los métodos de un interfaz, podemos usar instancias de esos structs como parámetros de una función que recibe un datos de tipo interface.
Errores
Los errores se definen con errors.New("mi error") importando el paquete "errors".
Habitualmente se compara el error con nil para evaluar que no hay errores en el retorno de una función.
Panic
Un tipo de error especial es el Panic. Es para errores inesperados, valores inesperados, muestra las trazasd de error y aborta el programa.
Defer
Para gestionar el cierre de recursos se usa defer por ejemplo para cerrar ficheros abiertos o conexiones de red.
Json/Xml/Base64
Go incorpora por defectos varios paquetes para codificar y descodificar JSON y XML y Base64.
Fechas
Go también incorpora paquetes para gestionar fechas y tiempos
Ficheros
Go permite trabajar con ficheros como los lenguajes habituales.
HTTP
Go incorpora paquetes para trabajar con http ya sea como cliente o como servidor.
Se puede programar un servidor web fácilmente usando
http.ListenAndServe("localhost:8000", handler)y el handler recibe un http.ResponseWriter, y un *http.Request
Procesos
Asimismo Go incluye paquetes para ejecutar procesos externos (fuera de Go, no se trata de una goroutine) y gestionar señales de tipo UNIX
dirCmd := exec.Command("ls") datos, err := dirCmd.Ouput()
Otra opción es sustituir el proceso actual de Go por otro proceso mediante el uso del paquete "syscall".
En el caso de las señales, gracias al paquete "os/signal" se puede gestionar una parada del proceso al recibir una señal SIGTERM.
Sentencias de flujo
Usa el if y el else. Lo mismo que el for, las condiciones sin paréntesis y las sentencias del if y el else van entre corchetes.
También usa switch para sentencias de flujo de varias opciones.
Goroutines
Es la manera de ejecutar código concurrente en un programa go, algo parecido a un thread ligero de Java por ejemplo. Se definen con la sentencia go mifun(), aunque también se pueden usar con una función anónima.
Para comunicar las goroutines se usan los canales. Se pueden enviar valores a través de los canales de una goroutine a otra. Para crear un canal se especifica mican := make(chan string).
Para enviar un valor al canal se usa mican <- "cadena de texto" y para recibirlo se usa valorecibido := <-mican
Este tipo de canales son unbuffered por defecto. Hay canales buffered que se crean con mican := make(chan string,4) donde 4 es el número de valores del buffer.
Se pueden usar canales con un tipo de dato booleano para sincronizar goroutines.
fin := make(chan bool, 1) go hilo(fin) <- fin //bloquea la ejecución hasta recibir un mensaje del hilo a través del canal
También se puede definir si un canal es para recibir o enviar cuando se pasa como parámetro de una función.
func recibo(r chan<- string, texto string) { r <- texto } func envio(r chan<- string, en <-chan string) { texto := <-r en <- texto }
Asimismo se puede usar la sentencia select para esperar en múltiples canales.
select { case menA := <-canal1: fmt.Println("recibi", menA) case menB := <-canal2: fmt.Println("recibi", menB) }
Si se añade el "case" default: saldrá inmediatamente por esa opción. Es la manera de gestionar operaciones entre canales de modo no-bloqueante.
Los canales se pueden cerrar, indicando que ya no se enviarán más mensajes a través de él. Se usa la sentencia close(mican) para cerrarlo.
Timeouts
En Go se implementan timeouts usando los canales y la sentencia select.
Para ello se usa <-time.After(n * time.Second) dentro de un select case
El paquete "time" también permite crear temporizadores con mitimer := time.NewTimer(5 * time.Minutes) y temporizador repetitivo con miticker := time.NewTicker( 2000 * time.Millisecond)
Colas
Un caso práctico en Go es la creación de un tipo de canal simulando una "cola". Dado que podemos iterar con range en un canal, podemos obtener los mensajes recibidos.
micola := make(chan string, 3) micola <- "hola" micola <- "tutoriales" micola <- "online" close(micola) for e:= range micola { fmt.Println(e) }
Sincronizar el estado
Un tema importante es la gestión del estado, debido al acceso concurrente de las goroutines a los datos. Para ello se usan funciones del paquete "sync" atomic y mutexes, para sincronizar el acceso a variables compartidas entre varias goroutines.
Estas funciones son:
var mutex = &sync.Mutex{} mutex.Lock() mutex.Unlock()
Ejemplo práctico
Vamos a crear un programa para que se descargue una página de internet.
Para ello el programa tendrá que pedir por la consola la dirección (url) y luego accederá a la URL e imprimirá el contenido.
Para la entrada de parámetros usaremos el paquete "flag" .
func main(){ weburl:=flag.String("url","google.com","La URL a buscar") flag.Parse() fmt.Println("Run:") fmt.Println(*weburl) }
A continuación con la ayuda del paquete net invocaremos a la url y con el paquete ioutils dispondremos de métodos para lectura y escritura.
También podemos usar el paquete os para salir del programa en caso de error.
Así pues el ejemplo quedaría de la siguiente manera:
package main import ("fmt" "flag" "net/http" "io/ioutil" "os") func main(){ weburl:=flag.String("url","http://google.com","La URL a buscar") flag.Parse() fmt.Println("Run:") fmt.Println(*weburl) resp, err := http.Get(*weburl) if err != nil { fmt.Println("Error http:", err) os.Exit(1) } body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println("Error lectura:", err) os.Exit(1) } fmt.Println(string(body)) }
Repasemos lo más importante del lenguaje Go:
Las sentencias if-else no requieren paréntesis, pero sí las llaves de apertura y cierre.
También se puede ver que para asignar una variable, se puede usar el ":=". Este es un modo fácil de asignar el valor y el tipo a la variable.
En Go existen los tipos habituales: int, bool, string, []int, etc. Si no asignamos un valor a la variable, Go lo pondrá a 0, "", 0.0, false, nil.
Por otro lado, se puede asignar una variable del modo más habitual, definiendo su tipo, con la sentencia var. Así var cadena string = "cadena"
Los arrays se definen como var lista [10]string y se pueden informar los valores como es habitual: lista[0] = "Hola"
Podemos definir lo que llaman slices, que son arrays sin un tamaño fijado, que pueden crecer dinámicamente: var lonchas []string
Para ello se utiliza la sentencia append(lonchas, "cadena"), que añade elementos al array. O de un modo más compacto: lonchas []string{"uno","dos"}
Luego para recorrer los elementos de un slice se puede hacer con un bucle:
for i,elem := range lonchas { ... }
Consejo: Mira la documentación oficial para conocer más sobre range y la variable no usada "_".
Otro modo de leer los parámetros de entrada es con os.Args. Esto es un array de elementos, donde el [0] es el nombre del ejecutable, el [1] el primer parámetro, y así sucesivamente. Para leer la longitud de un array se puede usar la función len(variable).
Si queremos compilar el programa escribiremos go build spider.go y se generará un ejecutable.
Definir estructuras
Un modo de enriquecer los tipos de datos es con el uso de estructuras.
Se definen fuera de la función main, y se inicializan dentro de la función main, del estilo:
type coche struct { marca string modelo string puertas int }
Luego se inicilizaría tal como micoche := coche{marca: "seat", modelo: "ibiza", puertas: 3}
Estas estructuras también pueden tener métodos, por ejemplo el color del coche y se definen con la sentencia:
func (c coche) color() string { ... }
Dentro del método se podría acceder a las propiedades de la estructura con el punto como c.marca o c.modelo.
Esto sería lo más parecido a orientación a objetos que Go puede tener. Exsite además la sentencia interface que permite definir métodos comunes que deben implementar las diferentes estructuras.
Nota: Para que una función se pueda usar fuera de su paquete hay que adoptar una convención de poner la primera letra en mayúsculas.
El puntero contrataca
Sí, de nuevo volvemos a tener punteros en Go. Es decir.
Si se define una variable cadena := "hola" y otra cadena2 := cadena. La segunda obtiene una copia del valor de la primera.
Si definimos cadena2 := &cadena entonces la segunda obtiene una referencia al valor de la primera.
Los métodos pueden tener punteros como parámetros, para definirlo así se usa el * antes del tipo de estructura.
Definir funciones
Para definir funciones, se utiliza la sintaxis:
func nombre (variable tipo) tiporetorno { ... }
Un modo curioso de implementar que una función puede devolver un error, es retornando dos variables, sí eso mismo, dos variables al mismo tiempo.
func nombre (variable tipo) (tiporetorno, error) { ... }
Para ello hay que importar el paquete errors y asignar un error tal como:
err := errors.New("Error a")
Si queremos devolver el resultado de una función sin error, lo podemos poner a nil. De ese modo
return variableretorno, nil
Por el contrario si encontramos un error, es algo común comprobarlo, entonces logamos el mensaje y salimos del programa:
if err != nil { fmt.Println(err) os.Exit(1) }
Un tipo especial de funciones son las gorutines, que son funciones que se ejecutan concurrentemente con el resto. Y para ello se definen con la sentencia go delante de la función.
Bucles
Realmente sólo vamos a usar la sentencia for, ya que no dispone de ninguna otra. Pero vamos a ver que realmente es como una navaja suiza. Nos va a permitir realizar todo tipo de bucles. Como cabía esperar, no utiliza los paréntesis, pero las llaves son obligatorias.
for expr antes 1a iterac ; condicion antes cada interac ; expr después de iterac { ... }
Por ejemplo para iterar de 0 a 9: for i := 0 ; i<10 otro="" uso="" en="" la="" forma:="" p="">
for condicion { ... }
sin especificar dentro del for la variable antes de la 1a iterac.
Para salir del bucle, se usa la sentencia break, como en muchos otros lenguajes de programación.
La sentencia
for { .. }
haría un bucle infinito, que se ejecutaría hasta que el programa se abortara.
Conclusión
En resumen, el lenguaje Go es un todoterreno, a nivel de programación concurrente, a nivel de backend e incluso para programar algunos protocolos blockchain como Ethereum y Ontology.
Varias empresas lo usan (Paypal, Salesforce, Uber) y es uno de los niños bonitos de Google.