Apuntes del lenguaje de programación Go

Advertencia
Este artículo se actualizó por última vez el 2023-03-09, es posible que el contenido no esté actualizado.
Work in progress
Apuntes incompletos del lenguaje de programación Go

Referencias

Books

Instalación

  • Bajamos el paquete con la última versión desde la página oficial.
  • Y lo descomprimimos como root en /usr/local/
1
2
3
4
cd ~/tmp/go
wget https://go.dev/dl/go1.17.5.linux-amd64.tar.gz
sudo cp /usr/local/go /usr/local/go.bkp
sudo tar -C /usr/local -xvzf go1.17.4.linux-amd64.tar.gz

Después de instalar es importante ajustar las rutas en .profile (ver la explicación en el siguiente punto):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# golang
if [ -d "/usr/local/go" ] ; then
#    export GOROOT="/usr/local/go"
#    PATH="$PATH:$GOROOT/bin"
fi
if [ -d "$HOME/go" ] ; then
    export GOPATH="$HOME/go"
    PATH="$PATH:$GOPATH/bin"
fi

export PATH
GOROOT
Varios autores (un ejemplo) recomiendan NO establecer el valor de GOROOT cuando usamos la localización estándar. Así que he comentado esa parte en mi fichero ~/.profile

Configuración del entorno de Go

Para dejar configurado el entorno de trabajo en Go conviene conocer las siguientes variables de entorno (aunque hay más):

GOROOT

Es el directorio donde se localiza la biblioteca estándar de Go. Por defecto Go asume la ruta /usr/local/go, así que estrictamente hablando NO es necesario establecer esta variable si has instalado en /usr/local. (Con retoques de esta variable es posible instalar varias versiones de Go)

GOBIN

Es el directorio donde quedan instalados los binarios de Go cuando ejecutamos un comando go install .... Solo deberías tocar esta variable si no quieres instalar los binarios en la localización por defecto: $GOPATH/bin.

GOOS

especifica el sistema operativo, es opcional. En mi caso tiene valor linux

GOARCH

especifica la arquitectura del procesador de nuestra máquina, también es opcional, en mi caso es amd64

GOPATH

La ruta a nuestro directorio de trabajo (nuestro workspace). Esta variable es obligatoria y no tiene valor por defecto, podeis ponerla en donde tenga sentido para vosotros, por ejemplo /home/salvari/code/go

Evidentemente tenemos que dejar las variables de entorno exportadas y añadir al PATH de nuestro usuario las que correspondan (ver punto anterior)

Ver el entorno
Puedes ver el entorno completo de Go con el comando go env

Más de GOPATH

Abandonando GOPATH

Estamos usando ya go 1.17. Hay planes firmes para abandonar el tratamiento de dependencias via GOPATH y centrarse exclusivamente en gestionarlas a través de módulos.

Todo lo que comento aquí de GOPATH hay que tratarlo con pinzas.

Go espera que en el directorio $GOPATH haya tres subdirectorios:

1
2
3
bin/
pkg/
src/
  • bin/ contiene los ejecutables que se generan cuando ejecutamos go install ...
  • pkg/ contiene los paquetes instalados en nuestro sistema con go get ...
  • src/ aquí es donde deberíamos alojar nuestro código fuente (no es obligatorio)

Como hemos dicho en $GOPATH/src/ es donde tenemos que programar, ahí es donde tiene que estar nuestro código fuente, pero ojo la ruta exacta de nuestros proyectos es función de nuestra plataforma de control de código, y tiene que tener la forma Source-Control-Platform/User/Repository.

Por ejemplo podríamos tener los siguientes proyectos en el directorio src/:

1
2
src/github.com/salvari/miAppGo
src/gilab.com/salvari/mach5

Instalación de herramientas

golint y godoc

Ya no instalamos con los comandos:

1
2
go get -u golang.org/x/lint/golint
go get -u  golang.org/x/tools/cmd/godoc

El comando go get se usa exclusivamente para añadir dependencias al módulo que estemos creando.

Los binarios que queremos usar en nuestro sistema se instalan con el nuevo estilo de instalación recomendado desde go v1.17:

1
2
go install golang.org/x/lint/golint@latest
go install golang.org/x/tools/cmd/godoc@latest

Alternativamente podemos instalar todas las herramientas de Go con:

1
go get -u golang.org/x/tools/...

Pero con esta opción me falla la instalación de gopls, una herramienta que necesito en mi configuración de Emacs

gopls para protocolos LSP en editores

Desde un directorio que no sea el GOPATH

1
2
# GO111MODULE=on  # not needed with 'go install'
go install golang.org/x/tools/gopls@latest

Herramientas adicionales

  • gopkgs
  • go-outline
  • dlv
  • dlv-dap
  • staticcheck
1
2
3
4
5
6
go install github.com/uudashr/gopkgs/v2/cmd/gopkgs@latest
go install github.com/ramya-rao-a/go-outline@latest
go install github.com/go-delve/delve/cmd/dlv@latest
go install github.com/go-delve/delve/cmd/dlv@master
go install honnef.co/go/tools/cmd/staticcheck@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

Alias para zsh

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# golang aliases
alias gob='go build'        # Build your code
alias goc='go clean'        # Removes object files from package source directories
alias god='go doc'          # Prints documentation comments
alias gof='go fmt'          # Gofmt formats (aligns and indents) Go programs.
alias gofa='go fmt ./...'   # Run go fmt for all packages in current directory, recursively
alias gog='go get'          # Downloads packages and then installs them to $GOPATH
alias goi='go install'      # Compiles and installs packages to $GOPATH
alias gol='go list'         # Lists Go packages
alias gom='go mod'          # Access to operations on modules
alias gop='cd $GOPATH'      # Takes you to $GOPATH
alias gopb='cd $GOPATH/bin' # Takes you to $GOPATH/bin
alias gops='cd $GOPATH/src' # Takes you to $GOPATH/src
alias gor='go run'          # Compiles and runs your code
alias got='go test'         # Runs tests
alias gov='go vet'          # Vet examines Go source code and reports suspicious constructs

Go Modules

Básicamente un módulo (Go Module) es una colección de paquetes (packages) almacenados en un arbol de directorios que tiene un fichero go.mod en su raiz. El fichero go.mod especifica el module path que es la ruta canónica al módulo que se usará para importarlo, y todas las depencias requeridas por el módulo, es decir el conjunto de módulos (otros módulos) que serán necesarios para compilar con éxito este módulo. Cada dependencia se especifica con su correspondiente module path y un número de versión semántica (semantic version)

Crear un módulo

El comando para crear un módulo es: go mod init

Antiguamente el compilador distinguía dos formas de funcionamiento, el go module mode y el gopath mode. A estas alturas tenemos que usar el go module mode el otro está en desuso. De hecho el compilador ahora funciona en go module mode por defecto.

Si estamos en un directorio por debajo del $GOPATH nuestro directorio de proyecto debería tener esta forma:

1
$GOPATH/src/example.com/$USER/moduleName

En este caso el comando go mod init no necesita parámetros, genera un module path basado en la estructura de directorios.

Si estamos trabajando en un directorio fuera del $GOPATH tendremos que especificar el module path en el comando:

1
go mod init example.com/modulename

Actualizar el fichero .mod

Ejecutamos: go mod tidy

TDD (Test Drived Development) en Go

Referencias

TDD es una buena práctica:

  • Mejora la calidad del código
  • En general hace que escribamos un código más desacoplado (tiendes a aislar más las cosas para facilitar los test)
  • Tiene la ventaja obvia de tener todo controlado con test fáciles de repetir
  • Bien escritos valen como documentación de bajo nivel
  • Previene regresiones (fallos que reaparecen al avanzar en el desarrollo)
  • Potencia la arquitectura y diseño modulares

Tres reglas

Se suele hablar del ciclo: ““rojo, verde y refactorizar” (red, green and refactor)

  • Primero hay que escribir un test que falla
  • Hay que programar lo justo para que el test no falle. No se puede escribir más código que el necesario para que el test no falle.
  • Se revisa el código para refinarlo (tanto el código producto como el código de los test)

Los pasos que nos proponen en la página de Learn Go with Test:

  • Escribe un test
  • Haz lo necesario para que el compilador pase sin errores
  • Ejecuta el test, comprueba que falla y que el mensaje de fallo es significativo
  • Escribe el código suficiente para que el test no falle
  • Refina el código (Refactor)

Nos dicen literalmente:

Este ciclo puede parecer tedioso pero es importante mantenerlo.

Al hacerlo no solo te aseguras de tener test significativos, sino que aseguras un buen diseño del software respaldado por la seguridad que dan los test.

Ver fallar el test es un paso importante por qué permite comprobar el mensaje de error (que debe ser significativo) Como desarrollador puede ser muy difícil trabajar con código cuando los mensajes de fallo de los test no dan una idea clara de que está pasando.

Asegurándote de que los test se ejecutan rápido y estableciendo un entorno de trabajo que facilite escribir los test puedes "entrar en sintonía" y programar de la forma más eficiente posible.

Si no escribes los test, te verás obligado a comprobar el funcionamiento del código ejecutándolo manualmente, eso rompera tu concentracion y a la larga te hara perder bastante mas tiempo que escribir correctamente los test.

Sintáxis para test

Escribir un test es como escribir una función pero:

  • Tiene que estar en un fichero de la forma xxx_test.go
  • El nombre de la función que implementa el test tiene que empezar por Test
  • La función que implementa el test tiene que tener un único argumento: t *testing.T
  • Para poder usar ese tipo de argumento y otras facilidades de testeo es necesario hacer import "testing"

Código independiente es más fácil de testear

En el ejemplo Hello World es mejor escribir una funcion que devuelve la cadena con el saludo que una función que escribe el saludo. Por que así somos independientes de la salida que puede ser por pantalla, por una página web, por un mensaje, escrita, etc. etc. Y de paso es más simple de testear.

Go

_

Es el black identifier podemos usarlo para pasar de variables que no vamos a usar.

1
2
3
4
a = "Una cadena"
for _, r := range a {
    fmt.Println(r)
}
make

Parece que vale para hacer un alloc de memoria.

1
counts := make(map[string]int)
time

La especificación de formatos tiene su gracia ¬_¬ ver referencia

Arrays

1
2
3
4
5
6
7
8
var myArray1 = [3]int            // will be filled with so called
                                 // zero values, for integers: 0

var myArray2 = [5]int{1,2,3,4,5} // number of values between { } can
                                 // not be larger than size (ofc)

var myArray3 = []int{1,2,3,4}   // the compiler will count the
                                 // array elements for you

Go Cheat Sheet

Copia descarada de esto

Crédito

La mayor parte de lo que se cuenta en esta sección está copiado de golang-cheat-sheet

Los créditos originales avisan de que la mayor parte de los ejemplos tomados de A Tour of Go, una excelente introducción al lenguage Go

Go en resumen

  • Lenguaje Imperativo
  • Tipado estático
  • Sintáxis muy similar a la de C (pero con menos paréntesis y sin punto y coma obligatorio) la estructura es parecida a la de Oberon-2
  • Compila a código nativo (nada de vm)
  • No hay clases, pero tiene structs con métodos
  • Interfaces
  • No implementa herencia. Hay algo llamado type embedding
  • Las Funciones son ciudadanos de primera clase
  • Las Funciones pueden devolver múltiples valores
  • Tiene closures
  • Hay Punteros pero sin aritmética de punteros
  • La concurrencia es “Built-in” mediante las primitivas: Goroutines y Channels

Sintáxis básica

Hello World

Fichero hello.go:

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
    fmt.Println("Hello Go")
}

$ go run hello.go

Operadores

Aritméticos

Operador Descripción
+ addition
- subtraction
* multiplication
/ quotient
% remainder
& bitwise and
| bitwise or
^ bitwise xor
&^ bit clear (and not)
<< left shift
>> right shift

Comparación

Operador Descripción
== equal
!= not equal
< less than
<= less than or equal
> greater than
>= greater than or equal

Lógicos

Operador Descripción
&& logical and
|| logical or
! logical not

Otros

Operador Descripción
& address of / create pointer
* dereference pointer
<- send / receive operator (see ‘Channels’ below)

Declaraciones

El tipo va después del indentificador de variable

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var foo int                 // declaración sin inicialización
var foo int = 42            // declaración con inicialización
var foo, bar int = 42, 1302 // declaración e inicialización múltiples
var foo = 42                // se omite el tipo, será inferido
foo := 42                   // abreviado, sólo es válido en el cuerpo de las funciones,
                            // el tipo siempre es implícito
const constant = "Esto es una constante"

// iota se puede usar para números que se incrementan, empezando por cero
const (
    _ = iota
    a
    b
    c = 1 << iota
    d
)
    fmt.Println(a, b) // 1 2 (0 is skipped)
    fmt.Println(c, d) // 8 16 (2^3, 2^4)

Funciones

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// una función simple
func functionName() {}

// función con parámetros (aquí también va el tipo después de la variable)
func functionName(param1 string, param2 int) {}

// multiples parámetros del mismo tipo
func functionName(param1, param2 int) {}

// podemos especificar el tipo devuelto por la función
func functionName() int {
    return 42
}

// Pueden devolver multiples valores
func returnMulti() (int, string) {
    return 42, "foobar"
}
var x, str = returnMulti()

// si los  valores devueltos tienen nombre no hace falta especificarlos en el Return
func returnMulti2() (n int, s string) {
    n = 42
    s = "foobar"
    // n and s will be returned
    return
}
var x, str = returnMulti2()

Funciones como valores y closures

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func main() {
    // asignar una función a una variable
    add := func(a, b int) int {
        return a + b
    }
    // usar el nombre para llamar a la función
    fmt.Println(add(3, 4))
}

// 'Closures', lexically scoped: Las funciones pueden acceder valores que estában dentro del alcance (scope)
// cuando se definió la función

func scope() func() int{
    outer_var := 2
    foo := func() int { return outer_var}
    return foo
}

func another_scope() func() int{
    // won't compile because outer_var and foo not defined in this scope
    outer_var = 444
    return foo
}


// Closures
func outer() (func() int, int) {
    outer_var := 2
    inner := func() int {
        outer_var += 99 // outer_var from outer scope is mutated.
        return outer_var
    }
    inner()
    return inner, outer_var // return inner func and mutated outer_var 101
}

Variadic Functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func main() {
	fmt.Println(adder(1, 2, 3)) 	// 6
	fmt.Println(adder(9, 9))	// 18

	nums := []int{10, 20, 30}
	fmt.Println(adder(nums...))	// 60
}

// Usando ... antes del nombre del tipo del último parámetro indicamos que la función acepta cero o mas parámetros de ese tipo
// La función se usa como cualquier otra, excepto que podemos pasar tantos parámetros como queramos del último parámetro definido
func adder(args ...int) int {
	total := 0
	for _, v := range args { // Iterates over the arguments whatever the number.
		total += v
	}
	return total
}

Tipos Built-in

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // alias for uint8

rune // alias for int32 ~= a character (Unicode code point) - very Viking

float32 float64

complex64 complex128

Conversión de tipos

1
2
3
4
5
6
7
8
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

// alternative syntax
i := 42
f := float64(i)
u := uint(f)

Paquetes (Packages)

Las unidades de encapsulado en Go de menor a mayor nivel serían:

  • Funciones
  • Paquetes
  • Módulos

Los paquetes nos permiten organizar los ficheros de código para hacerlos modulares y reutilizables además de facilitar el mantenimiento del software.

  • Cada fichero de código Go debe pertenecer a un paquete. La pertenencia se declara con package al principio de cada uno de los ficheros fuente, evidentemente un paquete puede “poseer” varios ficheros.
  • Los ejecutables están en el paquete main. Es un paquete especial.
  • Por convención los programas ejecutables (los del paquete main) se llaman comandos (commands). El resto se llaman simplemente paquetes (packages)
  • Por convención: <nombre_del_paquete> == último nombre del import path (import path math/rand => package rand) Lo normal es que todos los ficheros del paquete rand se guarden en el directorio rand/
  • Si el identificador empieza con mayúscula: se exporta el símbolo (será visible desde otros paquetes)
  • Si el identificador empieza con minúscula: privado (no será visible desde otros paquetes)

Los módulos son coleciones de paquetes. Son imprescindibles para crear nuestros propios paquetes por qué la ruta de nuestros paquetes viene dada por el módulo. En cuanto quieras empezar a organizar tu código en paquetes inevitablemente tendrás que crear también módulos.

El flujo típico de trabajo para un proyecto estructurado con Packages sería el siguiente:

  • Supongamos que nuestro proyecto será un “pomodoro” y que va a gestionar Timers y Notifications (por decir algo)
  • Creamos el directorio del proyecto que vamos a llamar pomodoro, dentro de ese directorio tendremos el package main con los “comandos”.
    1
    2
    3
    
    mkdir pomodoro
    cd pomodoro
    touch main.go
  • Iniciamos el modulo en la raiz del proyecto
    1
    2
    3
    4
    
    go mod init pomodoro
    
    # Aunque lo mas correcto sería iniciarlo como
    go mod init gitlab/salvari/pomodoro
  • Creamos los subdirectorios para los packages: timer y notification. Dentro de los subdirectorios creamos un fichero de código (pueden ser tantos ficheros como queramos)
    1
    2
    3
    4
    
    mkdir timer
    touch timer/timer.go
    mkdir notification
    touch notification/notification.go

Tendremos una estructura de directorios:

1
2
3
4
5
6
7
pomodoro
├── go.mod
├── main.go
├── notification
│   └── notification.go
└── timer
    └── timer.go

El contenido del fichero go.mod será (la versión de go depende de lo que tengas instalado):

1
2
3
module pomodoro

go 1.17

Cada fichero con extensión .go debe empezar siempre con la declaración del package. En nuestro caso main.go declarará package main y por ejemplo notification.go declarará package notification.

Ya tenemos todo estructurado, ahora en nuestros ficheros de código podremos hacer imports de este estilo

1
2
import "pomodoro/timer"
import "pomodoro/notification"

Gestión de dependencias con módulos en Go

https://go.dev/blog/go116-module-changes https://stackoverflow.com/questions/53368187/go-modules-installing-go-tools/57317864#57317864

Control del flujo de programa

If

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
	// Basic one
	if x > 10 {
		return x
	} else if x == 10 {
		return 10
	} else {
		return -x
	}

	// You can put one statement before the condition
	if a := b + c; a < 42 {
		return a
	} else {
		return a - 42
	}

	// Type assertion inside if
	var val interface{}
	val = "foo"
	if str, ok := val.(string); ok {
		fmt.Println(str)
	}
}

Bucles

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
    // Solo hay bucle `for`, no hay `while`, ni `until`
    for i := 1; i < 10; i++ {
    }
    for ; i < 10;  { // bucle while
    }
    for i < 10  { // se pueden omitir los ; si solo hay una condición
    }
    for { // Si omitimos la condición tenemos un  while (true)
    }

    // Podemos usar use break/continue en el bucle activo
    // o usar break/continue con etiquetas (para bucles más externos)
here:
    for i := 0; i < 2; i++ {
        for j := i + 1; j < 3; j++ {
            if i == 0 {
                continue here
            }
            fmt.Println(j)
            if j == 2 {
                break
            }
        }
    }

there:
    for i := 0; i < 2; i++ {
        for j := i + 1; j < 3; j++ {
            if j == 1 {
                continue
            }
            fmt.Println(j)
            if j == 2 {
                break there
            }
        }
    }

Switch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    // switch statement
    switch operatingSystem {
    case "darwin":
        fmt.Println("Mac OS Hipster")
        // cases break automatically, no fallthrough by default
    case "linux":
        fmt.Println("Linux Geek")
    default:
        // Windows, BSD, ...
        fmt.Println("Other")
    }

    // al igual que con el 'for' y el 'if' podemos tener una sentencia de asignación justo antes de la variable del switch
    switch os := runtime.GOOS; os {
    case "darwin": ...
    }

    // se pueden hacer comparaciones en los casos del switch
    number := 42
    switch {
        case number < 42:
            fmt.Println("Smaller")
        case number == 42:
            fmt.Println("Equal")
        case number > 42:
            fmt.Println("Greater")
    }

    // los casos pueden ser listas de valores separados por comas
    var char byte = '?'
    switch char {
        case ' ', '?', '&', '=', '#', '+', '%':
            fmt.Println("Should escape")
    }

Arrays, Slices, Ranges

Arrays

  • Tienen longitud fija. Los arrays de longitudes son tipos diferentes
  • No pueden redimensionarse
  • El índice empieza en cero
  • Un array no es un puntero en Go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var a [10]int // declara un array de enteros (int) con longitud 10. ¡La longitud determina el tipo!
a[3] = 42     // establece el valor de un elemento
i := a[3]     // lee el valor de un elemento

// array literals
var a = [2]int{1, 2}
a := [2]int{1, 2} //shorthand
a := [...]int{1, 2} // elipsis -> El compilador infiere la longitud del array

// no hay paso por referencia
arr1 := arr2  // Se hace una copia
arr1 := &arr2 // Se copia la referencia

// multidimensionales
// var variable_name [SIZE1][SIZE2]…[SIZEN] variable_type

a := [3][4]int{
    {0, 1, 2, 3} , // initializers for row indexed by 0
    {4, 5, 6, 7} , // initializers for row indexed by 1
    {8, 9, 10, 11} // initializers for row indexed by 2}

Slices

  • Un slice es una abstracción apuntando a un array.
  • Se pueden crear a partir de un array existente, en caso contrario Go creará el array detrás de las bambalinas
  • No se especifica la dimensión
  • Realmente un slice son tres datos:
    • Un puntero a la secuencia de datos en memoria
    • Una longitud (lenght, len(a)) que almacena el número de elementos
    • Una capacidad (capacity, cap(a)) que es el total de posiciones reservadas en memoria
  • Cuando se asigna un slice en realidad se copian esos valores, así que se copia la referencia
  • Si un slice tiene que crecer el compilador normalmente es conservador y duplica la capacidad. Eso implicará re-localizaciones del array subyacente en memoria
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
var a []int                              // declare a slice - similar to an array, but length is unspecified
var a = []int {1, 2, 3, 4}               // declare and initialize a slice (backed by the array given implicitly)
a := []int{1, 2, 3, 4}                   // shorthand
chars := []string{0:"a", 2:"c", 1: "b"}  // ["a", "b", "c"]


var b = a[lo:hi]	// creates a slice (view of the array) from index lo to hi-1
var b = a[1:4]		// slice from index 1 to 3
var b = a[:3]		// missing low index implies 0
var b = a[3:]		// missing high index implies len(a)
a =  append(a,17,3)	// append items to slice a
c := append(a,b...)	// concatenate slices a and b

// create a slice with make
a = make([]byte, 5, 5)	// first arg length, second capacity
a = make([]byte, 5)	// capacity is optional

// create a slice from an array
x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // a slice referencing the storage of x

Operaciones sobre Arrays y Slices

len(a) devuelve la longitud de un array o slice a. Es un built-in no un atributo o método del array.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// iterar sobre un array o slice
for i, e := range a {
    // i es el  index, e es el elemento
}

// si no vamos a usar el índice:
for _, e := range a {
    // e es el elemento
}

// si solo queremos usar el índice
for i := range a {
}

// Desde Go 1.4 se puede iterar sin variables:
for range time.Tick(time.Second) {
    // hacer algo cada segundo
}

Maps

  • Los mapas (maps) son inmutables
  • No se garantiza ningún orden
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
var m map[string]int
m = make(map[string]int)
m["key"] = 42
fmt.Println(m["key"])

delete(m, "key")

elem, ok := m["key"] // test if key "key" is present and retrieve it, if so

// map literal
var m = map[string]Vertex{
    "Bell Labs": {40.68433, -74.39967},
    "Google":    {37.42202, -122.08408},
}

// iterate over map content
for key, value := range m {
}

Structs

No hay clases en Go, solo structs. Las structs pueden tener métodos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Una struct es un tipo. También es una colección de campos

// Declaración
type Vertex struct {
    X, Y int
}

// Creación
var v = Vertex{1, 2}
var v = Vertex{X: 1, Y: 2}          // Creación de la struct definiendo sus valores con clave
var v = []Vertex{{1,2},{5,2},{5,5}} // Creación de un slice de structs

// Acceso a miembros de la struct
v.X = 4

// Se pueden declarar métodos sobre structs. La referencia a la struct que recibe el método
// tiene que ir entre la palabra clave 'func' y el nombre del método
// LA ESTRUCTURA SE COPIA EN CADA LLAMADA AL MÉTODO
func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// Invocando el método
v.Abs()

// Para los métodos que mutan la estructura usamos punteros a esa struct
// como tipos. De esta forma EVITAMOS LA COPIA de la estructura al invocar el método
func (v *Vertex) add(n float64) {
    v.X += n
    v.Y += n
}

Tenemos un caso especial con los contructores. Si queremos evitar que se construyan objetos sin usar el constructor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package matrix

type matrix struct {             // NO EXPORTADO
 ....
}
func NewMatrix(rows, cols int) *matrix {
    m := new(matrix)
    m.rows = rows
    m.cols = cols
    m.elems = make([]float, rows*cols)
    return m
}

Como no exportamos la estructura base, solo se pueden construir nuevos objetos a través del constructor.

La función NewMatrix puede simplificarse fácilmente:

1
2
3
func NewMatrix(rows, cols, int) *matrix {
    return &matrix{rows, cols, make([]float, rows*cols)}
}
structs y exportacion
Si el nombre de la struct o de alguno de sus campos empieza por minúscula no serán visibles fuera de la propia struct

Anonymous structs:

Más económicas y seguras que usar: map[string]interface{}.

1
2
3
point := struct {
	X, Y int
}{1, 2}

Punteros

1
2
3
4
5
6
7
p := Vertex{1, 2}  // p es de tipo Vertex
q := &p            // q es un puntero a un Vertex
r := &Vertex{1, 2} // r también es un puntero a un Vertex

// El tipo de un puntero a un Vertex es *Vertex

var s *Vertex = new(Vertex) // new crea un puntero a una nueva instancia de Vertex

Interfaces

  • Un interface es una especie de “contrato”.
  • Dentro del interface especificamos las funciones que tiene que soportar un tipo para satisfacer el interface
  • El interface nos da una capa extra de abstracción, podemos definir una función que procese o devuelva el “tipo” definido por el interface
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// declaración
type Awesomizer interface {
    Awesomize() string
}

// los tipos no declaran en ningún sitio implementar un interface
type Foo struct {}

// los tipos que implementan todos los métodos de un interface, satifacen ese interface implicitamente
func (foo Foo) Awesomize() string {
    return "Awesome!"
}

Embedding

There is no subclassing in Go. Instead, there is interface and struct embedding.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// ReadWriter implementations must satisfy both Reader and Writer
type ReadWriter interface {
    Reader
    Writer
}

// Server exposes all the methods that Logger has
type Server struct {
    Host string
    Port int
    *log.Logger
}

// initialize the embedded type the usual way
server := &Server{"localhost", 80, log.New(...)}

// methods implemented on the embedded struct are passed through
server.Log(...) // calls server.Logger.Log(...)

// the field name of the embedded type is its type name (in this case Logger)
var logger *log.Logger = server.Logger

Errores

No hay gestión de excepciones en Go. Las funciones que pueden lanzar un Error simplemente devuelven un valor adicional de tipo Error.

El interface Error tiene esta pinta:

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

Una función que puede devolver un error:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func doStuff() (int, error) {
}

func main() {
    result, err := doStuff()
    if err != nil {
        // handle error
    } else {
        // all is good, use result
    }
}

Concurrency

Goroutines

Goroutines are lightweight threads (managed by Go, not OS threads). go f(a, b) starts a new goroutine which runs f (given f is a function).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// just a function (which can be later started as a goroutine)
func doStuff(s string) {
}

func main() {
    // using a named function in a goroutine
    go doStuff("foobar")

    // using an anonymous inner function in a goroutine
    go func (x int) {
        // function body goes here
    }(42)
}

Channels

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ch := make(chan int) // create a channel of type int
ch <- 42             // Send a value to the channel ch.
v := <-ch            // Receive a value from ch

// Non-buffered channels block. Read blocks when no value is available, write blocks until there is a read.

// Create a buffered channel. Writing to a buffered channels does not block if less than <buffer size> unread values have been written.
ch := make(chan int, 100)

close(ch) // closes the channel (only sender should close)

// read from channel and test if it has been closed
v, ok := <-ch

// if ok is false, channel has been closed

// Read from channel until it is closed
for i := range ch {
    fmt.Println(i)
}

// select blocks on multiple channel operations, if one unblocks, the corresponding case is executed
func doStuff(channelOut, channelIn chan int) {
    select {
    case channelOut <- 42:
        fmt.Println("We could write to channelOut!")
    case x := <- channelIn:
        fmt.Println("We could read from channelIn")
    case <-time.After(time.Second * 1):
        fmt.Println("timeout")
    }
}
Channel Axioms
  • A send to a nil channel blocks forever

    1
    2
    3
    
    var c chan string
    c <- "Hello, World!"
    // fatal error: all goroutines are asleep - deadlock!
    
  • A receive from a nil channel blocks forever

    1
    2
    3
    
    var c chan string
    fmt.Println(<-c)
    // fatal error: all goroutines are asleep - deadlock!
    
  • A send to a closed channel panics

    1
    2
    3
    4
    5
    
    var c = make(chan string, 1)
    c <- "Hello, World!"
    close(c)
    c <- "Hello, Panic!"
    // panic: send on closed channel
    
  • A receive from a closed channel returns the zero value immediately

    1
    2
    3
    4
    5
    6
    7
    8
    
    var c = make(chan int, 2)
    c <- 1
    c <- 2
    close(c)
    for i := 0; i < 3; i++ {
        fmt.Printf("%d ", <-c)
    }
    // 1 2 0
    

Printing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fmt.Println("Hello, 你好, नमस्ते, Привет, ᎣᏏᏲ") // basic print, plus newline
p := struct { X, Y int }{ 17, 2 }
fmt.Println( "My point:", p, "x coord=", p.X ) // print structs, ints, etc
s := fmt.Sprintln( "My point:", p, "x coord=", p.X ) // print to string variable

fmt.Printf("%d hex:%x bin:%b fp:%f sci:%e",17,17,17,17.0,17.0) // c-ish format
s2 := fmt.Sprintf( "%d %f", 17, 17.0 ) // formatted print to string variable

hellomsg := `
 "Hello" in Chinese is 你好 ('Ni Hao')
 "Hello" in Hindi is नमस्ते ('Namaste')
` // multi-line string literal, using back-tick at beginning and end

Reflection

Type Switch

A type switch is like a regular switch statement, but the cases in a type switch specify types (not values), and those values are compared against the type of the value held by the given interface value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}

Snippets

HTTP Server

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
    "fmt"
    "net/http"
)

// define a type for the response
type Hello struct{}

// let that type implement the ServeHTTP method (defined in interface http.Handler)
func (h Hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello!")
}

func main() {
    var h Hello
    http.ListenAndServe("localhost:4000", h)
}

// Here's the method signature of http.ServeHTTP:
// type Handler interface {
//     ServeHTTP(w http.ResponseWriter, r *http.Request)
// }

Misc

  • Para ver el entorno usamos go env

TDD con Go

Para que funcionen correctamente los comandos go build, go clean y go test parece que es necesario haber definido correctamente go.mod (con go mod init)

go test buscará ficheros nombrados como *_test.go para ejecutarlos.

Dentro de esos de test habrá bloques funcionales con la firma: func TestXxxx(t *testing.T)

Los test fallidos se informan con t.Errorf

Ciclo de test

El ciclo de trabajo se resume en Red, Green, Refactor.

  • Para implementar una nueva funcionalidad el primer paso debe ser escribir los test que capturan los requisitos de esa nueva funcionalidad. Estos test obviamente fallarán (Red o rojo de fallo).
  • La segunda fase consiste en implementar el código necesario para cumplir con los test y queden todos en verde (Green). El código debe minimizarse para cumplir los test sin “sobreimplementar” nada.
  • El último paso es el refactoring. No implementamos nueva funcionalidad en este paso, se trata simplemente de hacer un código de calidad re-escribiendo el código de la segunda fase. No es un paso opcional, es imprescindible revisar el código y garantizar la coherencia y calidad del mismo.

Vicios en TDD

  1. Escribir demasiados test a la vez
  2. Concentrarse exclusivamente en los happy path y la cobertura del código
  3. No considerar los diferentes escenarios
  4. Demasiadas comprobaciones (asserts) en un sólo test
  5. Probar cosas diferentes en el mismo caso de prueba
  6. Escribir test triviales para mantener el código cubierto
  7. No ejecutar los test con frecuencia
  8. No seguir siempre las tres fases. Especialmente implementando código sin escribir antes el test
  9. Hacer asserts que no prueban nada
  10. Crear test difíciles de mantener

Dobles de prueba (test doubles)

Una implementación simplificada de algún tipo para facilitar las pruebas

Casos posibles:

Dummies

Tipos sin ningún comportamiento, se implementan únicamente para cumplir con la firma de alguna función que queremos probar

Stubs (“breves”)

Tipos que implementan el comportamiento mínimo para pasar un test

Mocks (parodias)

Implementaciones parciales que permiten definir como suponemos que serán los métodos sobre el tipo

Spies (espías)

Implementaciones parciales que nos permiten comprobar que métodos han sido invocados

Fakes (falsificaciones)

Implementaciones ligeras pero completas, por ejemplo implementar una base de datos en memoria a efectos de pruebas

Paquetes útiles

flag

Este paquete nos permite leer parámetros pasados en la llamada al programa por linea de comandos.

encoding/json

Este paquete nos permite salvar o recuperar estructuras de datos desde ficheros json

marshalling

El proceso de codificar nuestra estructura de datos en json.

unmarshalling

El proceso inverso al anterior para pasar de json a una estructura de datos

Arrays y Slices

Podemos codificar en json arrays o slices de Go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main

import (
	"encoding/json"
	"fmt"
)

type Book struct {
	Name   string
	Author string
}

func main() {

	//--------------------------------------------------
	// Marshalling
	book := Book{"C++ programming language", "Bjarne Stroutsrup"}
	my_json, err := json.Marshal(book)

	if err != nil {
		fmt.Println(err)
	}

	fmt.Printf("Json for book is: %s\n", string(my_json)) // {"Name":"C++ programming language","Author":"Bjarne Stroutsrup"}

	//--------------------------------------------------
	// Unmarshalling
	codString := `{"Name":"The Name of the Wind","Author":"Patrick Rothfuss"}`

	var cod Book

	err = json.Unmarshal([]byte(codString), &cod)

	if err != nil {
		fmt.Println(err)
	}

	fmt.Printf("Book for json is: %v\n", cod)

	//--------------------------------------------------
	// Arrays and Slices
	// You can marshall/unmarshall to array or slice

	var books []Book

	booksJson := `[{"Name": "Hacking for Dummies", "Author": "Kevin Beaver"},
                   {"Name": "Kerberos", "Author": "Jason Garman"}]`

	err = json.Unmarshal([]byte(booksJson), &books)

	if err != nil {
		fmt.Println(err)
	}
	fmt.Printf("Slice for json is: %v\n", books)

	some_books := []Book{
		{Name: "Libro Uno", Author: "Author Uno"},
		{Name: "Libro Dos", Author: "Author Dos"},
	}

	some_books_json, err := json.Marshal(some_books)

	fmt.Printf("json for slice is: %v\n", string(some_books_json))
}

Atributos “a medida” para json

Podemos añadir atributos json a una struct que vamos a leer o a salvar con json.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
	"encoding/json"
	"fmt"
)

type Book struct {
	Name   string `json:"title"`   // IMPORTANTE: Nada de espacios al definir los atributos
	Author string `json:"artist"`
}

func main() {

	//--------------------------------------------------
	// Marshalling
	book := Book{"C++ programming language", "Bjarne Stroutsrup"}
	my_json, err := json.Marshal(book)

	if err != nil {
		fmt.Println(err)
	}

	fmt.Printf("Json for book is: %s\n", string(my_json)) // {"title":"C++ programming language","artist":"Bjarne Stroutsrup"}
}

Además de cambiar los nombres de los campos podemos especificar un par de atributos json más:

  • .omitempty: nos permite saltarnos los datos si el campo está vacio
  • "-" : nos permite saltarnos este campo por completo
1
2
3
4
5
type userdata struct {
        login string `json:"username"`
        real_name string `json:"name, .omitempty"`
        password string `json:"-"`
}

json y maps

Siempre podemos cargar el json en un map, especialmente si no están muy bien estructurados:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
    "fmt"
    "encoding/json"
)

func main() {
    unstructuredJson := `{"os": {"Windows": "Windows OS","Mac": "OSX","Linux": "Ubuntu"},"compilers": "gcc"}`

    var result map[string]interface{}

    json.Unmarshal([]byte(unstructuredJson), &result)

    fmt.Println(result["os"])    // map[Linux:Ubuntu Mac:OSX Windows:Windows OS]
}

Por supuesto podemos también salvarlos en json:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
    "fmt"
    "encoding/json"
)

type Address struct {
    Street string
    City string
}

type Person struct {
    Name string
    Address Address
}

func main() {
    p := Person{
        Name: "Sherlock Holmes",
        Address: Address{
            "22/b Baker street",
            "London",
        },
    }

    str, err := json.Marshal(p)

    if err != nil {
        fmt.Println(err)
    }

    fmt.Println(string(str))  // {"Name":"Sherlock Holmes","Address":{"Street":"22/b Baker street","City":"London"}}
}

os/exec

https://www.darrencoxall.com/golang/executing-commands-in-go/

Este paquete es parte de la biblioteca estándar de Go. Nos permite ejecutar comandos de sistema.

Tres casos típicos:

  • Lanzar un comando y capturar su salida
  • Lanzar un comando y comprobar el exit code
  • Lanzar comandos de larga duración
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package main

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"strings"
	"syscall"
)

// printCommand prints command
func printCommand(cmd *exec.Cmd) {
	fmt.Printf("==> Executing: %s\n", strings.Join(cmd.Args, " "))
}

// printError print human friendly error objects
func printError(err error) {
	if err != nil {
		os.Stderr.WriteString(fmt.Sprintf("==> Error: %s\n", err.Error()))
	}
}

// printOutput prints command output
func printOutput(outs []byte) {
	if len(outs) > 0 {
		fmt.Printf("==> Output: %s\n", string(outs))
	}
}

// main el cuerpo ppal del programa
func main() {
	// Create an *exec.Cmd
	// Capture Stdout and Stderr together
	cmd := exec.Command("echo", "Called from Go!")
	printCommand(cmd)
	output, err := cmd.CombinedOutput() // runs cmd and return combined output
	printError(err)
	printOutput(output) // => go version

	// Create an *exec.Cmd
	cmd = exec.Command("go", "version")

	// Stdout buffer
	cmdOutput := &bytes.Buffer{}
	// Attach buffer to command
	cmd.Stdout = cmdOutput

	// Execute command
	printCommand(cmd)
	err = cmd.Run() // will wait for command to return
	printError(err)
	// Only output the commands stdout
	printOutput(cmdOutput.Bytes())

	// create a command that will fail
	cmd = exec.Command("ls", "/imaginary/dir")
	var waitStatus syscall.WaitStatus
	if err := cmd.Run(); err != nil {
		printError(err)
		// Did the command fail because of an unsuccessful exit code
		if exitError, ok := err.(*exec.ExitError); ok {
			waitStatus = exitError.Sys().(syscall.WaitStatus)
			printOutput([]byte(fmt.Sprintf("%d", waitStatus.ExitStatus())))
		}
	} else {
		// Command was successful
		waitStatus = cmd.ProcessState.Sys().(syscall.WaitStatus)
		printOutput([]byte(fmt.Sprintf("%d", waitStatus.ExitStatus())))
		printOutput(cmdOutput.Bytes())
	}
}

Para el último caso comandos de larga duración, no queremos que nuestro programa espere al resultado del comando. Es mejor hacerlo de forma asíncrona.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"time"
)

// printError print human friendly error objects
func printError(err error) {
	if err != nil {
		os.Stderr.WriteString(fmt.Sprintf("==> Error: %s\n", err.Error()))
	}
}

// printOutput prints command output
func printOutput(outs []byte) {
	if len(outs) > 0 {
		fmt.Printf("==> Output: %s\n", string(outs))
	}
}

// main()  programa ppal
func main() {
	cmd := exec.Command("cat", "/dev/random") // creamos un comando
	randomBytes := &bytes.Buffer{}            // creamos un buffer
	cmd.Stdout = randomBytes                  // asociamos la salida estándar al buffer

	err := cmd.Start() // Lanzamos el comando de forma asíncrona
	printError(err)

	// Create a ticker that outputs elapsed time
	ticker := time.NewTicker(time.Second)
	go func(ticker *time.Ticker) {
		now := time.Now()
		for _ = range ticker.C {
			printOutput(
				[]byte(fmt.Sprintf("%s", time.Since(now))),
			)
		}
	}(ticker)

	// Create a timer that will kill the process
	timer := time.NewTimer(time.Second * 4)
	go func(timer *time.Timer, ticker *time.Ticker, cmd *exec.Cmd) {
		for _ = range timer.C {
			err := cmd.Process.Signal(os.Kill)
			printError(err)
			ticker.Stop()
		}
	}(timer, ticker, cmd)

	// Only proceed once the process has finished
	cmd.Wait()
	printOutput(
		[]byte(fmt.Sprintf("%d bytes generated!", len(randomBytes.Bytes()))),
	)
}

tcell

Este paquete nos permite implementar interfaces de usuario basados en texto.

Instalamos:

1
go get -u github.com/gdamore/tcell

Recomiendan mirar primero el ejemplo incluido mouse

Casos prácticos

Leer y escribir json

Básico:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"encoding/json"
	"fmt"
)

// main ...
func main() {
	x := map[string]string{
		"foo": "bar",
	}
	data, _ := json.Marshal(x)
	fmt.Printf("Data contains:\n%s\n", (data))
}

Result:

1
2
Data contains:
{"foo":"bar"}

Un poco más complicado:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
	"encoding/json"
	"fmt"
)

// main ...
func main() {

	type person struct {
		Name        string `json:"name"`
		Age         int    `json:"age"`
		Description string `json:"descr,omitempty"`   // DON'T use spaces in tags
		secret      string // Unexported fields are Unmarshaled
	}

	x := person{
		Name:   "Bob",
		Age:    32,
		secret: "Shhh!",
	}

	data, _ := json.Marshal(x)
	fmt.Printf("Data contains:\n%s\n", (data))
}

Un ejemplo de unmarshalling, aunque siempre es preferible usar una struc si conocemos de antemano la estructura de los datos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package main

import (
	"encoding/json"
	"fmt"
)

// main test json
func main() {
	data := []byte(`{"foo":"bar"}`)

	var x interface{}
	_ = json.Unmarshal(data, &x)
	fmt.Printf("x contains:\n%s\n", x)
}

Un ejemplo de :

Best Practices

Para estudiar

0%