Abrir el instalador y seguir las instrucciones.
(Unix| Linux): /usr/local/go (varía según el sistema) Windows: C:\Go
Para chequear que la instalación está correcta escribir en la consola
$ go version
Activar variables de entorno para el trabajo con módulos
$ go env -w GO111MODULE=on GOPROXY=https://goproxy.io,direct
escribir en la consola
$ go env GOROOT = [Directorio de instalación] GOPROXY=https://goproxy.io,direct GO111MODULE=on
gopls
.$ go get golang.org/x/tools/gopls@latest
Vamos a programar el tradicional Hello World
.
hello-world
.main.go
dentro del directorio
creado en el paso 1.main.go
package main import "fmt" func main() { fmt.Println("Hello World") }
go run main.go
En esta sección vamos a revisar cada línea del programa Hello World
.
Linea 1 > =package main
Los packages son la forma de agrupar funcionalidad común en Golang. El
package default es main
y usualmente significa que el archivo que lo
contiene es el punto de entrada.
Linea 3 > =import "fmt"
En esta línea especificamos que el package fmt
(que se incluye como
parte de Golang), es requerido para ejecutar este programa.
Linea 5 > =func main() {
Las funciones en Golang se definen con el keyword func
. En esta línea
estamos definiendo una función llamada main
sin argumentos. El código
de la función se escribe entre { }
.
Para crear un ejecutable en Golang tiene que existir una funcion llamada
main
dentro de un package también llamado main
. Solo puede haber una
funcion main
, incluso si tenemos muchos archivos .go
(solo puede
existir un entrypoint por ejecutable)
Línea 6 > =fmt.Println("Hello World")
Esta línea imprime Hello World
en la consola. Println
es una función
dentro del package fmt
que recibe como argumento el string
"Hello World"
.
La función Println
hace dos cosas::
Una variable es un nombre que se le asigna a una porción de memoria para almacenar un valor de un determinado tipo.
El keyword var
se usa para declarar variables. La sintaxis es
var <name> <type>
seguido (opcionalmente) por un valor inicial.
var a int // Inicializado por defecto var b = 10 // Se infiere el tipo var c, d = 20, 30 // Asignando valores múltiples e, f := 40, 50 // Crear y asignar
Si no se especifica un valor inicial Golang asigna uno por defecto dependiento del tipo de dato de la variable.
Para imprimir el valor de una variable se puede usar Println
.
fmt.Println("a = ", a)
Los siguientes tipos de datos están disponibles en Golang:
bool
representa valores boleanos true
o false
.
Para tipos numéricos, el número que sigue al tipo indica la cantidad de bits que se usa para representar el valor en memoria. El número de bits determina qué tan grande el número puede ser.
int8
: 8 bit signed integers
int16
: 16 bit entero con signo
int32
: 32 bit entero con signo
int
: 32 or 64 bit entero con signo (dependiende de la plataforma).
uint
: 32 or 64 bit entero sin signo (dependiende de la plataforma).
float32
: 32 bit número flotante.
float64
: 64 bit número flotante.
byte
alias de uint8
rune
alias de int32
complex64
: números complejos con parte real e imaginaria float32
complex128
: números complejos con parte real e imaginaria float64
La función complex
se utiliza para construir un número complejo con
partes reales e imaginarias:
func complex(r, i FloatType) ComplexType
string
es una colección de caracteres encerrados entre comillas.
first := "Allen" last := "Varghese" name := first +" "+ last fmt.Println("My name is",name)
error
es un tipo especial para indicar condiciones de error.
chan
es un tipo especial que representa un canal de comunicación.
No hay conversión automática de tipos en Golang. Se utilizan funciones de tipo para hacer una conversión.
int(<float value>) float64(<integer value>)
Las constantes no cambian sus valores una vez asignadas. Se usa el keyword
const
en vez de var
para declarar una constante.
const a bool = true const b int32 = 32890 const c string = "Something"
Una función es un grupo de instrucciones que se ejecutan todas juntas como un bloque. Una función puede o no tener argumentos de entrada, y retornar valores.
En Golang una función se define con el keyword func
. El siguiente es
un ejemplo de una función que suma dos enteros:
func AddIntegers(a int, b int) int { return a + b }
El keyword return
es usado para indicar qué valor va a retornar la
función.
Golang soporta que una función devuelva múltiples valores:
func SumDifference(a int, b int) (int, int) { return a + b, a - b }
Se utiliza el identificador en blanco en el lugar de un valor que se quiere descartar al llamar a una función:
var _, diff = SumDifference(10, 20) fmt.Println("Difference is ", diff)
Cuando se define una función se le puede asignar un nombre al tipo de dato de retorno para luego referenciarlo en el código de la función.
func Product(a int, b int) (prod int) { prod = a * b return }
Al asignar un valor de retorno con nombre no hace falta incluirlo en
la sentencia return
.
Golang soporta funciones anónimas y de segundo orden. Tomemos por
ejemplo la función sort.Slice
func Slice(slice interface{}, less func(i, j int) bool)
El parámetro less
describe una función que toma dos enteros y
retorna un valor bool
Podemos asignar una función a una variable o definirla en el momento de su uso. Las funciones anónimas forman clausuras
c := []int{20, 11, 12, 1, 5} less := func(i int, j int) bool { return c[i] < c[j] } sort.Slice(c, less) fmt.Println("Despues del sort", c) //Output: [1 5 11 12 20] c := []int{20, 11, 12, 1, 5} sort.Slice(c, func(i int, j int) bool { return c[i] < c[j] }) fmt.Println("Despues del sort", c) //Output: [1 5 11 12 20]
Golang soporta múltiples instrucciones de control e iteradores.
if=/=else
Se pueden tener muchas instrucciones else if
. El bloque else
es
opcional:
if condition { // codigo a ejecutar } else if condition { // codigo a ejecutar } else { // codigo a ejecutar }
El bloque
else
debe estar en la misma línea que la llave de cierre (}
).
La instruccion if
soporta indicar una instrucción opcional que se
ejecuta antes de la condición:
if statement; condition { // código a ejecutar }
for
Hay una sola instrucción de iteración en Golang, el iterador for
.
for initialization; condition; post { // codigo a ejecutar }
Los tres componentes initialization
, condition
y post
son
opcionales en Golang.
for
en detallefor
El siguiente ejemplo imprime números del 1 al 10 usando un iterador
for
:
for i := 1; i <= 10; i = i + 1 { fmt.Println(i) }
switch
La instrucción de control switch
evalúa una expresión y la compara
contra una lista de posibles valores o expresiones que puedan coincidir.
Es una forma abreviada de escribir muchas cláusulas if else
.
switch expression { case expression or value | (, expression or value)*: // código a ejecutar (break | fallthrough) case expression or value | (, expression or value)*: // código a ejecutar default: // código a ejecutar }
break
, fallthrough
y default
break
detiene el flujo y sale del switch
fallthrough
continúa al próximo case
.default
.Una colección de elementos de un mismo tipo es un arreglo. Como Golang es un lenguaje fuertemente tipado no es posible mezclar valores de diferentes tipos en un arreglo.
Un arreglo se define como [size]type
:
var a [3]int fmt.Println(a) // Output: [0 0 0]
El primer índice de un arreglo es 0
en vez de 1
. Se accede a los
valores de un arreglo usando el número del índice, y se asignan valores
con el operador =
.
var a [3]int a[0] = 1 a[1] = 2 a[2] = 3 fmt.Println(a) // Output: [1 2 3]
Un arreglo se puede inicializar en la declaración:
a := [3]int{1, 2, 3}
Se puede omitir el número de elementos si se inicializa con valores en la declaración:
a := [...]int{1, 2, 3}
Un detalle importante a tener en cuenta es que un arreglo es un tipo valor, esto quiere decir que cada vez que un arreglo es asignado a una nueva variable, se hace una copia del arreglo original. Si se cambian valores en la nueva variable, esto no se ve reflejado en la variable original.
a := [...]string{"IRL", "IND", "US", "CAN"} b := a b[1] = "CHN" fmt.Println("Original:", a) fmt.Println("Copy :", b)
Esto afecta el modo en que los arreglos son pasados por parámetros.
len
La función len
se usa para conocer el tamaño de un arreglo:
a := [...]int{1, 2, 3} fmt.Println(len(a)) // Output: 3
range
Una forma de interactuar con un arreglo es utilizar el keyword range
.
Este retorna el índice del arreglo y el valor.
a := [...]int{1, 2, 3, 4, 5} sum := 0 for i, v := range a { fmt.Println("Index:", i, " Value is:", v) sum += v } fmt.Println("Sum:", sum) // Output: 15
range
y el blank identifier
Se puede usar el blank identifier (_
) si no nos interesa alguno de
los valores que retorna el keyword range
.
_, v := range a
Se pueden crear arreglos de más de una dimensión de la siguiente manera:
a := [3][2]int
a := [3][2]string{ {"lion", "tiger"}, {"cat", "dog"}, {"pigeon", "peacock"}, } fmt.Println(a) // Output: [[lion tiger] [cat dog] [pigeon peacock]]
Los arreglos tienen tamaño fijo, reservan memoria, y son tipos valor. Un slice es una forma flexible de acceder a una arreglo. Un slice no tiene datos, solo apunta a un arreglo.
Un slice vacío se crea así:
var sa []int
El valor de un slice vacío es nil
Un slice también se puede crear apuntando a un subconjunto de valores de un arreglo:
a := [...]int{1, 2, 3, 4, 5} sa := a[1: 4] fmt.Println(sa) // Output: [2 3 4]
Como un slice es un tipo referencia, modificar un valor en un elemento del slice modifica el arreglo original.
a := [...]int{1, 2, 3, 4, 5} sa := a[1: 4] fmt.Println("Before:", a) sa[0] = 22 fmt.Println("After:", a) // Output: Before: [1 2 3 4 5] // Output: After: [1 22 3 4 5]
make
Un slice también se puede crear utilizando la funcion make
,
especificando el tipo, y el tamaño, y opcionalmente la capacidad (que
indica el máximo tamaño que el slice puede crecer):
#+begin_src go i := make([]int, 5, 5) fmt.Println(i) // Output: [0 0 0 0 0]
Crear un slice con make
inicializa todos sus valores con los valores
por defecto del tipo del slice.
El tamaño de un slice se puede incrementar utilizando la función
append
.
sa := []int{1, 2, 3} newSa := append([]int{}, sa...) fmt.Println(newSa) // Output: [1, 2, 3]
En vez de valores se puede indicar otro slice. El operador ...
se usa
para expandir el slice en sus valores.
map
Un map
es un tipo de dato incluido en Golang que asocia una clave con un
valor. El valor puede ser recuperado a partir de la clave.
Un map
vacío se crea con la función make
:
make(map[type of key]type of value) eg: make(map[string]int)
Los valores en un map
se referencian igual que en un arreglo. Un valor
se agrega a un map
asignándole una clave, si la clave ya existe el valor
para esa clave se sobrescribe.
m := make(map[string]int) m["a"] = 1 m["b"] = 2 fmt.Println(m) // Output: map[a:1 b:2]
map
Un map
se puede inicializar con valores en su declaración igual que un
arreglo:
m := map[string]int { "a": 1, "b": 2, }
Una manera de chequear si una clave existe es la siguiente:
data, ok := m["some key"]
Si ok
es true
, la clave existe y la variable data
contendrá la
información recuperada, si ok
es false
la clave no existe.
Acceder a una clave inexistente en un map
causa un panic
.
El iterador for
se utiliza para recorrer las claves y valores de un
map
m := map[string]int { "a": 1, "b": 2, "c": 3, } for key, value := range m { fmt.Println("Key:", key, " Value:", value) } // Output: Key: a Value 1 // Key: b Value 2 // Key: c Value 3
Se puede utilizar el identificador en blanco (_
) para descartar
cualquiera de los elementos del par.
El orden en que se recorre el
map
no esta garantizado, y cada ejecución puede tener un order diferente.
Los valores se eliminan de un map
con la función delete
. La función no
retorna nada. La sintaxis es la siguiente:
delete(m, key)
El tamaño de un map
es retornado por la funcion len
. Esta funcion
retorna la cantidad de claves que hay en un map. La sintaxis es la
siguiente:
len(m)
Un map
es un tipo referencia. Por lo tanto si un map
se asigna a
otra variable, ambas variable apuntan a los mismos pares (clave,
valor). Lo mismo sucede si se pasa como argumento en una funcion.
En Golang, los paquetes corresponden a los archivos en un mismo directorio. Por convención, los paquetes llevan el nombre del directorio que los contiene. Aunque esto no es requerido es considerado una buena práctica.
Supongamos que creamos el proyecto github.com/ourcompany/superproduct
+-- LICENSE +-- README.md +-- config.go # package superproduct +-- go.mod +-- go.sum +-- client +-- lib.go # package client +-- lib_test.go # package client +-- cmd +-- super-client +-- main.go # package main +-- super-server # package main +-- main.go +-- internal +-- auth +-- auth.go # package auth +-- auth_test.go # package auth +-- server +-- lib.go # package server
El paquete main
tiene significado especial ya que representa el
punto de entrada de la aplicación. Como se ve en el ejemplo anterior
podemos tener más de una aplicación en un mismo proyecto Golang
(super-client
y super-server
).
Al menos uno de los archivos en el paquete main
debe tener una
función main
.
client
y server
en el ejemplo). Pueden existir varios niveles de
subpaquetes.internal
(Golang no permite importar internal
o ninguno
de sus subpaquetes desde módulos de terceros)cmd
.Para importar paquetes utilizamos la ruta completa del paquete dentro del módulo
package main import "fmt" import "github.com/ourcompany/superproduct" import "github.com/ourcompany/superproduct/client" func main() { fmt.Println(superproduct.Config()) fmt.Println(client.Hello()) }
import
Es posible usar alias para los paquetes importados
import s "github.com/ourcompany/superproduct"
Cuando existen varios imports
, la convención es agruparlos.
import ( "fmt" s "github.com/ourcompany/superproduct" "github.com/ourcompany/superproduct/client" )
Existen dos casos especiales de alias para import
.
incorpora todos los elementos del paquete que se importa a
nuestro namespace._
no incorpora ninguno de los elementos del paquete importado
a nuestro namespace.
La función init
nos permite ejecutar código de inicialización para
nuestros paquetes. A diferencia de la función main
, puede existir más
de una función init
por paquete.
// tomado de /github.com/lib/pq/conn.go func init() { sql.Register("postgres", &Driver{}) }
Las reglas de visibilidad en Golang siguen un patrón sencillo.
En el ejemplo el tipo square
no es exportado la función NewSquare
si.
package geometry type square struct { a int b int } func NewSquare(a, b int) *square { return &square{a, b} }
Es posible para un tipo no exportado tener campos o métodos que sean exportados.
// Area of a square func (s square) Area() int { return s.a * s.b }
En este caso es posible acceder a los elementos exportados aunque no sea posible declarar explicitamente que se accede al tipo.
// Inválido porque square no es exportado //var s *geometry.square = geometry.NewSquare(length, breadth) s := geometry.NewSquare(length, breadth) fmt.Println("Area is", s.Area())
A partir de la versión 1.13
, Golang incluye un sistema nativo de
manejo de dependencias utilizando módulos. En versiones anteriores el
código de nuestros proyectos tenía que ubicarse en $GOPATH/src
. Ese
enfoque es ahora considerado obsoleto
Para crear un módulo ejecutamos el siguiente comando:
go mod init <nombre del módulo>
Por convención el nombre del módulo es la URL del repositorio de control de versiones que alberga el código.
El sistema de módulos depende de dos archivos.
go.mod
que incluye la definición y las dependencias directas.go.sum
que incluye las dependencias directas e indirectas con
versiones exactas y suma de verificación.
Golang incluye el subcomando mod
para ejecutar diferentes tareas
relacionadas con módulos. Para más detalles ejecutar
go help mod
Existen varias formas de adicionar dependencias a nuestro módulo. La más simple es importar la dependencia en el código y ejecutar el siguiente comando.
go mod tidy
Los entornos de desarrollo modernos y el Go Language Server hacen este proceso de forma automática
Interactuar con la línea de comandos es muy útil y muy común al ejecutar
scripts y programas que no tienen GUI. El paquete os
se usa para tener
acceso a los argumentos de la línea de comandos al ejecutar un programa
de Golang.
Los argumentos de la línea de comandos están disponibles en el arreglo
os.Args
:
import "os" os.Args // Lista completa incluyendo el nombre del programa os.Args[1:] // Solo los argumentos
Especificar los argumentos como valores separados por espacios en la
terminal es muy básico y difícil de extender. El uso de flags
provee
mas flexibilidad en cómo los argumentos se especifican, sobre todo los
opcionales. Golang cuenta con un paquete llamado flag
para
soportarlos.
import "flag" flag.<type>(nombre del flag, valor default, descripcion)
Un flag se define con la sintaxis anterior. El <type>
puede ser
string
, int
o bool
.
Un flag opcional se puede indicar controlando si el valor es el mismo que el valor especificado por defecto.
Un flag se puede leer independientemente de la posición en que el usuario los hubiera especificado.
Las variables de entorno se usan para configurar aspectos de sistema en la mayoría de los *NIX y también en Windows. Son pares de clave-valor usualmente disponibles para todos los programas que corren en la terminal/shell. Se pueden definir variables de entorno custom solo disponibles durante la ejecución de un script de shell.
El paquete os
proporciona dos funciones, Setenv
y Getenv
, para
escribir y leer variables de entorno. Un string vacío se retorna si la
clave no existe.
import "os" os.Setenv("FOO", "1") fmt.Println("FOO:", os.Getenv("FOO")) fmt.Println("BAR:", os.Getenv("BAR")) //Output: FOO: 1 // BAR:
La función Environ
retorna la lista completa de todas las variables de
entorno existentes.
import "strings" for _, e := range os.Environ() { pair := strings.Split(e, "=") fmt.Println(pair[0]) }
En lugar de excepciones como Python, Java o C#, Golang sigue un enfoque más cercano a los lenguajes funcionales donde el estado de error es representado por un tipo de datos,
var DivisionByZero = errors.New("División por cero") func safedivide(a int, b int) (int, error) { if b == 0 { return 0, DivisionByZero } return a / b, nil }
El tipo error
es una interfaz built-in del lenguaje
type error interface { Error() string }
Esto nos permite crear nuestros propios tipos de errores
type MyError struct { message: string status: int } func (m *MyError) Error() string { return fmt.Sprintf("Error - %s. Status %d", m.message, m.status) } func returnMyError() error { return &MyError{ message: "None", status : -1, } }
El paquete errors
contiene funciones dedicadas a manejar tipos de
error. Podemos usar la función errors.Is
para verificar si un error
es de un tipo específico.
if _, err := os.Open("non-existing"); err != nil { if errors.Is(err, os.ErrNotExist) { fmt.Println("file does not exist") } else { fmt.Println(err) } }
La sentencia defer
se utiliza para indicar que la función a
continuación se debe ejecutar a la salida de la función actual
Es muy usual en Golang usar defer
para destruir o liberar cualquier
tipo de recurso temporal que se obtenga en la función.
func doSomething() { a := getExternalResource(); defer a.Release() /// Resto de la función }
defer
se ejecuta sin importar las causas por las que la función
actual haya terminado, lo que garantiza en el ejemplo anterior que
a.Release()
siempre se ejecute.
Golang incluye una función especial panic
para indicar un error que no
puede ser manejado de forma correcta.
La contraparte de panic
es la función recover
, que verifica si
ocurrió una llamada a panic
en el contexto de la función actual.
Si una llamada a panic
no es seguida por un recover
, la función
termina y el contexto pasa al invocador. Esto continúa hasta que se
encuentre un recover o se llegue a la función main
en cuyo caso el
programa se detendrá con un mensaje de error.
func f() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered in f", r) } }() fmt.Println("Calling g.") g(0) fmt.Println("Returned normally from g.") } func g(i int) { if i > 3 { fmt.Println("Panicking!") panic(fmt.Sprintf("%v", i)) } defer fmt.Println("Defer in g", i) fmt.Println("Printing in g", i) g(i + 1) }
Uno de los errores más comunes para los que se inician en Golang es
pensar en panic
y recover
como una alternativa a los bloques
try-catch
que aparecen en otros lenguajes. Esto es considerado una
mala práctica.
La función panic
se debe utilizar solo para indicar estados en el
flujo de una aplicación para los cuales no hay solución efectiva. Por
otro lado recover
debería utilizarse para liberar o destruir recursos
adquiridos por la aplicación antes de hacer una salida forzosa.
Golang incluye en el paquete testing las herramientas necesarias para
hacer pruebas unitarias. Por convención las pruebas unitarias se
incluyen en el mismo paquete de la funcionalidad a probar, adicionando
a los archivos el sufijo _test
.
Además de pruebas unitarias, Golang nos permite escribir pruebas de rendimiento o benchmarks y ejemplos de prueba.
Si tenemos una función para calcular los números de fibonacci.
func Fibonacci(n int) int { if n < 2 { return n } return Fibonacci(n-1) + Fibonacci(n-2) }
func TestFibonacci(t *testing.T) { var in int = 10 var want int = 55 got := Fibonacci(in) if got != want { t.Errorf("Fibonacci(%d) == %d, want %d", in, got, want) } }
Los ejemplos además de ejecutar pruebas, son una forma de documentar el uso de funcionalidades.
func ExampleFibonacci() { fmt.Println(Fibonacci(10)) // Output: 55 }
Tanto las pruebas unitarias como los ejemplos se ejecutan utilizando el
comando go test
.
Un benchmark o prueba de rendimiento nos permite examinar el desempeño de nuestras funcionalidades.
func BenchmarkFibonacci(b *testing.B) { for n := 0; n < b.N; n++ { Fibonacci(10) } }
Por convención, Golang ignora cualquier directorio que empiece con .
,
_
o se llame testdata
. Dado que las pruebas siempre se ejecutan en
el directorio donde se encuentra el archivo _test
podemos usar la
función Join
del paquete path/filepath
para acceder a los datos de
prueba.
func TestBytesInFile(t *testing.T) { l, err := BytesInFile(filepath.Join("testdata", "hello_world")) if err != nil { t.Errorf("Error %s", err) t.FailNow() } if l != 12 { // New line at end of file t.Errorf("Got wrong number of bytes %d", l) } }
struct
Un struct
es un tipo de dato definido por el usuario que representa
una colección de campos.
type Employee struct { firstName string lastName string age int }
Los campos individuales de un struct
se acceden con .
.
var emp := Employee { firstName: "Something" } fmt.Println(emp.firstName) //Output: Something
Golang permite que los campos de un struct
puedan ser de tipo struct
.
type Address struct { city, state string } type Employee struct { firstName, lastName string age int address Address }
struct
Para inicializar un campo struct
simplemente nos referimos al mismo en
el bloque de inicialización
var emp Employee emp := Employee{ firstName: "Peter", lastName: "Parker", age: 22, address: Address{ city: "New York", state: "New York", }, } fmt.Println(emp) //Output: {Peter {New York New York}}
Los campos anidados se acceden con múltiples niveles de notación de punto.
fmt.Println(emp.address.city) //Output: New York
Los campos en las estructuras pueden opcionalmente tener etiquetas. Las etiquetas funcionan como metadatos y no afectan la forma en que se representan los datos de la estructura.
Las etiquetas pueden opcionalmente ser pares llave:"valor"
.
type Employee struct { firstName string `json:"nombre"` lastName string `json:"apellido"` age int `json:"edad"` }
Por convención la llave corresponde al nombre del paquete encargado de procesar la anotación.
Un struct
puede también "heredar" todos los campos de otro usando
composición. Para esto existe una notación especial
type Address struct { city, state string } type Employee struct { firstName, lastName string age int Address }
struct
compuestoAl crear una estructura compuesta tenemos que especificar todos los campos
emp := Employee{ firstName: "Peter", lastName: "Parker", age: 22, Address: Address{ city: "New York", state: "New York", }, } fmt.Println(emp.city) //Output: New York
Un struct
es un tipo por valor. Dos variables struct
se consideran
iguales si todos los valores de sus campos son iguales. Esto quiere
decir que si un struct
tiene campos que no se pueden comparar, como un
map
, la operación (==
) va a fallar.
Los métodos (también llamados receptores) son funciones asociadas a un tipo especifico. Son similares al concepto de métodos de clase en el mundo OOP. La sintaxis es la siguiente:
func (t Type) MethodName(parameter list) { // codigo del metodo }
Usualmente se define el código del método en el mismo archivo que el tipo que lo contiene.
// Receptor por valor func (e Employee) Print() { fmt.Println("Employee Record:") fmt.Println("Name:", e.firstName, e.lastName) fmt.Println("Address:", e.address) } // en main var emp Employee emp.Print() // Outpput Employee Record: // Name: Allen Varghese // Address: {AA CO}
Golang soporta punteros para actualizar valores pero no admite
aritmética de punteros como en C. *
se usa como prefijo para definir
un puntero para un tipo dado. El operador &
se usa para crear
punteros a tipos.
El valor por defecto de los punteros en Golang es nil
, este valor
también se utiliza para indicar que un puntero es nulo.
Tener en cuenta que un puntero solo permite recibir punteros de su tipo y no otros.
var emp *Employee // puntero nil emp = &Employee{...} // puntero a Employee
Un puntero es una referencia a un tipo, por lo que podemos utilizarlo para modificar el valor original. Los receptores por puntero o por referencia son una aplicación directa de este concepto.
// Receptor por puntero func (e *Employee) updateAge(newAge int) { e.age = newAge } // En main emp := Employee{ age: 33, } fmt.Println("Before:", emp.age) emp.updateAge(34) fmt.Println("After:", emp.age) //Output: Before: 33 // After: 34
sync.Mutex
).map
, func
o chan
int
o string
En Golang, una interfaz es un conjunto de firmas de métodos. Si un tipo tiene una definición para esos métodos, se dice que implementa la interfaz. A diferencia de otros lenguajes, la asociación de un tipo con una interfaz es implicita.
Un tipo puede implementar más de una interfaz.
type Permanent struct { empID int basicPay int pension int } type Contract struct { empID int basicPay int } func (p Permanent) calculateSalary() int { return p.basicPay + p.pension } func (p Contract) calculateSalary() int { return p.basicPay } type SalaryCalculator interface { calculateSalary() int } // In main sc := [...]SalaryCalculator{Permanent{1, 5000, 50}, Contract{3, 7000}} totalPayout := 0 for _, v := range sc { totalPayout += v.calculateSalary() } fmt.Println(totalPayout)
Las interfaces al igual que las estructuras pueden anidarse.
type Reader interface { Read(p []byte) (n int, err error) } type Seeker interface { Seek(offset int64, whence int) (int64, error) } type ReadSeeker interface { Reader Seeker }
Para que un tipo implemente la interfaz ReadSeeker
tiene que
implementar Read
y Seeker
a la vez.
De la misma forma que una estructura "hereda" los métodos de otra en la composición, implementa las interfaces de aquellas que la componen.
type Animal interface { Name() string } type Dog struct{} func (d *Dog) Name() string { return "Dog" } func Bark(d *Dog) { fmt.Println("Woof!") } type GuideDog struct { *Dog }
En el ejemplo anterior el tipo GuidedDog
implementa la interfaz
Animal
El tipo interface{}
representa un valor comodín al estilo del tipo
object
de C# o void *
en C (técnicamente todos los tipos
implementan una interfaz sin métodos)
func describe(i interface{}) { fmt.Printf("(%v, %T)\n", i, i) }
A veces nos es necesario saber de qué tipo es el valor guardado en la interfaz.
var i interface{} = "hello" s := i.(string) fmt.Println(s) s, ok := i.(string) fmt.Println(s, ok) f, ok := i.(float64) fmt.Println(f, ok) f = i.(float64) // panic fmt.Println(f)
La sintaxis variable.(Tipo)
es un type assertion, concepto muy
parecido al type casting de otros lenguajes.
Los type assertions se ejecutan con alguna de las siguientes variantes
// variante segura v, ok := i.(Tipo) // variante insegura v := i.(Tipo)
La variante segura retorna un par (Tipo,bool)
donde el segundo
valor representa el estado de la operación. Un estado de true
significa que se pudo efectuar la conversión, false
que la
conversión no es posible y el primer valor de la tupla estará con el
valor nulo.
En la variante insegura si no se puede efectuar la conversión el
runtime de Golang lanzará un panic
.
Los type switches son una construcción especial que nos permite determinar el tipo de una variable y actuar en consecuencia
switch v := v.(type) { case string: fmt.Printf("%v is a string\n", v) case int: fmt.Printf("%v is an int\n", v) default: fmt.Printf("The type of v is unknown\n") }
En lugar de usar la sintaxis v.(Tipo)
para la conversión se utiliza
v.(type)
.
Las gorutinas son funciones o métodos que se ejecutan de modo concurrente. Para los efectos se pueden considerar como hilos o threads ligeros.
Las gorutinas se crean usando la palabre clave go
antes de la
llamada a una función.
// llamar name como una gorutina go name() // gorutina anónima como go func() { // código }()
Las gorutinas se ejecutan de modo concurrente. Para sincronizar
gorutinas es común utilizar elementos de sincronización como
sync.WaitGroup
.
func main() { wg := &sync.WaitGroup{} wg.Add(4) for i:=0; i < 4; i++ { go worker(wg) } // Sin esto es posible que main termine // antes de las gorutinas. wg.Wait() } func worker(w *sync.WaitGroup) { defer wg.Done() DoWork() }
Los canales son un mecanismo para sincronización y comunicación entre gorutinas. Los usuarios de sistemas basados en Unix pueden pensar en canales como pipes.
Para crear un canal usamos la función make
.
c := make(chan string) // canal sin buffer cb := make(chan string, 200) // canal con buffer de 200
La comunicación vía canales en síncrona. Cuando se envían datos a un canal la gorutina se bloquea hasta que estos datos sean recibidos. Lo mismo ocurre si se intenta recibir sin haber datos disponibles.
Los canales con buffer permiten acumular datos enviados y solo bloquean si
n
datos, donde n
es el tamaño del buffer.
Para cerrar un canal se utiliza close
. Un canal cerrado no puede
usarse para enviar datos. Recibir datos de un canal cerrado siempre
retornará el valor cero del tipo de datos del canal.
// Enviar c <- "Hola" // Recibir v := <- c // Recibir mientras el canal esté abierto for v := range c { processValue(v) }
La sentencia select
nos permite esperar por el resultado de más de
un canal.
// Esperamos por mensaje o timeout for { select { case v := <-input: doOperation(v) // Canal de tipo <- chan Time case <- time.After(time.Duration(80) * time.Millisecond): fmt.Println("Timeout!") return results } }
encoding
.
El paquete encoding
define interfaces para convertir datos
desde/hacia bytes o representaciones de texto.
ascii85
Formato utilizado por Adobe PDF y Postcript.asn1
Estructuras ASN.1 en formato DER.base32
Codificación Base32, RFC 4648.base64
Codificación Base64, RFC 4648.binary
Traducción entre formatos numéricos y codificación a bytes de los mismos.csv
Archivos con valores separados por coma (CSV).gob
Transmisión de flujos de datos entre emisor y receptor ()hex
Valores hexadecimales.json
JSON como se define en la RFC 7159.pem
Formato PEM usado actualmente para representar llaves TLS y certificados.xml
Parser XML 1.0 con soporte para espacios de nombres.
json.Marshal
codifica tipos Golang en JSON
bool
como JSON Boolean.string
como JSON String, eliminando etiquetas HTML.struct
como JSON Object.nil
como null
chan
, complex y literales de funciones provocan error.Las etiquetas definen como se serializan las estructuras.
type Address struct { // omitempty hace que los campos vacíos no // no se serialice. City string `json:"ciudad,omitempty"` State string `json:"estado,omitempty"` // Campos marcados con - no se serializan Zip strung `json:"-"` } type Employee struct { FirstName string `json:"nombre"` LastName string `json:"apellido"` Age int `json:"edad"` // no se serializa porque no es exportado id string Address }
emp1 := Employee{"Peter", "Parker", 22, Address{"Manhattan", "New York"}} d, _ := json.Marshal(&emp1) fmt.Println(string(d))
json.Unmarshal
es la contraparte de json.Marshal
.
tony := `{"nombre":"Tony","apellido":"Stark","edad":44}` var emp3 Employee err = json.Unmarshal([]byte(tony), &emp3) if err != nil { log.Fatal("No se pudo deserializar") }
Unmarshal
a un slice elimina los valores del slice.Unmarshal
a un array descarta los valores extra si el array
destino es muy pequeño.Unmarshal
a un map
conserva las llaves existentes.null
se traduce a nil
o al valor que tenga el tipo sin
inicializar
El paquete net/http
contiene implementaciones de servidor y cliente
para este protocolo. Crear un programa que responda a peticiones HTTP es
una tarea sencilla.
func main() { http.HandleFunc("/hello", func(w http.ResponseWWriter, r *http.Request) { fmt.Fprintf(w, "Hello world") }) log.Fatal(http.ListenAndServe(":8080", nil)) }
Las estructuras Server
y ServeMux
son las encargadas de operar el
servidor y el ciclo de petición y respuesta. La variable
DefaultServeMux
es utilizada por el servidor por defecto
Usar DefaultServeMux
tiene sentido para proyectos pequeños, pero lo
usual es que utilicemos nuestra propio ServeMux
mux := http.NewServeMux() mux.HandleFunc("/hello", func(w http.ResponseWriter, req *http.Request) { fmt.Fprintf(w, "Hello world") }) log.Fatal(http.ListenAndServe(":8080", mux))
Alternativamente podemos crear nuestro propio Server
.
// Create a server listening on port 8000 s := &http.Server{ Addr: ":8000", Handler: mux, } // Continue to process new requests until an error occurs log.Fatal(s.ListenAndServe())
Tanto Server
como ServeMux
proveen el nivel mínimo de abstracción
necesario para manejar peticiones HTTP.
Tomemos por ejemplo el código para hacer un API REST sencilla
type H = func(w http.ResponseWriter, r *http.Request) func dispatchResource(get, post, put, delete H) H { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.Method { case "GET": get(w, r) case "POST": post(w, r) case "PUT": put(w, r) case "DELETE": delete(w, r) default: w.WriteHeader(http.StatusNotFound) w.Write([]byte(`{"message": "not found"}`)) } } } func main() { // initialize mux getFunc := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"message": "get called"}`)) } postFunc := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) w.Write([]byte(`{"message": "post called"}`)) } m.HandleFunc("/resource", dispatchResource(getFunc, postFunc, nil, nil)) // use mux }
El paquete http contiene una implementacion de un cliente http que permite emular las acciones que realiza un web browser.
Para este ejercicio se utiliza el tester HTTP https://httpbin.org (codigo fuente, imagen de Docker para uso local).
resp, _ := http.Get("https://httpbin.org/get") defer resp.Body.Close() data, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(data))
payload := "Hello world!" resp, _ := http.Post("https://httpbin.org/post", "text/plain", strings.NewReader(payload)) defer resp.Body.Close() data, _ := ioutil.ReadAll(resp.Body) fmt.Println(string(data))
Notar que si obtenemos un error de http.Get
o http.Post
debemos
cerrar el body (utilizando defer
).
Podemos especificar un tiempo maximo de espera para que un request que tarda mucho termine automaticamente.
Crear un nuevo objeto request:
req, _ := http.NewRequest(http.MethodGet, url, nil)
Crear un context
con timeout:
ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel()
Ejecutar el request pasando por parametro el context
con el timeout:
resp, _ := http.DefaultClient.Do(req.WithContext(ctx)) defer resp.Body.Close()
Crear una funcion Reverse que devuelva un string pasado por parametro.
Modificar la funcion Reverse para invertir el casing de las vocales (solamente).
Crear un paquete stringutil que contega la funcion Reverse y se utilice en funcion la main.
Cambiar el programa para que el parametro string de la funcion Reverse se obtenga desde la linea de comandos con un flag llamado text.
Crear un test, un benchmark, y un example para la funcion Reverse.
Modelar la funcionalidad de un sistema de precios para una aerolinea que calcule los ingresos netos de un vuelo en base a los pasajeros y el precio base del ticket.
El precio base del ticket es el mismo para todos los pasajeros.
Existen 3 tipos de pasajeros: * Base: Paga el 100% del precio base. * De ultimo minuto: Paga el 50% del precio base. * Empleado de la aerolinea: No paga el ticket.
Crear un test para verificar el codigo creado en el challenge 2.1 anterior.
Agregar un nuevo tipo de pasajero 'empleado de aerolinea de ultimo minuto' cuyo descuento sea la suma de los descuentos de los tipos de pasajero 'empleado de aerolinea' y 'ultimo minuto'.
func Squares(number int) (sum int) { sum = 0 for number != 0 { digit := number % 10 sum += digit * digit number /= 10 } return } func Cubes(number int) (sum int) { sum = 0 for number != 0 { digit := number % 10 sum += digit * digit * digit number /= 10 } return } func SquaresPlusCubes(number int) int { return Squares(number) + Cubes(number) }
Crear una API REST para una aplicacion de recordatorios (ToDo) que contenga todas las funciones CRUD para la siguiente entidad:
ID int Title string IsDone bool
Hints:
Modificar la API Rest del challenge 4.1 para que pueda acceder a diferentes fuentes de datos (en memoria y MongoDB) usando el patron Repository.
Hints:
Modificar la API Rest del challenge 4.2 para usar Gin.
El paquete database/sql
contiene interfaces genéricas para el acceso
a bases de datos SQL. El concepto es similar a ADO.NET en .NET
Framework o JDBC en Java.
Para el resto de los ejemplo asumamos que tenemos una base de datos con una tabla única.
CREATE TABLE `userinfo` ( `uid` INTEGER PRIMARY KEY AUTOINCREMENT, `username` VARCHAR(64) NULL, `departname` VARCHAR(64) NULL, `created` DATE NULL );
Para acceder a una base de datos usamos la función sql.Open
. La
función recibe el nombre del driver y el DSN (ver documentación del
driver)
import _ "github.com/mattn/go-sqlite3" // En una función db, err := sql.Open("sqlite3", "./data.db") if err != nil { panic("Error accediendo a la base de datos") } defer db.Close() // Cerrar la base de datos siempre
La función db.Query
nos permite consultar la base de datos. Las
llamadas a db.Query
retornan una estructura db.Rows
que nos
permite iterar sobre las filas recibidas e inspeccionar su valor.
// Consultar rows, err := db.Query("SELECT * FROM userinfo") var uid int var username string var department string var created time.Time // Verdadero si existen más filas for rows.Next() { // Tomar los valores de la fila err = rows.Scan(&uid, &username, &department, &created) checkErr(err) fmt.Println(uid) fmt.Println(username) fmt.Println(department) fmt.Println(created) } rows.Close() // Close libera recursos del iterador
Para ejecutar consultas que manipulen datos debemos crear una
estructura sql.Stmt
.
// Insertar stmt, err := db.Prepare("INSERT INTO userinfo(username, departname, created) values(?,?,?)") res, err := stmt.Exec("pparker", "Avengers", "2021-01-01") // Actualizar stmt, err = db.Prepare("update userinfo set username=? where uid=?") res, err = stmt.Exec("spiderman", id) // Eliminar stmt, err = db.Prepare("delete from userinfo where uid=?") res, err = stmt.Exec(id) // Llamar Close para liberar recursos err := stmt.Close()
La mayoriá de los drivers para bases de dato NOSQL de Golang no
implementan las interfaces en database/sql
por lo que cada
biblioteca maneja sus propios tipos e interfaces.
// Necesitamos estos imports import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" )
bson
para serializar y deserializar hacia la base
de datos.json
para comunición entre servicios.type Item struct { ID int `json:"id,omitempty" bson:"id,omitempty"` Title string `json:"title,omitempty" bson:"title,omitempty"` IsDone bool `json:"isdone,omitempty" bson:"isdone,omitempty"` }
type MongoDB struct { *mongo.Client } func NewMongoDB(ctx context.Context) (*MongoDB, error) { client, err := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017")) if err != nil { return nil, err } return &MongoDB{client}, nil } func (m *MongoDB) Disconnect(ctx context.Context) { defer m.Client.Disconnect(ctx) }
func (m *MongoDB) CreateItem(ctx context.Context, newItem Item) string { collection := m.Database("todo").Collection("items") result, _ := collection.InsertOne(ctx, newItem) return result.InsertedID.(primitive.ObjectID).Hex() } func (m *MongoDB) UpdateItem(ctx context.Context, item Item) { update := bson.M{"$set": bson.M{"title": item.Title, "isdone": item.IsDone}} collection := m.Database("todo").Collection("items") collection.UpdateOne(ctx, Item{ID: item.ID}, update) }
func (m *MongoDB) GetItems(ctx context.Context) (items []Item) { collection := m.Database("todo").Collection("items") cursor, _ := collection.Find(ctx, bson.M{}) defer cursor.Close(ctx) for cursor.Next(ctx) { var oneItem Item cursor.Decode(&oneItem) items = append(items, oneItem) } return }
func (m *MongoDB) GetItem(ctx context.Context, id int) (item Item) { collection := m.Database("todo").Collection("items") collection.FindOne(ctx, Item{ID: id}).Decode(&item) return }
func (m *MongoDB) DeleteItem(ctx context.Context, id int) { collection := m.Database("todo").Collection("items") collection.DeleteMany(ctx, Item{ID: id}) return }
Echo es un microframework Golang para crear servicios web.
:CUSTOMID: rutas-middleware
echo.Context
representa el acceso al estado de la solicitud (ruta,
parámetros, handlers, etc) y contiene los métodos para generar las
respuesta
func updateUser (c echo.Context) (err error) { // ... }
import "github.com/labstack/echo/v4" func main(){ e := echo.New() e.POST("/users", createUser) e.GET("/users/:id", findUser) e.PUT("/users/:id", updateUser) e.DELETE("/users/:id", deleteUser) e.Any("/",home) }
Establecer opciones similares para varias rutas.
//Todas las URLs con /v2/* g := e.Group("/v2") e.POST("/users", createUserV2) e.GET("/users/:id", findUserV2) e.PUT("/users/:id", updateUserV2) e.DELETE("/users/:id", deleteUserV2)
Funciones que se procesan antes de un handler.
func MyMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { //Hacer algo return next(c) } }
//Antes de ejecutarse el router e.Pre(MyMiddleware) //Después de ejecutar el router e.Use(MyMiddleware) // A nivel de grupo admin := e.Group("/admin", MyMiddleware) //A nivel de ruta e.GET("/", <Handler>, <Middleware...>)
func(c echo.Context) error { name := c.FormValue("name") // valor de formulario printFull := c.QueryParam("full") // valor de query return c.String(http.StatusOK, name) }
Bind
type User struct { Name string `json:"name" form:"name" query:"name"` Email string `json:"email" form:"email" query:"email"` } func handle(c echo.Context) (err error) { u := new(User) if err = c.Bind(u); err != nil { return } // Hacer algo con el usuario }
echo.Context
es también utilizada para generar respuestas.
// Retornar una cadena c.String(http.StatusOK, "Hello, World!") // Retornar HTML c.HTML(http.StatusOK, "<p>Hello, World!</p>") // Retorna JSON, serializa el valor de u c.JSON(http.StatusOK, u) // Retorna XML, serializa el valor de u c.XML(http.StatusOK, u) // Retorna el contenido del fichero c.File("<PATH_TO_YOUR_FILE>") // Retorna el contenido del fichero como flujo de datos c.Stream(http.StatusOK, "<CONTENT_TYPE>", file) // Redirige c.Redirect(http.StatusMovedPermanently, "<URL>")
sarama
sarama
es una biblioteca desarrollada por Shopify para la
comunicación con Apache Kafka
Para utilizar sarama
solo tenemos que importar el módulo
import "github.com/Shopify/sarama"
config := sarama.NewConfig() config.Producer.RequiredAcks = sarama.WaitForAll // Wait for all in-sync replicas to ack the message config.Producer.Retry.Max = 10 // Retry up to 10 times to produce the message config.Producer.Return.Successes = true tlsConfig := createTlsConfiguration() if tlsConfig != nil { config.Net.TLS.Config = tlsConfig // de crypto/tls config.Net.TLS.Enable = true }
sarama.SyncProducer
bloquea la gorutina en espera de confirmación
brokerlist := [...]string{"host1:por1", "host2:port2"} producer := sarama.NewSyncProducer(brokerList, config) partition, offset, err := s.DataCollector.SendMessage(&sarama.ProducerMessage{ Key: sarama.StringEncoder("llave"), Topic: "topic", Value: sarama.StringEncoder("Mensaje"), }) producer.Close() //Liberar recursos
sarama.AsyncProducer
envía de forma asíncrona.
brokerlist := [...]string{"host1:por1", "host2:port2"} producer, err := sarama.NewAsyncProducer(brokerList, config) producer.Input() <- &sarama.ProducerMessage{ Key: sarama.StringEncoder("llave"), Topic: "topic", Value: sarama.StringEncoder("Mensaje"),, } producer.AsyncClose() // Liberar recursos
sarama
sarama
permite dos modos de recepción de mensajes.
sarama.Consumer
sarama.ConsumerGroup
En la carpeta day-07/21-sarama/consumergroup
se incluye un ejemplo
de consumidor usando sarama.ConsumerGroup
Los interceptors permiten procesar un mensaje antes de que sea enviado o recibido.
// func (m *myinterp) OnSend(p *sarama.ProducerMessage) { // se procesa el mensaje } // Configuración del productor conf.Producer.Interceptors = []sarama.ProducerInterceptor{&myinterp{}}
// Implementar sarama.ConsumerInterceptor func (m *myinterp) OnConsume(p *sarama.ConsumerMessage) { // se procesa el mensaje } // Configuración del consumidor conf.Consumer.Interceptors = []sarama.ConsumerInterceptor{&myinterp{}}
Las futures representan el resultado de un cálculo que se ejecuta de forma concurrente. Este patrón es nos permite ejecutar una operación costosa sin que impacte la ejecución del proceso principal.
Definamos una interfaz que exprese el comportamiento que queremos en un future
type Value interface{} type Future interface { Get(c context.Context) (Value, error) }
Usamos context.Context
para dar la posibilidad de cancelar la espera.
Una implementación simple para future es usar un canal que comunique el estado de completamiento o error.
type result struct { value Value err error } type futureImpl struct { result chan *result }
Por conveniencia las estructuras son privadas.
El método Get
bloquea la gorutina actual en espera del resultado del
future o la señal Done
del contexto.
func (f *futureImpl) Get(c context.Context) (Value, error) { select { case <-ctx.Done(): return nil, ctx.Err() case result := <-f.result: return result.value, result.err } }
Finalmente una función para crear nuevas futures.
func NewFuture(f func() (Value, error)) Future { fut := &futureImpl { result: make(chan *result) } go func(){ defer close(fut.result) value, err := f() f.result <- &result{value, err} }() return fut }