49 KiB
weight | title | date | draft | summary | categories | tags | |||
---|---|---|---|---|---|---|---|---|---|
4 | Apuntes del lenguaje de programación Go | 2021-05-18T09:55:41+0200 | false | Apuntes de Go |
|
|
{{< admonition type=warning title="Work in progress" open=true >}}
Apuntes incompletos del lenguaje de programación Go
{{< /admonition >}}
Referencias
- Documentación Oficial
- CodeReviewComments
- Golang 101 hacks
- Un video curso de Traversy Media
- Un video curso de 7 horas de freeCodeCamp
- Go Workshop Book
- Awesome Go
- Golang by example: All design patterns in Go
- Design Patters (lo mismo pero con más dibujitos)
- Go Modules
- Build a web app in Golang
Books
- Esencia del lenguaje Go: "The Go Programming Language" Addison-Wesley
- Panorámica del go-stdlib: "Mastering Go" Packt Publishing; 2nd Revised edition
Instalación
- Bajamos el paquete con la última versión desde la página oficial.
- Y lo descomprimimos como root en
/usr/local/
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):
# 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
{{< admonition type=danger title="GOROOT" open=true >}}
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
{{< /admonition >}}
Configuración del entorno de Go
{{< admonition type=info title="Referencias" open=false >}}
- Golang Doc: gopath_code
- Golang Enviroment Configuration
- Build a Web Application with Golang {{< /admonition >}}
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)
{{< admonition type=tip title="Ver el entorno" open=true >}}
Puedes ver el entorno completo de Go con el comando go env
{{< /admonition >}}
Más de GOPATH
{{< admonition type=warning title="Abandonando GOPATH" open=true >}}
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.
{{< /admonition >}}
Go espera que en el directorio $GOPATH
haya tres subdirectorios:
bin/
pkg/
src/
bin/
contiene los ejecutables que se generan cuando ejecutamosgo install ...
pkg/
contiene los paquetes instalados en nuestro sistema congo 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/
:
src/github.com/salvari/miAppGo
src/gilab.com/salvari/mach5
Instalación de herramientas
golint y godoc
Ya no instalamos con los comandos:
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:
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:
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
# GO111MODULE=on # not needed with 'go install'
go install golang.org/x/tools/gopls@latest
Herramientas adicionales
- gopkgs
- go-outline
- dlv
- dlv-dap
- staticcheck
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
# 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:
$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:
go mod init example.com/modulename
Actualizar el fichero .mod
Ejecutamos: go mod tidy
TDD (Test Drived Development) en Go
{{< admonition type=info title="Referencias" open=true >}}
- Learn Go with Tests Un buen sitio para empezar
- An introduction to Testing in Go Un video muy ilustrativo
- Advanced Testing in Go Continuación del anterior
{{< /admonition >}}
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.
a = "Una cadena" for _, r := range a { fmt.Println(r) }
make
-
Parece que vale para hacer un alloc de memoria.
counts := make(map[string]int)
time
-
La especificación de formatos tiene su gracia ¬_¬ ver referencia
Arrays
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
- Diferencias entre array y slice
- The Geek Stuff: Golang Array vs Slice
- A comprehensive guide on array and slices in Go
- Go: Array vs Slices Bonanza
- Slice Tricks by Ian Lance Taylor
- DigitalOcean: Understanding array and slices in Go
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
:
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
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
// 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
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
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
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
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)
{{< admonition type=info title="Referencias" open=false >}}
- Golangbot: Go Packages
- The Go blog: Using Go Modules
- How to write Go code with GOPATH {{< /admonition >}}
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
=> packagerand
) Lo normal es que todos los ficheros del paqueterand
se guarden en el directoriorand/
- 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".mkdir pomodoro cd pomodoro touch main.go
- Iniciamos el modulo en la raiz del proyecto
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
ynotification
. Dentro de los subdirectorios creamos un fichero de código (pueden ser tantos ficheros como queramos)mkdir timer touch timer/timer.go mkdir notification touch notification/notification.go
Tendremos una estructura de directorios:
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):
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
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
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
// 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
// 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
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
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.
// 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
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.
// 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:
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:
func NewMatrix(rows, cols, int) *matrix {
return &matrix{rows, cols, make([]float, rows*cols)}
}
{{< admonition type=warning title="structs y exportacion" open=true >}}
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
{{< /admonition >}}
Anonymous structs:
Más económicas y seguras que usar: map[string]interface{}
.
point := struct {
X, Y int
}{1, 2}
Punteros
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
// 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.
// 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:
type error interface {
Error() string
}
Una función que puede devolver un error:
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).
// 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
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
var c chan string c <- "Hello, World!" // fatal error: all goroutines are asleep - deadlock!
-
A receive from a nil channel blocks forever
var c chan string fmt.Println(<-c) // fatal error: all goroutines are asleep - deadlock!
-
A send to a closed channel panics
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
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
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.
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
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
- Escribir demasiados test a la vez
- Concentrarse exclusivamente en los happy path y la cobertura del código
- No considerar los diferentes escenarios
- Demasiadas comprobaciones (asserts) en un sólo test
- Probar cosas diferentes en el mismo caso de prueba
- Escribir test triviales para mantener el código cubierto
- No ejecutar los test con frecuencia
- No seguir siempre las tres fases. Especialmente implementando código sin escribir antes el test
- Hacer asserts que no prueban nada
- 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
{{< admonition type=warning title="Referencias" open=false >}}
- json con golang
- read a json file
- Parse json to a nested struct {{< /admonition >}}
- 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
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.
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
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:
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:
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
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.
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:
go get -u github.com/gdamore/tcell
Recomiendan mirar primero el ejemplo incluido mouse
Casos prácticos
Leer y escribir json
Básico:
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:
Data contains:
{"foo":"bar"}
Un poco más complicado:
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:
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 :