Tabla de Contenidos
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
- 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
packagedebe ser lo primero que escribamos en nuestro código fuente. maines el nombre del paquete al que pertenece el archivo que creamos.funcdefine una función, un bloque de código reutilizable.maines 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:
breakdefaultfuncinterfaceselectcasedefergomapstructchanelsegotopackageswitchconstfallthroughifrangetypecontinueforimportreturnvar
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
intint32: 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-.50.00.1.01.
Lógicos
bool
truefalse
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:
boolint(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)stringfloat32,float64complex64complex128
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 variablenombrees 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 comobooly se asigna atruevelocidad: se autodeclara comointy 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ódigofmt: 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.goerrors.goformat.goprint.goscan.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 programaArgs[1]: primer argumentoArgs[n]: argumento que ocupa la posiciónn.
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: stringc: caracteresb: bitsf: decimalesg: decimales sin las partes innecesariasq: stringd: enterost: bool.T: tipo de variablev: 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: índicev: 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
- The Go Playground: prueba o comparte código desde el navegador.
- Friends of Go (en español)
- https://forum.golangbridge.org: foro oficial
- Gophercises: Coding exercises for budding gophers (requiere crearse una cuenta)
