Herramientas de usuario

Herramientas del sitio


informatica:programacion:go

Go

Lenguaje de programación concurrente y compilado inspirado en la sintaxis de C, que intenta ser dinámico como Python y con el rendimiento de C o C++.

Las notas aquí recogidas están centradas en la versión 1.17 de Go

Lo bueno

  • Número bajo de palabras reservadas (keywords)
  • Rapidez como C++
  • go doc: permite consultar y generar documentación
  • gofmt: herramienta oficial que automáticamente formatea código Go. Tan sencillo como gofmt -w codigo.go
  • Compilación cruzada: poder compilar en diferentes sistemas y hacia cualquier sistema.
  • Manejo muy eficiente de concurrencia

Go te obliga a mantener el código limpio. Si declaras la importación de un paquete y no lo usas, Go se quejará y no compilará:

package main
 
import "fmt"
 
func main() {
}

El código anterior no compilará. Go dirá imported and not used: “fmt”

Tampoco permite tener variables o sentencias que no se usen:

package main
 
func main() {
    a := 0
}

El código anterior no compilará. Go dirá a declared and not used

Ejemplos

package main
 
// Varias formas de importar paquetes
import (
    "fmt"
    . "fmt"
    _ "log"
)
 
const language = "language"
 
var global = true
 
func main() {
    // Método largo de declaración e inicialización de variables
    var a float64 = 2
 
    // Método abreviado
    b := 0
 
    fmt.Println(a)
    Println(b)
}

Ejemplo de bucles:

package main
 
import (
    "fmt"
    . "fmt"
    _ "log"
)
 
func main() {
 
    // Sintaxis normal de bucle 'for'
    for j := 0; j < 5; j++ {
        Println(j)
    }
 
    // Creación de un array de 3 enteros
    nums := []int{10, 20, 30}
 
    // Forma de recorrer array con 'range' (palabra reservada)
    // 'range' devuelve 2 valores: índice del valor
    // que se tiene en la iteración y el otro es el valor
 
    // Con el identificador blanco (_) ignoramos una de las variables
    // que devuelve 'range'
    for _, n := range nums {
        Println(n)
    }
 
}

Ejemplo de switch:

package main
 
import (
    "fmt"
    . "fmt"
    _ "log"
)
 
func main() {
 
    bestLanguage := "Go"
 
    // A diferencia de otros lenguajes, no hay que terminar
    // cada 'case' con un 'break'. En Go funciona al revés,
    // es decir, si ponemos un break, continúa al siguiente
    // 'case'
    switch bestLanguage {
        case "Python":
            fmt.Println("Ouch, no! It's for scripting")
        case "Scala":
            fmt.Println("Ouch, no! It's funcional")
        case "Go":
            fmt.Println("Awww yeees!!!")
    }
 
}

Funciones en Go:

package main
 
import (
    . "fmt"
    _ "log"
)
 
// Si el nombre de la función está en mayúscula, la
// función se puede "exportar", es decir, sería
// pública, expuesta fuera de este paquete
func Message() string {
    return "Hello World"
}
 
// Podemos devolver el mensaje de forma implícita
// Es como si declarásemos una variable con un valor
func MessageImplicit() (msg string() {
    msg = "Hello World"
    return
}
 
func main() {
 
}

Instalación

Linux

Para instalar el compilador estándar de Go podemos ir a la web oficial o buscar el paquete para nuestra distribución. Por ejemplo, para el caso de Arch Linux:

pacman -S go

Comprobamos si todo está instalado y configurado correctamente:

$ go version

go version go1.20.3 linux/amd64

Linux manual

  1. Descargar de la página oficial

Extraemos el contenido en /usr/local:

rm -rf /usr/local/go && tar -C /usr/local -xzf go1.17.linux-amd64.tar.gz

El comando anterior borrará la instalación (si la hubiera) de /usr/local/go

Añadimos /usr/local/go/bin a la variable de entorno PATH, por ejemplo añadiendo la siguiente línea a $HOME/.profile (o /etc/profile para todos los usuarios):

export PATH=$PATH:/usr/local/go/bin

Para que se apliquen los cambios en la variable de entorno, tenemos que cerrar la sesión y volver a entrar o podemos aplicarlos inmediatamente con source $HOME/.profile

Verificamos que la instalación ha sido correcta:

go version

Ejemplo de salida:

go version go1.21.0 linux/amd64

Configuración

Para ver las variables de entorno y configuraciones que usa Go:

go env

GOPATH

Muestra la localización del directorio de trabajo, es decir, donde trabajaremos con archivos de Go.

Actualmente esta variable en vías de desaparición

Por defecto, está establecido en el directorio personal del usuario. home/usuario/go en Linux, por ejemplo.

El directorio $GOPATH/src se usa para almacenar el código fuente de los paquetes. Cuando se compila un programa Go, también se crean los directorios bin para ejecutables y pkg como caché para los paquetes individuales.

En Go, cada programa ejecutable necesita su propio directorio:

$GOPATH
|- src
   |- foo
      |- main.go

Terminología

Paquetes

Un paquete (package) es una forma de agrupar funciones. Está formado por una serie de ficheros contenidos en un mismo directorio.

Módulos

En un módulo se agrupan paquetes relacionados que contienen funciones útiles. El código de Go está agrupado en paquetes y los paquetes están agrupados en módulos.

Biblioteca estándar de Go

Standard library la forman paquetes y utilidades incluidas en la instalación de Go, es decir, que no necesitamos recursos externos para utilizarlas en nuestro código. Esto permite ampliar las características básicas del lenguaje. Esta biblioteca no solo incluye documentación sino trozos de código de ejemplo.

Para usarla en nuestros programas, sencillamente hacer la llamada mediante la palabra reservada import del paquete que queramos de esa biblioteca:

import "fmt"

Herramientas

Utilidades que vienen con la instalación de Go

go fmt

Formatea el código fuente de Go para respetar las especificaciones.

gofmt -w main.go

Si no ponemos la opción -w, gofmt mostraría los cambios que va a realizar.

go run

Compila y ejecuta los ficheros Go:

go run main.go

go build

Compila para poder obtener el binario:

go build -o main main.go

Con la opción -o indicamos el nombre del archivo resultante.

La compilación genera un binario adaptado a la arquitectura donde se ejecuta la compilación. Si queremos compilarlo para una arquitectura diferente, se puede indicar como argumento.

go mod

Dentro del directorio de nuestro proyecto:

go mod

Hola, mundo

package main
 
import "fmt"
 
func main() {
    fmt.Println("Hola, mundo!")
}
  • La cláusula package debe ser lo primero que escribamos en nuestro código fuente.
  • main es el nombre del paquete al que pertenece el archivo que creamos.
  • func define una función, un bloque de código reutilizable.
  • main es una función especial que Go ejecuta automáticamente.
  • fmt (formatting), paquete que incluye la biblioteca estándar de Go.

Cada programa Go debe pertenecer a un único paquete

En Go, el código que será ejecutado como una aplicación debe estar en el paquete main

Si existe una función main, se llama por defecto al ejecutar el paquete main

Compilación

El código escrito en Go debe pasar por un proceso de compilación para que sea traducido a lenguaje máquina y el sistema operativo lo entienda y sea capaz de ejecutarlo.

La compilación se realiza con go build.

Nos situamos en el directorio que contiene el código Go y ejecutamos:

go build

Se creará un ejecutable llamado igual que el directorio:

[usuario@machine hello]$ ls -l
total 1988
-rwxr-xr-x 1 tempwin wheel 2030685 dic  1 17:17 hello
-rw-r--r-- 1 tempwin wheel      76 dic  1 17:11 main.go

Podremos ejecutarlo con ./hello

Por Sistema Operativo

A la hora de compilar un programa de Go, podemos indicar en qué sistema operativo se utilizará:

  • Linux: GOOS=linux GOARCH=arm GOARM=7 go build
  • Window: GOOS=windows GOARCH=386 go build
  • OS X: GOOS=darwin GOARCH=386 go build

Combinaciones válidas:

$GOOS $GOARCH
aix ppc64
android 386
android amd64
android arm
android arm64
darwin amd64
darwin arm64
dragonfly amd64
freebsd 386
freebsd amd64
freebsd arm
illumos amd64
js wasm
linux 386
linux amd64
linux arm
linux arm64
linux ppc64
linux ppc64le
linux mips
linux mipsle
linux mips64
linux mips64le
linux s390x
netbsd 386
netbsd amd64
netbsd arm
openbsd 386
openbsd amd64
openbsd arm
openbsd arm64
plan9 386
plan9 amd64
plan9 arm
solaris amd64
windows 386
windows amd64

Compilación y ejecución

go build está muy bien para grandes aplicación o si queremos desplegarlo en algún sitio, pero no es nada cómodo para el desarrollo.

Hay otra herramienta para poder compilar el programa y ejecutarlo a continuación:

go run main.go

go run no crea un ejecutable, solo compila y ejecuta el código compilador

Si queremos ver lo que hace esa herramienta, podemos utilizar la opción -x:

go run -x main.go

Si queremos ejecutar todo lo que hay en el directorio actual:

go run .

También podríamos hacerlo como go run *.go o go run main.go otro.go otro-mas.go

Palabras reservadas

Las palabras reservadas son aquellas propias del lenguaje y que no pueden ser utilizada como identificadores:

  • break
  • default
  • func
  • interface
  • select
  • case
  • defer
  • go
  • map
  • struct
  • chan
  • else
  • goto
  • package
  • switch
  • const
  • fallthrough
  • if
  • range
  • type
  • continue
  • for
  • import
  • return
  • var

Sentencias

Las sentencias (statements) son instrucciones que controlan el flujo de ejecución del programa.

package main
import "fmt"
 
func main() {
    fmt.Println("Hola, mundo!")
}

El código está formado por sentencias que se ejecutan de arriba a abajo, salvo los bucles y condicionales.

Una sentencia por línea. Si quisiéramos poner más de una instrucción por línea, usaríamos el punto y coma (;) para separarlos:

    fmt.Println("Hola, mundo!"); fmt.Println("Adiós, mundo!")

Incremento/decremento

Aumenta o reduce los valores numéricos en 1:

var n int
 
n++ // es lo mismo que n = n + 1 o n += 1
 
n-- // es lo mismo que n = n - 1 o n -= 1

Al ser considerado una sentencia, el incremento y decremento no pueden ser usados en expresiones. 5 + n-- sería incorrecto.

Expresiones

Una expresión es un código que produce uno o varios valores.

package main
import "fmt"
 
func main() {
    // Concatenamos cadenas de caracteres con el operador +
    fmt.Println("Hola, " + "mundo!")
}

Comentarios

Texto que el compilador de Go no interpretará. Se utiliza para añadir notas en el código, explicaciones, etc.

// Comentario de una línea
package main
 
import "fmt"
 
/*
    Comentarios de
    bloque, para poder escribir
    varias líneas sin tener que
    precederlas de //
*/
func main() {
}

Tipos de datos

Enteros

  • int
  • int32: 4 bytes

Ejemplos de literales enteros:

  • -1
  • 0
  • 27

En programación un literal es un valor en el código fuente.

Decimales

float64

Ejemplos:

  • -0.5
  • -.5
  • 0.0
  • 0.
  • 1.0
  • 1.

Lógicos

bool

  • true
  • false

Cadenas

string

Los literales de cadena en Go se codifican en UTF8.

  • "hola"

Un string es una serie de bytes

Las runas representan un codepoint.

Si queremos un string con varias líneas, utilizamos el acento grave (`):

(...)
var s string
 
s = "hola"
s = `hola`
 
fmt.Println(s)
 
s = "<html>\n\t<body>\"Hello\"</body>\n</html>"
 
// Lo podemos hacer más legible:
 
s = `
<html>
    <body>"Hello"</body>
</html>`
 
fmt.Println(s)

Las cadenas crudas (raw string literal) no son interpretadas por Go, se muestra tal como se escriben, así que no son necesarias las secuencias de escape.

Byte

“hey” se puede representar como []byte{104, 101, 121}

Tipos compuestos

Tipos de datos compuestos por otros tipos de datos.

  • Arrays: colección de elementos. Tamaño fijo.
  • Slices: colección de elementos. Tamaño variable.
  • String Internals: slices de bytes
  • Maps: colección de pares de clave-valor
  • Structs: agrupación de diferentes tipos de variables juntos.

Sistema de tipos

Go utiliza un sistema de tipos estático. A diferencia de los dinámicos, en los estáticos la comprobación de los tipos de datos se hace en tiempo de compilación, antes de ejecutar el programa.

Un sistema de tipos responde a las siguientes preguntas:

  • ¿Qué clase de tipos están disponibles?
  • ¿Cómo interactúan esos tipos entre ellos?
  • ¿Qué está permitido y qué no?
  • ¿Cómo se crean nuevos tipos?

Tipos predeclarados

Tipo integrado en el lenguaje así que puede ser usado inmediatamente. En Go:

  • bool
  • int (se comporta como int32 o int64 dependiendo de la máquina), int8, int16, int32, int64 (los números indican los bits de longitud)
  • uint, uint8, uint16, uint32, uint64 (enteros positivos)
  • string
  • float32, float64
  • complex64
  • complex128

Tipos definidos

Son tipos creados a partir de tipos ya existentes.

type Duration int64

Duration sería un nuevo tipo creado a partir del tipo int64

Aunque un tipo se defina a partir de otro, no son iguales. Para el ejemplo anterior, Duration <> int64. Tendríamos que convertirlo de esta manera: ns = Duration(ms); ms = int64(ns)

Ejemplo:

// Convierte gramos en onzas
package main
 
import "fmt"
 
type gramo float64
type onza float64
 
func main() {
    var g gramo = 1000
    var o onza
 
    // Debemos convertir 'gramo' en el tipo definido 'onza'
    o = onza(g) * 0.035274
 
    fmt.Print("%g gramos son %.2f onzas\n", g, o)
}

Los tipos definidos heredan las operaciones de los tipos en los que se basan, pero no sus métodos.

Las razones para definir nuevos tipos:

  • Permite declarar nuevos métodos
  • Seguridad de tipos
  • Legibilidad

Variables

Una variable es un contenedor donde se guarda alguna información. El nombre de la variable nos permite acceder al valor que contiene.

Go es un lenguaje de tipos estáticos, es decir, cada variable tiene un tipo y no puede ser cambiado. Si decidimos que una variable almacena un número entero, no podremos usarla para almacenar otro tipo de dato.

Declaración

Antes de poder usar una variable, hay que declararla, es decir, darle un nombre:

var nombre int
  • var: abreviatura de variable que indica que vamos a declarar una variable
  • nombre es el nombre de la variable, su identificador.
  • int: tipo de dato que contendrá la variable.

Las variables deben comenzar siempre por una letra o un guión bajo (_).

Las variables declaradas deben ser usadas o Go se quejará.

Las buenas prácticas de Go indican que la nomenclatura de las variables debe ser camelCase

Declaración múltiple:

var (
    speed int
    heat  float64
    off    bool
    brand string
)

Se pueden declarar también agrupándolas por tipo de dato:

var speed, velocity int

Tras declarar una variable, Go inicializa automáticamente al valor cero que corresponda:

  • int: 0
  • float64: 0.0
  • string: ""
  • bool: false

Inicialización

Declaramos una variable y le damos un valor inicial:

var seguro bool = true

Go utiliza inferencia de tipos para deducir el tipo de la variable según el valor, de tal manera que podríamos ahorrarnos indicar el tipo:

var seguro = true

La declaración corta nos ahorra aún más código:

seguro := true

La declaración breve permite también la declaración múltiple:

seguridad, velocidad := true, 50
  • seguridad: se autodeclara como bool y se asigna a true
  • velocidad: se autodeclara como int y se asigna a 50

La declaración abreviada no se puede usar fuera de las funciones. A nivel de paquete las declaraciones deben empezar con las palabras reservadas (package, var, func)

Asignación

Una vez declarada, podemos cambiar el valor de una variable mientras sea el mismo tipo de dato.

El operador de asignación es el igual (=):

// Se inicializa a 0
var velocidad int
 
// Cambiamos el valor de la variable
velocidad = 100

Para hacer una asignación múltiple:

var velocidad int
var seguro bool
 
velocidad, seguro = 100, true

Podemos asignar valores desde una función:

package main
 
import (
    "fmt"
    "path"
)
 
func main() {
    var dir, file string
 
    dir, file = path.Split("ruta/fichero.ext")
 
    fmt.Println("dir: ", dir)
    fmt.Println("file: ", file)
}

La función Split() devuelve dos valores y los asignamos a sendas variables.

Si una función devuelve más de un valor y solo nos interesa uno de ellos, podemos utilizar el identificador vacío para descartarlo:

(...)
func main() {
    var file string
 
    // Solo nos interesa el nombre del fichero
    _, file = path.Split("ruta/fichero.ext")
 
    fmt.Println("file: ", file)    

Versión resumida:

...)
func main() {
 
    // Solo nos interesa el nombre del fichero
    _, file := path.Split("ruta/fichero.ext")
 
    fmt.Println("file: ", file)    

Nomenclatura

El nombrado de variables, constantes… es importante porque tiene que ver con la legibilidad lo que lo hace importante para el mantenimiento de un programa.

Abreviaturas populares en Go:

var s string      // string
var i int         // índice
var num int       // número
var msg string    // mensaje
var v string      // valor
var val string    // valor
var fv string     // valor de bandera
var err error     // valor de error
var args []string // argumentos
var seen bool     // se ha visto?
var parsed bool   // se ha 'parseado' correctamente?
var buf []byte    // buffer
var off int        // offset
var op int        // operacion
var opRead int    // operación de lectura
var l int         // logitud
var n int         // número o número de
var m int         // otro número
var c int         // capacidad
var c int         // caracter
var a int         // array
var r rune        // runa
var sep string    // separador

Constantes

Las constantes no cambian su valor.

const nombre_constante = valor

Ejemplo:

(...)
func main() {
    // Definimos una constante
    const metros int = 100
 
    cm := 100
    m := cm / metros
 
    fmt.Println("%dcm son $dm\n", cm, m)
}

No es necesario declarar el tipo de las constantes, Go lo infiere de acuerdo al valor que le asignemos.

Declaración múltiple de constantes:

const min, max int = 1, 100

Otra forma:

const (
    min int = 1
    max int = 100
)

Las constantes toman el tipo de la anterior así como su valor si no se especifica:

const (
    min int = 1
    max
)
 
fmt.Println(min, max)

Devolverá: 1 1

iota

Generador de constantes integrado en el lenguaje Go. Genera todos los números de forma incremental.

func main() {
    const (
        lunes = iota
        martes
        miercoles
        jueves
        viernes
        sabado
        domingo
    )
 
    fmt.Println(lunes, martes, miercoles, jueves, sabado, domingo)
}

Al ejecutarlo, imprimirá por pantalla: 0 1 2 3 4 5 6

iota comienza en 0

Operadores

Aritméticos

8 * 2

8 y 2 son operandos y * es el operador, el que indica la operación a realizar.

Nombre Operador Expresión Resultado
Negación - -(-2) 2
Producto * 8 * -4.0 -32.0
Cociente / -4 / 2 -2
Módulo % 5 % 2 1
Suma + 1 + 2.5 3.5
Resta - 2 - 3 -1

Si alguno de los operandos es decimal, el resultado será decimal.

Comparación

Nombre Operador Expresión Resultado
Igualdad == 3 == 3 true
No igualdad != 2 != 3 true
Menor < 3 < 2.5 false
Menor o igual <= 4.0 ⇐ 4 true
Mayor > 3 > 2.5 true
Mayor o igual >= 4.0 >= 4 true

Lógicos

Nombre Operador Expresión Resultado
AND && true && false true
OR || 2 != 3 true
NOT ! !false true

Para la operación AND Go utiliza lo que se conoce como corto-circuito: solo será cierto si todos los operandos son ciertos.

Para la operación lógica OR solo devuelve falso si todos los operandos son falsos.

Asignación

El igual (=) se usa para la asignación de valores a variables.

Para hacer alguna operación sobre una variable y asignarle el nuevo valor, se pueden utilizar las operaciones de asignación:

area := 20
 
area -= 10 // Reduce el área en 10
area += 10 // Aumenta el área en 10
area *= 2  // Duplica el área
area /= 2  // Divide el área

Precedencia

La precedencia determina el orden de las operaciones.

Las operaciones se evalúan de izquierda a derecha y con la siguiente prioridad:

Precedencia Operador
5 * / % « » & &^
4 + - | ^
3 == != < <= > >=
2 &&
1 ||

Cadenas

Para concatenar (combinar) cadenas se utiliza el operador +:

(...)
nombre, apellido := "Carl", "Sagan"
 
fmt.Println(nombre + " " + apellido)

Conversión de tipos

Go permite la conversión de tipos, también llamada casting:

tipo(valor)

Ejemplos:

velocidad := 100 // int
fuerza := 2.5 // float64
 
velocidad = velocidad * fuerza // error, el resultado sería float64

Realizamos una conversión:

velocidad = velocidad * int(fuerza) // funciona porque convertimos a entero

Las conversiones son destructivas. Por ejemplo, si tenemos el valor 2.5 y hacemos int(2.5), el resultado será 2, perdiendo la parte decimal.

Funciones

Bloques de código reutilizables.

Para definir una función se utiliza la palabra reservada func:

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

Documentación

Local

Si queremos ver la documentación de cierta función de un determinado paquete:

go doc -src fmt Println
  • -src: opción para indicar que queremos ver el código
  • fmt: paquete que contiene la función que nos interesa.
  • Println: función de la que queremos obtener información.

Ejemplo de salida:

package fmt // import "fmt"

// Println formats using the default formats for its operands and writes to standard output.
// Spaces are always added between operands and a newline is appended.
// It returns the number of bytes written and any write error encountered.
func Println(a ...interface{}) (n int, err error) {
	return Fprintln(os.Stdout, a...)
}

Si solo queremos ver la documentación:

go doc fmt Println

Ejemplo de salida:

package fmt // import "fmt"

func Println(a ...interface{}) (n int, err error)
    Println formats using the default formats for its operands and writes to
    standard output. Spaces are always added between operands and a newline is
    appended. It returns the number of bytes written and any write error
    encountered.

Paquetes

Un paquete es una forma de agrupar funciones.

En Go todo depende de paquetes. Los paquetes están alojados en una estructura de directorios. Si van a estar en un repositorio externo (Github, Gitlab…), también hay que indicarlo on go mod

Todos los ficheros que pertenezcan a un paquete deben estar en el mismo directorio.

package main

La cláusula package solo puede aparecer una vez en el código, ya que un fichero solo puede pertenecer a un único paquete.

Hay dos clases de paquetes en Go:

  • Paquetes ejecutables: contienen func main(). Se crean para ser ejecutados.
  • Paquetes de biblioteca: solo se pueden importar. Se crean para poder reutilizarlos.
Biblioteca Ejecutable
Creados para reutilizar Creados para ejecutarse
No ejecutable Ejecutable
Se pueden importar No se pueden importar
Pueden tener cualquier nombre El nombre debe ser main
No contienen func main Contienen func main

Paquetes de biblioteca

Una buena práctica es llamar a nuestro paquete de la misma manera que el nombre del directorio.

Por ejemplo, creamos un directorio llamado printer y dentro de él un fichero llamado printer.go:

package printer
 
import "fmt"
 
fun hello() {
    fmt.Println("unexported hello")
} 

Un paquete de biblioteca no puede ser ejecutado y no tiene que compilarse. Puede ser importado directamente.

Si queremos instalarlo, utilizaríamos el comando go install. Esto haría que se crease un directorio dentro de $GOPATH/pkg/arquitectura/ruta_proyecto/ con el contenido:

  • printer.a: paquete ya compilado y archivado (ocupa menos espacio)

Las funciones/métodos de las bibliotecas que se pueden exportar son los que empiezan por mayúscula. Lo mismo sucede para variables y constantes.

Creamos un fichero que usará el paquete creado anteriormente. Por convención, el ejecutable debería ir en la carpeta cmd dentro de nuestro proyecto (en este caso printer:

// main.go
package main
 
import "proyecto/printer"
 
func main() {
    printer.hello()
}

Si intentamos compilarlo o ejecutarlo, mostrará un error porque hello() no se puede exportar. Para solucionarlo, tendríamos que ir al fichero printer.go y hacer el cambio en el nombre de la función:

package printer
 
import "fmt"
 
func Hello() {
    fmt.Println("unexported hello")
} 

Entonces podremos llamar a la función Hello() desde nuestro main.go:

// main.go
package main
 
import "proyecto/printer"
 
func main() {
    printer.hello()
}

Ámbito / scope

Visibilidad de quién puede ver qué y hacer qué.

Variables

Cuando hacemos una declaración, esta debe ser única, no puede aparecer otra vez dentro del mismo ámbito. Por ejemplo, no podemos tener dos funciones main dentro del paquete main.

package main
 
// Solo será visible en este fichero
import "fmt"
 
const ok = true
 
func main() {
    // La siguiente variable solo será visible dentro de este bloque
    // Fuera del bloque de esta función, nada en el programa podrá
    // acceder a esta variable
    var hello = "Hello!"
 
    fmt.Println(hello, ok)
}
package main
 
import "fmt"
 
func nope() {
    const ok = true
    var hello = "Hello!"
}
 
func main() {
    // Producirá un error porque la función *nope* está fuera
    // del ámbito/alcance/scope de la función *main*
    fmt.Println(hello, ok)
}

Paquetes

Los nombres solo pertenecen al paquete en el que son declarados:

package main
 
import "fmt"
 
// La función *main* solo es visible a través del paquete *main*
func main() {
    fmt.Println("Hola!")
}

Si tenemos este otro fichero dentro del paquete main:

package main
 
import "fmt"
 
func bye()
    fmt.Println("Bye!")
}

Podríamos llamar a la función bye() en el otro fichero porque ambos pertenecen al paquete main:

package main
 
import "fmt"
 
func main() {
    fmt.Println("Hola!")
    bye()
}

Importación

La importación es incluir código externo en nuestro código. Es como si declarásemos lo que hay dentro de los ficheros del paquete en nuestro propio fichero de Go.

El paquete fmt está formado por los ficheros:

  • doc.go
  • errors.go
  • format.go
  • print.go
  • scan.go

Cuando importamos fmt estamos incluyendo todos esos ficheros en el nuestro:

// mi-fichero.go
import "fmt"
 
CODIGO

Para importar más de un paquete, podemos hacerlo de dos maneras:

import "fmt"
import "runtime"

O en la misma sentencia:

import (
    "fmt",
    "runtime"
)

Si queremos ahorrarnos escribir el nombre del paquete a la hora de usar alguna de sus funciones/métodos, lo importamos con el punto:

import . "fmt"
 
func main() {
    // fmt.Println
    Println("Hola")
}

Si queremos temporalmente que no se importe algún paquete (por pruebas, por ejemplo), utilizaremos el identificador en blanco (_):

import (
    "fmt"
    _ "log"
)

La lista de los paquetes a importar debe estar ordenada alfabéticamente o Go lo considerará como un error

Renombrar

Podemos importar múltiples paquetes, pero todos deben ser únicos:

import "fmt"
import "fmt"

Sería incorrecto, pero si hacemos:

import "fmt"
import f "fmt"

Podríamos usar el paquete fmt mediante fmt o f:

package main
import "fmt"
import f "fmt"
 
func main() {
    fmt.Println("Hello!")
    f.Println("There!")
}

Conversión de tipos

El paquete strconv dispone de funciones para la conversión de diferentes tipos de datos

Convertir de entero a string:

miCadena = strconv.Itoa(123)

Convertir de string a entero:

miEntero, _ = strconv.Atoi("123")

Convertir a bool:

miBool, _ = strconv.ParseBool("true")

Convertir a decimal:

miDecimal, _ = strconv.ParseFloat(123)

Convertir a entero:

miEntero, _ = strconv.ParseInt("123")

Entrada por teclado

Para que el programa tome argumentos en línea de comandos usaremos el paquete os que se encarga de ofrecer funcionalidades de sistema operativo.

Dentro de ese paquete hay una variable llamada Args:

var Args []string // trozos de strings

Cuando ejecutamos un programa Go, los argumentos se meten en esa variable automáticamente.

go run main.go hi yo

Para acceder a cada parámetro lo hacemos mediante la posición que ocupa en la variable Args:

  • Args[0]: ruta al programa
  • Args[1]: primer argumento
  • Args[n]: argumento que ocupa la posición n.
package main
 
import(
    "fmt"
    "os"
)
 
func main() {
    fmt.Printf("%#v\n", os.Args)
 
    fmt.Println("Path: ", os.Args[0])
    fmt.Println("Primer argumento: ", os.Args[1])
    fmt.Println("Segundo argumento: ", os.Args[2])
    fmt.Println("Path: ", os.Args[0])
 
    fmt.Println("Número de elementos dentro de os.Args: ", len(os.Args))
}

Programa que tome el nombre desde línea de comandos y salude:

(...)
 
func main() {
    var nombre string
 
    nombre = os.Args[1]
    nombre_dos, edad := "gandalf", 2019
 
    fmt.Println("Hola, gran", nombre, "!")
    fmt.Println("Mi nombre es ", nombre_dos)
    fmt.Println("Mi edad es ", edad)
    fmt.Println("Por cierto, ¡puedes pasar!") 
}

Lo compilamos:

go build -o saludo

Lo ejecutamos:

./saludo pepito

La salida:

Hola, gran pepito!
Mi nombre es gandalf
Mi edad es 2019
Por cierto, ¡puedes pasar!

Los valores que se guardan en Args son strings, así que si queremos realizar operaciones aritméticas tendremos que convertirlos en números. Para ello echamos mano del paquete strconv y su función ParseFloat():

// Programa que convierte pies a metros
package main
 
import (
    "os"
    "fmt"
    "strconv"
)
func main() {
    arg := os.Args[1]
 
    // La función strconv devuelve también un error, pero
    // no nos interesa, así que lo asignamos al identificador
    // vacío.
    pies, _ = strconv.ParseFloat(arg, 64)
 
    metros := pies * 0.3048
 
    fmt.Print("%f pies ess %f metros.\n", pies, metros
 
}

Salida por pantalla

fmt.Printf

Imprime por pantalla con el formato establecido.

(...)
 
func main() {
    var marca = "Google"
 
    fmt.Printf("%q\n", marca)
}

El primer argumento de la función indica el tipo de dato que vamos a querer imprimir y donde, y el segundo argumento es el dato a imprimir

  • s: string
  • c: caracteres
  • b: bits
  • f: decimales
  • g: decimales sin las partes innecesarias
  • q: string
  • d: enteros
  • t: bool.
  • T: tipo de variable
  • v: imprime cualquier valor

Por ejemplo, para imprimir el siguiente texto:

total: 1234 éxito: 1200 / 34

Lo haríamos:

(...)
total := 1234
exito := 1200
error := 34
 
fmt.Printf("total: %d éxito: %d / %d\n", total, exito, error)

En caso de los valores decimales, en Printf podemos indicar la precisión con la que se muestran:

(...)
 
pi := 3.1415
 
fmt.Printf("Pi vale: %f\n", pi)
fmt.Printf("Pi vale: %.2f\n", pi)

Salida:

Pi vale: 3.1415
Pi vale: 3.14

Secuencias de escape

  • \n: nueva línea
  • \\: \
  • \": “

Bytes, runas y cadenas

// Imprime una tabla de caracteres y sus valores en decimal
package main
 
import (
    "fmt"
    "strings"
)
 
func main() {
    start, stop := 'A', 'Z'
 
    fmt.Printf("%-10s %-10s\n%s\n", "literal", "dec", "hex", "encoded", strings.Repeat("-", 45))
 
    for n := start; n <= stop; n++ {
        fmt.Printf("%-10c %-10[1]d %-10[1]x % -12x\n", n, string(n))
    }
}

Cadenas

Tamaño

len(): devuelve el tamaño de una cadena.

Los caracteres Unicode pueden tener de 1 a 4 bytes, así que la función len() devuelve el número de bytes, no de caracteres:

(...)
nombre := "çeszc"
 
fmt.Println(len(nombre))
 
// 6 y no 5 porque 'ç' son 2 bytes

Si queremos saber el número de caracteres de una cadena hay que usar el paquete utf8:

import (
    "fmt"
    "unicode/utf8"
)
 
func main() {
    nombre := "çeszc"
 
    fmt.Println(utf8.RuneCountInString(nombre)) // 5
}

Una runa representa un caracter inglés y no inglés. Las runas también son llamados codepoints

Manipulación

Repetir cadenas:

package main
 
import (
    "fmt"
    "os"
    "strings"
)
 
func main() {
    msg := os.Args[1]
    l := len(msg)
 
    // 'Repeat' permite repetir una cadena X número de veces
    s := msg + strings.Repeat("!", l)
 
    // 'ToUpper' convierte en mayúsculas un string
    s = strings.ToUpper(s)
 
    fmt.Println(s)
}

Arrays

Colección de elementos. Tiene un tamaño fijo.

Los slices son como los arrays, pero su tamaño es variable.

var libros [4]string

Si queremos declarar e inicializar:

var libros [4]string{1, 3, 10, 23}

Para acceder a cada elemento del array lo hacemos por la posición que ocupa. Empezando por el 0:

libros[0]
 
libros[1]
 
libros[2]
for i := 0, i < len(libros); i++ {
    fmt.Println(libros[i])
}
 
// Con un for range:
for i:= range libros {
    fmt.Println(libros[i])
 
}

Arrays multidimensionales

Array que contiene otros arrays. Podemos imaginarlos como tablas o matrices.

[3]int{5, 6, 1}
 
[4]int{9, 8, 4}
 
// Multidimensional
[2][3]int{
    [3]int{5, 6, 1}
    [4]int{9, 8, 4}
}
 
// Go permite omitir el tipo de los arrays internos:
[2][3]int{
    {5, 6, 1}
    {9, 8, 4}
}

El primer índice [2] indica el tamaño del array multidimensional

Se pueden crear arrays indexados:

notas := [3]float65{
    0: 1.5
    1: 10.0
    2: 3
}

Go inicializa a 0 los elementos no inicializados:

notas := [...]float65{
    5: 1.5
}

Go creará un array de 6 elementos con valor 0. El sexto elemento valdrá 1.5

La notación […] permite que no especifiquemos implicitamente la longitud del array.

Slices

Colección de elementos de un mismo tipo. El tamaño de los slices es variable. Las slices no contiene ningún elemento sino que son una vista de un array, es decir, por debajo tienen un array.

Lo más habitual es trabajar con slices y no con arrays, salvo que de antemano se conozca el tamaño de los datos.

En tiempo de ejecución, se pueden añadir o quitar elementos a un slice.

var nums []int

A la hora de declarar un slice no definimos su tamaño, así que no tiene un tamaño fijo en tiempo de compilación lo cual permite que modamos modificarlo.

A diferencia de los arrays, el valor cero de un slice es nill

var nums []int // nums es nil; len(nums) es 0

Para crear e inicializar un slice se utiliza la función interna make:

// Creamos un slice de enteros y el tamaño que queremos que tenga
listado := make([]string, 3)

Internamente, esa instrucción con make se creará un array (espacio en memoria) y un slice que es una represetanción de ese array.

Añadir elementos

Se utiliza la función append()

append(slice, newElement[,newElement2,...] )

Ejemplos:

nums := []int{1, 2, 3}
 
nums = append(nums, 4)

Si queremos añadir un slice a otro, utilizamos el operador elipsis ():

nums := []int{1, 2, 3}
tens := []int{12, 13}
 
nums = append(nums, tens...)

Quitar elementos

Se utilizan expresiones

msg := []byte{'h', 'o', 'l', 'a'}
 
msg[0:2] // corta el slice desde la posición 0 hasta la 2 (no la incluye) -> hol
 
msg[:3] // ho

Slices multidimensionales

Un slice que contiene otros:

gastos := [][]int {
    {200, 100}
    {500}
    {50, 25, 75},
}

A diferencia de los arrays multidimensionales, cada slice interno puede tener una longitud diferente.

Maps

Pares de clave-valor (como los diccionarios de Python).

Si queremos crear estructuras más complejas con mapas (y otros tipos de datos), debemos usar structs

Tenemos dos formas de crearlos:

miMapa := map[string]int{}

Utilizando make:

b := make(map[string]float32)
 
b["mahou"] = 0.59
b["buckler"] = 0.60

Podemos inicializarlos:

misMascotas := map[string]int{
    "perro": 2,
    "gato": 1,
    "planta": 5,
}

Acceder a los valores

Podemos acceder a los valores a través de sus claves:

fmt.Println(misMascotas["perro"])

Eliminar elementos

delete(b, "buckler")

Si no queremos eliminar la clave sino modificar un valor:

misMascotas["perro"] = misMascotas["perro"] - 1 // misMascotas["perro"]  -= 1

Control de flujo

Condicionales

If

if <condicion> {
   <codigo>
}

La condición siempre debe evaluarse (reducirse) a un valor lógico (true o false)

Salvo que sea estrictamente necesario, en Go no hay que poner paréntesis en la condición del condicional

if <condicion> {
 
} else if <condicion> {
 
} else {
 
}

Switch

switch <expresion> {
    case <valor1>:
        <codigo>
    case <valor2>:
        <codigo>
 
    default:
        <codigo>
}

La cláusula default se ejecuta cuando la expresión del switch no está recogida en ninguno de los case.

En Go es posible añadir varias condiciones en los case:

switch ciudad {
    case "París", "Lyon":
        fmt.Println("Francia")
    case "Tokio":
        fmt.Println("Japón")
}

Bucles

En Go solo hay una sentencia para bucles (no hay while por ejemplo)

for

for var := valor; <condicion>; var++

Ejemplo:

(...)
 
for i := ; i <= 1000; i++ {
    sum += i
}
 
fmt.Println(sum)

Para tener el comportamiento de una sentencia while, usamos for de esta manera:

for <condicion> {
}

Para romper un bucle se usa la palabra reservada break:

for {
    if i > 5 {
        break
    }
    sum += i
    i++
 
}

Si queremos continuar un bucle, usamos la palabra reservada continue:

for {
    if i > 10 {
        break
    }
 
    // Comprobar si el número es par o no
    if i % 2 != 0 {
        i++
        continue
    }
 
    sum += i
    fmt.Println(sum)
    i++
}

En el código anterior, cuando se llega a continue, Go va al comienzo del bucle, sin ejecutar lo que viniese después de continue.

for range

Permite iterar fácilmente en arrays/slices.

import (
    "fmt"
    "strings"
)
 
func main() {
 
    palabras := strings. Fields("En un lugar de la Mancha")
 
    for i, v := range palabras {
        <codigo>
    }
  • i: índice
  • v: valor

Structs

Tipo compuesto que agrupa diferentes tipos de datos en un único tipo.

Structs nos permite representar conceptos declarando una especie de plantilla para un conjunto de datos relacionados.

Los structs de Go son como las clases en la Programación Orientada a Objetos.

Por ejemplo, podríamos crear un struct para personas, películas… Para el struct de personas, se compondría de los campos Nombre, Apellidos, Edad.

Creación

type NombreStruct  struct {
    Campo1 string
    Campo2 string
    Campo3 bool
}

Ejemplo:

type VideoGame struct {
    Title     string
    Genre     string
    Published bool
}
 
// Creamos un valor struct utilizando el creado anteriormente:
pacman := VideoGame {
    Title:     "Pac-Man",
    Genre:     "Arcade Game",
    Published: true
}

Podemos acceder a los campos de un struct y/o modificar sus valores mediante nombreStruct.campoStruct:

(...)
type person struct {
    name, lastname string
    age            int
}
 
//  Declaramos un nuevo struct
//  Recordemos que al no inicializarlo, Go lo
//  hace por nosotros.
var picasso person
 
// Modificamos los valores de sus campos
picasso.name = "Pablo"
picass.lastname = "Picasso"
picasso.age = 91
 
fmt.Println("\nPicasso: %v\n", picasso)

Un struct puede contener otro struct:

type song struct {
    title, artist string
}
 
type playlist struct {
    genre string
    songs []song
}
 
songs := []song{
  {title: "Wonderwall", artist: "Oasis"},
  {title: "Super Sonic", artist: "Oasis"},
}
 
rock := playlist{genre: "indie rock", songs: songs}

Volviendo a la comparación con POO, en Go no existe la herencia sino más bien la composición: un struct puede componerse de otros structs. Es lo que en Go llaman embedding.

Los campos anónimos nos ahorran código a la hora de usar structs dentro de structs. Por ejemplo:

type text struct {
    title string
    words int
}
 
 
type book struct {
    text text
    isbn string
}
/*
// Lo de arriba se podría escribir:
type book struct {
    text
    isbn string
}
 
*/
 
moby := book {
    text: text {title: "Moby Dick", words: 206052,
    isbn: "102030",
}

Con los campos anónimos, Go toma el nombre del tipo del struct al que hacen referencia, text en este caso.

Otro ejemplo completo:

package main
 
import "fmt"
 
type Mascota struct {
    Edad int,
    Tipo string
}
 
func main () {
 
    misMascotas := map[string]Mascota{
        "Bobby": Pet{
            Edad: 12,
            Tipo: "Perro"
        },
        "Slow": Pet{
            Edad: 20,
            Tipo: "Tortuga"
        },
    }
 
    fmt.Println(misMascotas)
}

Codificación JSON

Los motivos por los que se usa JSON para el intercambio de datos:

  • Legible (por los humanos)
  • Los ordenadores lo pueden entender fácilmente.
  • Ampliamente usado y soportado
type permissions map[string]bool
 
type user struct {
    Name string
    Password string
    Permissions permissions
}
 
func main() {
 
    users := []user{
        {"god", "42", permissions{"admin", true}},
        {"devil", "666", permissions{"write", }},
 
    }
 
    out, err := json.MarshallIndent(users, "", "\t")
 
    if err != nil {
        fmt.Println(err)
        return
    }
 
    fmt.Println(string(out))
 
}

Si queremos cambiar el nombre de las claves de JSON:

type user struct {
    Name        string      `json: "username"`
    Password    string      `json: "-"`
    Permissions permissions `json: "permis"`
}

Interface

Tipo de dato no concreto.

Los tipos structs indican qué es algo mientras que los tipos de datos interface indican qué puede hacer.

Completar la explicación con ejemplos claros

Goroutines y channels

Mucha de la programación que se hace es secuencial. Las rutinas de Go y los canales permiten realizar operaciones mientras otro código está en ejecución. Son como los hilos.

Los canales se comunican entre las rutinas de go. Las rutinas necesitan comunicarse para saber cuándo finalizan y saber si deben detenerse o continuar.

Un ejemplo de uso sería una rutina para leer varios ficheros. En lugar de abrir uno, leer todo el contenido, cerrar el fichero y proceder con el siguiente, se puede hacer todo esto al mismo tiempo. Los canales pueden avisar cuándo se ha terminado de leer todos los ficheros.

Otro ejemplo sería contar la duración de la ejecución de una función. Se podría lanzar una goroutine con un contador de tiempo para que guarde la duración de la función. Estos dos procesos se ejecutan a la vez y uno de ellos está esperando a que el otro termine.

func goRoutinesAndChannels() {
    files := []string{
        "file1.csv",
        "file2.csv",
        "file3.csv",
    }
 
    rowChannel := make(chan int, len(files))
 
    for _, file := range files {
        file := file // this is stupid, but we have to because closures. big go pitfall!
 
        go func() {
            numRows, err := countRows(file)
            if err != nil {
                panic("aaaaah!")
            }
 
            rowChannel <- numRows
        }()
    }
 
    var totalRows int
    for range files {
        numRows := <- rowChannel
        totalRows += numRows
    }
 
    return totalRows
}

Punteros

Manejo de errores

Es necesario un manejo de los posibles errores:

  • Problemas con la red
  • Problemas con el acceso a ficheros
  • Entrada del usuario

nil es un valor que indica que algo no tiene valor, que aún no ha sido inicializado.

La función strconv.Atoi se define como:

func Atoi(s string) (int, error)

Así que a veces devuelve un error. Podemos usar 'nil' para el manejo de errores:

package main
 
import (
    "os"
    "fmt"
    "strconv"
)
 
func main() {
    n, err := strconv.Atoi(os.Args[1])
    fmt.Println("Número convertido: ", n)
    fmt.Println("Error: ", err)
}

Si lanzamos el programa con un valor correcto:

go run main.go 42

Salida:

Número convertido: 42
Error: <nil>

Cuando lanzamos el programa con un valor incorrecto:

go run main.go hola

Salida:

Número convertido: 0
Error: strconv.Atoi: parsing "hola": invalid syntax

Modificación del programa para controlar errores:

package main
 
import (
    "os"
    "fmt"
    "strconv"
)
 
func main() {
    n, err := strconv.Atoi(os.Args[1])
 
    if err != nil {
        fmt.Println("ERROR: ", err)
        return // finaliza la ejecución
    }
 
    fmt.Println("Número convertido: ", n)
    fmt.Println("Error: ", err)
 
}

Bibliotecas

Las más usadas:

  • Testify (testing)
  • Logrus (log): como un estándar para logs. La estándar no es suficiente.
  • pkg/errors (errors)
  • cobra (cli): el cliente de Docker está hecho usando este framework
  • godog (testing)

Frameworks

Utilidades para APIs

  • Gorilla mux: muy usada para montar APIs. Resuelve todo el tema de routing
  • Negroni
  • google/jsonapi
  • grpc
  • echo

Frameworks para Web-Scraping

  • Pholcus (Chino)
  • go_spider
  • ants-go
  • colly
  • Dataflow Kit

Aplicaciones hechas en Go

Recursos

informatica/programacion/go.txt · Última modificación: por tempwin