Logo de Go

Go

Herramientas

Comandos

Conceptos

Go es un lenguaje de programación de código abierto que facilita la creación de software simple, confiable y eficiente. Es un lenguaje compilado, concurrente, imperativo, estructurado y orientado a objetos.

El punto de entrada de un programa en Go es la función main que se encuentra en el paquete main.

Estructura de un archivo .go:

package main

import "fmt"

func main() {
  fmt.Println("Hello, World!")
}

Si el nombre de una función, variable, etc., empieza con mayúscula, significa que es pública y se puede acceder desde otros paquetes. Si empieza con minúscula, es privada y solo se puede acceder desde el paquete en el que se encuentra.

Variables

Las variables son pasados por valor.
Si se declaran fuera de la función, quedan declaradas a nivel de paquete y se pueden usar en cualquier parte del paquete.

var name string = "John"
var age int = 30
// declarar varias variables
var name, lastName string = "John", "Doe"
// declarar varias variables de distinto tipo
var (
  name string = "John"
  age int = 30
)
// se puede declarar y despues inicializar
var name string
name = "John"
// declarar y dejar que Go infiera el tipo
var name = "John"
// declarar e inicializar sin usar var, solo se puede hacer dentro de una función
name := "John"
name, age := "John", 30
// byte
var a byte = 'a' // 97 (ASCII)
s := "Hello"
b := s[0] // 72 (ASCII)
// rune
var r rune = '' // 9829 (Unicode)

Constantes

Si se declaran fuera de la función, quedan declaradas a nivel de paquete y se pueden usar en cualquier parte del paquete.

const name = "John"
const age = 30
const (
  name = "John"
  age = 30
)

// iota es una constante que se incrementa en 1 en cada constante, empieza en 0
const (
  lunes = iota
  martes
  miercoles
  jueves
  viernes
  sabado
  domingo
)
// lunes = 0, martes = 1, miercoles = 2, jueves = 3, viernes = 4, sabado = 5, domingo = 6

Tipos de datos

Cada tipo de dato tiene un valor por defecto.

A menos que se requiera optimizar recursos al máximo, el estándar de tipos que se deberia usar sería int, uint32, float64.
Si solo ponemos int o uint Go infiere el tamaño del entero dependiendo del sistema operativo, si es de 32 bits sera int32 y si es de 64 bits sera int64.

Tambien se pueden crear tipos de datos personalizados:

type Age int
var age Age = 30

Conversión de tipos (casting)

Para hacer operaciones con tipos de datos diferentes, se deben convertir.

temperatureInt := 88
temperatureFloat := float64(temperatureInt)

Si se quiere convertir de número a string o viceversa, se puede hacer con el paquete strconv.

import "strconv"
temperatureInt := 88
temperatureString := strconv.Itoa(temperatureInt)

temperatureString := "88"
// el segundo valor que devuelve es un error, si no se logra hacer la conversion
temperatureInt, _ := strconv.Atoi(temperatureString)

Type assertions

Se usa para convertir un tipo de dato a otro tipo de dato.

var i interface{} = "Juan"
// se convierte a string, si no se puede convertir, devuelve "" y false
j, ok := i.(string)
if ok {
  fmt.Println(strings.ToUpper(j))
}

Type switches

Se usa para hacer un switch de tipos.

var i interface{} = "Juan"
switch v := i.(type) {
case int:
  fmt.Println("Es un entero")
case string:
  fmt.Println("Es un string")
default:
  fmt.Println("No se que es")
}

Arrays

Los arrays son una estructura fija, no se puede cambiar su tamaño.

var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3
// se puede declarar e inicializar
arr := [3]int{1, 2, 3}
// se puede declarar e inicializar sin poner el tamaño, Go infiere el tamaño
arr := [...]int{1, 2, 3}
// obtener la longitud del array
len(arr)

// arrays multidimensionales
var arr [3][3]int
// se puede declarar e inicializar
arr := [3][3]int{
  {1, 2, 3},
  {4, 5, 6},
  {7, 8, 9}
}

Slices

Los slices son una referencia a un array, son punteros a un array. Los arrays son una estructura fija, no se puede cambiar su tamaño, los slices son una estructura dinámica, se puede cambiar su tamaño.
Si creo un slice a partir de un array y modifico el slice, tambien se modifica el array original.

// se crea un array
arr := [3]int{1, 2, 3}
// se puede crear un slice a partir de un array, haria referencia a todo el array
slice := arr[:]
// se puede crear un slice a partir de un array, desde la primera posicion hasta la indicada
slice := arr[:2] // [1, 2]
// se puede crear un slice a partir de un array, desde la posicion indicada hasta el final
slice := arr[1:] // [2, 3]
// se puede crear un slice a partir de un array con un rango
slice := arr[1:3] // [2, 3]

// se puede crear un slice sin un array
slice := []int{1, 2, 3}
// se puede crear un slice con un tamaño,
slice := make([]int, 3)
// se puede crear un slice con un tamaño y capacidad
slice := make([]int, 3, 5)
// se puede agregar un elemento a un slice
slice = append(slice, 4)
// se puede agregar varios elementos a un slice
slice = append(slice, 5, 6, 7)
// se puede agregar un slice a otro slice
slice = append(slice, []int{8, 9, 10}...)
// se puede eliminar un elemento de un slice, se elimina el elemento en la posicion 2,
// se copian los elementos desde el inicio hasta la posicion 1 (2 no esta incluido) y desde la posicion 3
// hasta el final del slice, los ... es para desempaquetar el slice
slice = append(slice[:2], slice[3:]...)
// se puede eliminar el primer elemento de un slice
slice = slice[1:]
// se puede eliminar el ultimo elemento de un slice
slice = slice[:len(slice)-1]
// se puede copiar un slice
slice2 := make([]int, len(slice))
copy(slice2, slice)
// longitud de un slice
len(slice)
// capacidad de un slice, es el numero de elementos del array original, a partir de donde se
// creo el slice hasta el final del array
animal := [5]string{"gato", "perro", "conejo", "pajaro", "pez"}
slice := animal[1:3]
cap(slice) // 4

Maps

Son una estructura de datos que permiten almacenar y recuperar datos por llave-valor.

// se puede declarar e inicializar
m := map[string]int{
  "age": 30,
  "height": 180
}
// se puede declarar e inicializar sin valores
m := map[string]int{}
// se puede declarar e inicializar sin valores
m := make(map[string]int)
// agregar un valor
m["age"] = 30
// obtener un valor
m["age"]
// obtener un valor y si existe
value, ok := m["age"]
// obtener un valor y si existe
if value, ok := m["age"]; ok {
  fmt.Println(value)
}
// eliminar un valor
delete(m, "age")
// longitud de un map
len(m)
// recorrer un map
for key, value := range m {
  fmt.Println(key, value)
}

Estructuras (Structs)

Es un tipo que contiene otros tipos, de tipo llave-valor. Similar a una clase de JS. Se usa la primera letra en mayúscula para que sea pública y se pueda acceder desde otros paquetes. La primera letra en minúscula para que sea privada y solo se pueda acceder desde el paquete en el que se encuentra.

// esta seria una nested struct
type Person struct {
  Name string
  Age uint8
  AddressInfo Address
}

type Address struct {
  Street int
  City string
}

// cuando se trabaja con json, se indica el nombre de la llave que se va a usar el json si queremos que sea diferente
type Person struct {
  Name string `json:"name"`
  Age uint8 `json:"age"`
  AddressInfo Address `json:"address"`
}

// crear “instancia”, si solo se ponen las llaves, los valores se iniciaran con su valor por defecto
myPerson := Person{}
myPerson.AddressInfo.City = "Pereira"
myPerson.Name = "John"
myPerson.Age = 30
// crear “instancia” con valores
myPerson := Person{
  Name: "John",
  Age: 30,
  AddressInfo: Address{
    City: "Pereira",
    Street: 123
  }
}

Estructuras anónimas:
Cuando se usan las estructuras anónimas, se deben instanciar inmediatamente. Se deberían usar cuando solamente se crea una instancia de esta.

myPerson := struct {
  Name string
  Age int
} {
  Name: "John"
  Age: 30
}

Embedded structs:
Son estructuras que contienen otras estructuras, se puede decir que “heredan” las propiedades y metodos de la otra struct. Se diferencian de las nested struct porq en estas solo una de sus propiedades hace referencia a otra struct, en las embedded struct se heredan las propiedades de la otra struct.

type person struct {
  name string
  age int
}
func (p person) greet() {
  fmt.Println("Hello")
}
type employee struct {
  person
  salary int
}
// para usarla seria:
myEmployee := employee{
  salary: 5000,
  person: person{
    name: "Jhon",
    age: 30
  }
}
myEmployee.name // Jhon
myEmployee.greet() // Hello

Métodos en estructuras:
Se vincula una función a una estructura, se puede decir que es un método de la estructura.

type rect struct {
  width int
  height int
}
func (r rect) area() int {
  return r.width * r.height
}
r := rect{
  width: 5,
  height: 10
}
fmt.Println(r.area()) // 50

Punteros en estructuras:
Como en todas las funciones en Go, las estructuras también se pasan por valor, si se quiere modificar la estructura original, se debe pasar un puntero a la estructura.
Si hay por lo menos un metodo que recibe un puntero, se deberia usar punteros en todos los metodos.

type Rect struct {
	width  int
	height int
}
func NewRect(width, height int) *Rect {
  return &Rect{width, height}
}
func (r *Rect) ChangeWidth(width int) {
	r.width = width
}
r := NewRect(5, 10)
r.ChangeWidth(10)
fmt.Println(r.width) // 10

Punteros

Un puntero es una dirección de memoria, es una variable que almacena la direccion de memoria de un valor o de otra variable. Se usan para referenciar y acceder a la variable original.

x := 10
// el & se usa para obtener la direccion de memoria de la variable
y := &x // y seria de tipo *int (puntero a un entero)
fmt.Println(y) // 0xc0000b6010
// el * se usa para obtener el valor de la direccion de memoria
fmt.Println(*y) // 10
// se podria modificar el valor de x a traves del puntero
*y = 20 // x ahora seria 20

Interfaces

Las interfaces definen la implementación de métodos.
Como recomendacion, se debe poner el nombre de la interfaz como el nombre del método con el sufijo er, ejemplo, si el metodo es Greet(), la interfaz seria Greeter.

type Circle struct {
	radius float64
}
func NewCircle(radius float64) *Circle {
  return &Circle{radius}
}
func (c *Circle) Area() float64 {
	return math.Pi * math.Pow(c.radius, 2)
}

type Square struct {
	width  float64
	height float64
}
func NewSquare(width, height float64) *Square {
  return &Square{width, height}
}
func (s *Square) Area() float64 {
	return s.width * s.height
}

type Sizer interface {
	Area() float64
}

func calculateArea(s Sizer) float64 {
	return s.Area()
}

func main() {
	c := NewCircle(10)
	s := NewSquare(5, 10)
	fmt.Println(calculateArea(c)) // 314.1592
	fmt.Println(calculateArea(s)) // 50
}

Las interfaces quedan definidas implícitamente, en el ejemplo si un struct implementa el método ‘Area()’, queda implícitamente relacionado con la interfaz Sizer. No se necesita decir explícitamente la palabra implements como en otros lenguajes. Las interfaces quedan desacopladas de la implementación del type.

Interfaces embebidas
Crea una interfaz que se compone de otras interfaces y de sus metodos.
Como recomendacion el nombre se compone de los nombres de las interfaces que contiene.

type Greeter interface {
  Greet() string
}

type Byer interface {
  Bye() string
}

type GreeterByer interface {
  Greeter
  Byer
}

Tambien se pueden retornar interfaces.
Ver curso-go-edteam/02-POO

Condicionales

if 10 > 2 {
  fmt.Println("Greater")
} else if 10 == 10 {
  fmt.Println("Equal")
} else {
  fmt.Println("Lower")
}

// También se pueden definir de la siguiente forma:
if declaracion_inicial; condicion {
  // codigo
}
// La variable `age` solo seria accesible en el scope del if. Se usa para limitar el scope de esta variable.
if age := 18; age < 18 {
  fmt.Println("Es menor de edad")
}

// en Go no es necesario poner la sentencia break, el break es implícito
age := 18
switch age {
  case 18:
    fmt.Println("Tiene 18 años")
  case 20:
    fmt.Println("Tiene 20 años")
  default:
    fmt.Println("No tiene 18 ni 20 años")
}
// tambien se puede definir de la siguiente forma:
switch age := 18; age {
  case 18:
    fmt.Println("Tiene 18 años")
  case 20:
    fmt.Println("Tiene 20 años")
  default:
    fmt.Println("No tiene 18 ni 20 años")
}
// se pueden agrupar condiciones
switch age := 18; age {
  case 18, 20:
    fmt.Println("Tiene 18 o 20 años")
  default:
    fmt.Println("No tiene 18 ni 20 años")
}
// se pueden usar condiciones en los cases
switch age := 18; {
  case age < 18:
    fmt.Println("Es menor de edad")
  case age == 18:
    fmt.Println("Tiene 18 años")
  case age > 18:
    fmt.Println("Es mayor de edad")
}

Ciclos

for i := 0; i < 5; i++ {
  if i == 4 {
    break
  }
  if i == 2 {
    continue
  }
  fmt.Println(i)
}

// ciclo infinito
for {
  fmt.Println("Hola")
}

// ciclo "while"
i := 0
for i < 5 {
  fmt.Println(i)
  i++
}

// ciclo for range, se usa para recorrer arrays, slices, maps, strings
arr := []int{1, 2, 3}
for index, value := range arr {
  fmt.Println(index, value)
}

Funciones

func sum(x int, y int) int {
  return x + y
}

Si los parametros son del mismo tipo, se puede declarar solo una vez:

func sum(x, y int) int {
  return x + y
}

Se pueden pasar funciones a otras funciones, similar a como se hace en javascript con los callbacks:

func myFunc(func(x int, y int), int) int {
  // codigo
}

myFunc seria una funcion que toma 2 argumentos, el primero es una funcion que toma 2 enteros como argumentos y devuelve un entero, y el segundo argumento es un entero. myFunc devuelve un entero.

Se pueden devolver múltiples valores de una función:

func getNames() (string string) {
  return "Jhon", "Doe"
}
name, lastName := getNames()

Se puede ignorar un valor de retorno:

name, _ := getNames()
// con _ se ignora completamente esa variable, el compilador la remueve

Se pueden nombrar los valores de retorno:

func getCoords() (x, y int){
  // x y ‘y’ se inicializan con 0
  return // retorna implícitamente x y ‘y’
}
// seria la forma abreviada de escribir:
func getCoords() (int, int){
  var x int
  var y int
  return x, y
}

Igualmente lo recomendado es un retorno explicito, poner explicitamente x y ‘y’ en el return.

Funciones variádicas

Son funciones que pueden recibir un número variable de argumentos.

func sum(nums ...int) int {
  total := 0
  for _, num := range nums {
    total += num
  }
  return total
}
sum(1, 2, 3, 4, 5) // 15

// se pueden pasar varios tipos de datos (Genericos)
func PrintList(values ...interface{}) int {
  for _, value := range values {
    fmt.Println(value)
  }
}
PrintList(1, "Juan", true, 4, "Pablo") // 1, Juan, true, 4, Pablo

Funciones recursivas

Las funciones recursivas son funciones que se llaman a sí mismas.

func factorial(n int) int {
  if n == 0 {
    return 1
  }
  return n * factorial(n-1)
}
factorial(5) // 120

Funciones anónimas

Las funciones anónimas son funciones que no tienen nombre.

func main() {
  func() {
    fmt.Println("Hello")
  }()
}
// puedo asignar la funcion anonima a una variable
f := func(name string) {
  return "Hello " + name
}
f("Juan") // Hello Juan

Funciones de orden superior

Las funciones de orden superior son funciones que toman otras funciones como argumentos o devuelven funciones.

func main() {
  result := calculate(10, 5, sum)
  fmt.Println(result) // 15
}
func sum(x, y int) int {
  return x + y
}
func calculate(x, y int, f func(int, int) int) int {
  return f(x, y)
}

Closures

Un closure es una función que captura variables del entorno en el que fue creada.

func main() {
  f := increment()
  fmt.Println(f()) // 1
  fmt.Println(f()) // 2
  fmt.Println(f()) // 3
}

func increment() func() int {
  i := 0
  return func() int {
    i++
    return i
  }
}

Genericos

Son una forma de escribir código que funciona con cualquier tipo de dato.
Desde la versión 1.18 de Go, se añadió soporte para genéricos.

// se pueden pasar varios tipos de datos
func PrintList(values ...interface{}) int {
  for _, value := range values {
    fmt.Println(value)
  }
}
PrintList(1, "Juan", true, 4, "Pablo") // 1, Juan, true, 4, Pablo

// tambien se puede usar any
func PrintList(values ...any) int {
  for _, value := range values {
    fmt.Println(value)
  }
}
PrintList(1, "Juan", true, 4, "Pablo") // 1, Juan, true, 4, Pablo

// se puede poner tipos a los parametros, crear una restriccion (constraint) de tipo
func Sum[T int | float64](nums ...T) T {
  var total T
  for _, num := range nums {
    total += num
  }
  return total
}
Sum[int](1, 2, 3, 4, 5) // 15
Sum[float64](1.5, 2.5, 3.5, 4.5, 5.5) // 17.5

// se puede usar tipos de datos derivados o por aproximacion
type MyInt int
var num MyInt = 10
var num2 MyInt = 20
// ~ se usa para decir que el tipo de dato es aproximado o derivado
func Sum[T ~int | float64](nums ...T) T {
  var total T
  for _, num := range nums {
    total += num
  }
  return total
}
Sum[int](num, num2) // 30

// otra forma de crear un constraint. Tambien podria usar el paquete constraints que ya tiene tipos definidos
type Numeric interface {
  ~int | ~float64 | ~float32 | ~uint
}
func Sum[T Numeric](nums ...T) T {
  var total T
  for _, num := range nums {
    total += num
  }
  return total
}
Sum(1, 2, 3, 4, 5) // 15

// estructura generica
type Product[T uint | string] struct {
  Id T
  Name string
  Price float64
}
product1 := Product[uint]{Id: 1, Name: "Product 1", Price: 10.5}
product2 := Product[string]{Id: "1", Name: "Product 2", Price: 20.5}

Defer

Se usa para posponer la ejecución de una función hasta que la función que la contiene termine.

func main() {
  defer fmt.Println("World")
  fmt.Println("Hello")
}
// Hello
// World

Se ejecutan en el orden inverso en el que se declaran.

func main() {
  defer fmt.Println("World")
  defer fmt.Println("Hello")
}
// Hello
// World

Se usa para cerrar archivos, conexiones, liberar recursos, etc.

file, err := os.Open("file.txt")
defer file.Close()
if err != nil {
  fmt.Println("Error al abrir el archivo")
  return
}

Manejo de errores

Go no tiene excepciones, se manejan los errores con el tipo error.

func divide(x, y int) (int, error) {
  if y == 0 {
    return 0, errors.New("No se puede dividir por 0")
  }
  return x / y, nil
}
result, err := divide(10, 0)
if err != nil {
  fmt.Println(err)
} else {
  fmt.Println(result)
}

// se puede crear un error personalizado
type customError struct {
  message string
}
func (e *customError) Error() string {
  return e.message
}
func divide(x, y int) (int, error) {
  if y == 0 {
    return 0, &customError{"No se puede dividir por 0"}
  }
  return x / y, nil
}
result, err := divide(10, 0)
if err != nil {
  fmt.Println(err.Error()) // No se puede dividir por 0
} else {
  fmt.Println(result)
}

// manejo de errores en funciones anidadas
var errNotFound = errors.New("not found")

var food = map[int]string{
	1: "🍕",
	2: "🍔",
}

func main() {
	found, err := search("34")
  // usando el paquete errors, comprobar si el error es igual a otro
	if errors.Is(err, errNotFound) {
		fmt.Println("pudimos controlar el error correctamente")
		return
	}
	if err != nil {
    // le doy contexto al error, diciendo de donde viene
		fmt.Println("search()", err)
		return
	}
	fmt.Println(found)
}

func search(key string) (string, error) {
	num, err := strconv.Atoi(key)
	if err != nil {
    // le doy contexto al error, diciendo de donde viene
    // %w es para envolver el error y despues poder compararlo con errors.Is()
		return "", fmt.Errorf("strconv.Atoi(): %w", err)
	}

	emoji, err := findFood(num)
	if err != nil {
    // le doy contexto al error, diciendo de donde viene
    // %w es para envolver el error y despues poder compararlo con errors.Is()
		return "", fmt.Errorf("findFood(): %w", err)
	}
	return emoji, nil
}

func findFood(id int) (string, error) {
	value, ok := food[id]
	if !ok {
		return "", errNotFound
	}
	return value, nil
}

Panic y Recover

Cuando existe un panic, deberiamos leer los errores en consola de abajo hacia arriba.

panic se usa para detener la ejecución de un programa. recover se usa para recuperar el control del programa después de un panic.

func main() {
	division(100, 10)
	division(34, 0)
	division(120, 10)
}

func division(dividend, divisor int) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("me recupere del panic")
		}
	}()

	validateZero(divisor)
	fmt.Println(dividend / divisor)
}

func validateZero(divisor int) {
	if divisor == 0 {
		panic("🤕 no puedes dividir por cero")
	}
}
// 10
// me recupere del panic
// 12

Concurrencia (Goroutine)

La concurrencia es la capacidad de ejecutar varias tareas al mismo tiempo.
Las Goroutines es una función que se ejecuta de forma independiente en el mismo espacio de tiempo que otras Goroutines, son hilos de ejecución ligeros que se pueden ejecutar en paralelo.

Go tiene minimo una Goroutine que es la principal (main), la que se ejecuta cuando se inicia el programa. Go utiliza el modelo “fork-join” para crear Goroutines, se crea una Goroutine principal y se crean Goroutines hijas que se ejecutan de forma independiente y en algun punto vuelven a la principal, como se ve en la imagen. Para crear una Goroutine se usa la palabra clave go.

Para crear los puntos de unión (join-points) se necesita sincronizar la Goroutine padre con la hija, se puede hacer con el paquete sync, los canales o esperar que la Goroutine hija finalice, es recomendable usar el paquete sync o los canales.

Modelo de Goroutine

Los valores de retorno de las Goroutines hijas son descartados, no se pueden recuperar estos valores en las Goroutines padres, si se necesita recuperar el valor de retorno se debe usar un canal.

Para trabajar con Goroutines se tienen 2 enfoques, a traves del paquete sync y a traves de canales. El paquete sync se usa para sincronizar las Goroutines, solo queremos que las Goroutines terminen su ejecución; los canales se usan para comunicar las Goroutines.

func main() {
  // go permite ejecutar la funcion en un hilo separado
  go sayHello()
  fmt.Println("Hello")
}
func sayHello() {
  fmt.Println("World")
}

Problemas de concurrencia:

WaitGroup

El paquete sync se usa para sincronizar las Goroutines, se puede usar WaitGroup para esperar a que todas las Goroutines terminen su ejecución.

func main() {
  // instancia de WaitGroup
  var wg sync.WaitGroup
  // definir el contador, en este caso, agregar 2 Goroutines
  wg.Add(2)
  go func() {
    // cuando la Goroutine termina, se llama a Done y se resta 1 del contador
    defer wg.Done()
    fmt.Println("Hello")
  }()
  go func() {
    defer wg.Done()
    fmt.Println("World")
  }()
  // esperar a que todas las Goroutines terminen, es decir, que el contador llegue a 0
  wg.Wait()
}

Mutex

El paquete sync se usa para sincronizar las Goroutines, se puede usar Mutex para evitar que las Goroutines compitan por acceder a la misma variable.

func main() {
	// instancia de Mutex
	mu := sync.Mutex{}
  // instancia de WaitGroup
	wg := sync.WaitGroup{}
  // definir el contador, en este caso, agregar 1 Goroutines
	wg.Add(1)

	data := 1

	go func() {
  	// bloquear el Mutex, para que solo una Goroutine pueda acceder a la sección crítica
		mu.Lock()
		data++
  	// desbloquear el Mutex, para que otra Goroutine pueda acceder a la sección crítica
		mu.Unlock()
  	// cuando la Goroutine termina, se llama a Done y se resta 1 del contador
		wg.Done()
	}()
  // esperar a que todas las Goroutines terminen, es decir, que el contador llegue a 0
	wg.Wait()
	mu.Lock()
	fmt.Println(data)
	mu.Unlock()
}

Data race detector

Go tiene una herramienta para detectar el problema de Data race, este solo funciona en tiempo de ejecución, es decir, solo funciona en el código que ejecuta, no en todo el código. Se debe ejecutar el programa con la bandera -race. Ej: go run main.go -race

Canales (Channels)

Los canales se usan para comunicar Goroutines. Es el prinicpal metodo de sincronizacion entre Goroutines.
Se pueden enviar y recibir valores de un canal.
Los canales son bloqueantes, la rutina se bloquea hasta que se reciba el mensaje. Ya no se tiene que hacer el bloqueo de forma manual, los canales se encargan de esto.
Se debe asegurar que alguna rutina envie el mensaje y otra lo reciba, si no se hace, se produce un deadlock.

func main() {
  // declarar un canal
  ch := make(chan string)
  go func() {
    // enviar un valor al canal
    ch <- "Hello"
  }()
  // recibir un valor del canal
  msg := <-ch
  fmt.Println(msg)
}

En la imagen se puede ver como funciona un canal.

Modelo de Goroutine

Se pueden enviar y recibir valores de un channel de forma asíncrona.

func main() {
  // declarar un canal
  ch := make(chan string)
  go func() {
    // enviar un valor al canal
    ch <- "Hello"
  }()
  go func() {
    // recibir un valor del canal
    msg := <-ch
    fmt.Println(msg)
  }()
}

Se puede indicar que una función solo reciba o envie valores de un canal.

// chan<- int: canal de solo escritura, solo puede enviar valores
func send(ch chan<- string) {
	// enviar valores al canal
  ch <- "Hello"
}
// <-chan int: canal de solo lectura, solo puede recibir valores
func receive(ch <-chan string) {
  // recibir valores del canal
  msg := <-ch
  fmt.Println(msg)
}
func main() {
	// crear canal
  ch := make(chan string)
	// crear gorutinas
  go send(ch)
  go receive(ch)
}

Se pueden enviar y recibir valores de un canal con buffer, la funcion de envio puede ir enviando valores, sin que quede bloqueada hasta que haya una operacion de lectura. Enviar se bloquea cuando el buffer esta lleno, y recibir se bloquea cuando el buffer esta vacio.

func main() {
  // crear canal con buffer de 2
  ch := make(chan string, 2)
  // enviar valores al canal
  ch <- "Hello"
  ch <- "World"
  // recibir valores del canal
  fmt.Println(<-ch)
  fmt.Println(<-ch)
}

Se pueden cerrar los canales, quien envia los valores debe cerrar el canal, para que el receptor sepa que no se enviaran mas valores.

func main() {
  // declarar un canal
  ch := make(chan string)
  go func() {
    // enviar valores al canal
    ch <- "Hello"
		ch <- "World"
    // cerrar el canal
    close(ch)
  }()
  for msg := range ch {
    fmt.Println(msg)
  }
}
// Hello
// World

Testing

Los tests se deben crear en un archivo con el nombre del archivo que se va a testear seguido de _test.go.
Para ejecutar los tests: go test ó go test -v para ver los detalles (para ver los logs se deberia usar con -v).

// archivo a testear: main.go
package main

func sum(x, y int) int {
  return x + y
}

// archivo de test: main_test.go
package main

import "testing"

func TestSum(t *testing.T) {
  // para testear varias condiciones
  table := []struct {
    x, y, expected int
  }{
    {5, 5, 10},
    {2, 3, 5},
    {0, 0, 0},
  }

  for _, test := range table {
    total := Sum(test.x, test.y)
    if total != test.expected {
    	t.Errorf("Expected %d, got %d", test.expected, total)
    }
  }
}

Paquete fmt

Sirve para formatear texto e imprimir y recuperar datos en consola.

var name string
fmt.Println("Ingrese su nombre:")
// se pone la referencia de la variable que va a recibir el valor
fmt.Scanln(&name)
fmt.Println("Hola", name)

Con el paquete “fmt” se puede interpolar.

// Interpolar valores generales:
fmt.Printf("I am %v years old", 10) // I am 10 years old
fmt.Printf("I am %v years old","too many") // I am too many years old

// Interpolar string:
fmt.Printf("I am %s years old", "too many") // I am too many years old

// Interpolar integer:
fmt.Printf("I am %d years old", 10) // I am 10 years old

// Interpolar float
fmt.Printf("I am %f years old", 10.05) // I am 10.05 years old
fmt.Printf("I am %.2f years old", 10.0534) // I am 10.05 years old

Paquete strings

Sirve para trabajar con cadenas de texto.

import "strings"
// concatenar cadenas
strings.Join([]string{"Hello", "World"}, " ") // Hello World
// separar cadenas
strings.Split("Hello World", " ") // [Hello World]
// buscar una cadena
strings.Contains("Hello World", "World") // true
// reemplazar una cadena
strings.Replace("Hello World", "World", "Go", 1) // Hello Go
// convertir a mayúsculas
strings.ToUpper("Hello") // HELLO
// convertir a minúsculas
strings.ToLower("Hello") // hello
// obtener la longitud de una cadena
len("Hello") // 5
// contar cuantas veces aparece una cadena
strings.Count("Hello", "l") // 2
// obtener el indice de una cadena
strings.Index("Hello", "l") // 2
// obtener el indice de una cadena desde la derecha
strings.LastIndex("Hello", "l") // 3
// obtener si una cadena empieza con otra
strings.HasPrefix("Hello", "He") // true
// obtener si una cadena termina con otra
strings.HasSuffix("Hello", "lo") // true

Operadores y Paquete math

Operadores: + - * / % ++ —
Operadores de comparación: == != < > <= >= Operadores lógicos: && || !

// operadores en asignacion
x := 10
x += 5 // 15 (x = x + 5)
x -= 5 // 10 (x = x - 5)
x++ // 11
x-- // 9

// paquete math
import "math"
math.Pi
math.Pow(2, 3) // 8
math.Sqrt(9) // 3
math.Round(3.14) // 3
math.Floor(3.14) // 3
math.Ceil(3.14) // 4
math.max(1, 2) // 2
math.min(1, 2) // 1
math.abs(-1) // 1
math.IsNaN(0/0) // true

// paquete math/rand para generar numeros aleatorios
import "math/rand"
rand.Intn(100) // numero aleatorio entre 0 y 100
rand.Float64() // numero aleatorio entre 0 y 1

Paquete time

Paquete para trabajar con fechas y horas.

import "time"
time.Now()
time.Now().Year()
time.Now().Month()
time.Now().Day()
time.Now().Hour()
time.Now().Minute()
time.Now().Second()
time.Now().Weekday()
time.Now().Add(time.Hour * 24)
time.Now().Sub(time.Now().Add(time.Hour * 24))

Paquete constraints

Paquete para trabajar con constraints.

import "constraints"
// se puede usar para restringir el tipo de dato
func sum[T constraints.Integer | constraints.Float](x, y T) T {
  return x + y
}
sum(1, 2) // 3

El tipo de dato constraints.Ordered se usa para restringir el tipo de dato a los que se pueden comparar (>, <, <=, >=, ==, !=). Estos son todos los enteros, flotantes y strings.

Paquete os

Paquete para trabajar con el sistema operativo.

import "os"
os.Getwd() // obtener el directorio de trabajo
os.Mkdir("folder", 0777) // crear un directorio, 0777 es el permiso
os.MkdirAll("folder/subfolder", 0777) // crear un directorio con subdirectorios
os.Remove("folder") // eliminar un directorio
os.RemoveAll("folder") // eliminar un directorio con subdirectorios
os.Create("file.txt") // crear un archivo
os.Remove("file.txt") // eliminar un archivo

Pquete runtime

Paquete para trabajar con el runtime de Go.

import "runtime"
runtime.GOOS // obtener el sistema operativo
runtime.NumCPU() // obtener el número de CPUs
runtime.NumGoroutine() // obtener el número de gorutinas

Paquete log

Paquete para trabajar con logs.

import "log"
log.Println("Hello") // imprimir en consola
log.Fatalln("Error") // imprime y termina la ejecución del programa
log.Panicln("Panic") // imprime y termina la ejecución del programa y muestra el stack trace
log.SetPrefix("Prefix: ") // establecer un prefijo

Paquete http

Paquete para trabajar con servidores web, permite crear servidores y clientes HTTP.

Existen diferentes tipos de handlers predefinidos:

import (
  "net/http"
  "fmt"
)

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello")
}

func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":8080", nil)
}

La estructura http.Request contiene la informaciòn de la petición de un cliente, se pueden encontrar los siguientes campos:

Los metodos màs comunes de http.Request son:

La estructura http.ResponseWriter se usa para enviar la respuesta al cliente, se pueden encontrar los siguientes métodos:

Se puede modificar la estructura Serve para personalizar el servidor:

import (
  "net/http"
  "fmt"
)

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello")
}

func main() {
  http.HandleFunc("/", handler)

  server := http.Server {
    Addr: ":8080",
    Handler: nil,
    ReadTimeout: 10 * time.Second,
    WriteTimeout: 10 * time.Second,
    MaxHeaderBytes: 1 << 20,
  }

  server.ListenAndServe()
}

Bases de datos

Go no tiene un driver nativo para bases de datos, se deben instalar los drivers de las bases de datos que se van a usar. Se debe instalar el driver en el proyecto, se puede encontrar y como instalar en la página de los drivers de Go, https://go.dev/wiki/SQLDrivers , se selecciona el driver que se va a usar y se siguen las instrucciones. Es recmendable instalar los drivers que estan marcados con [*]

Consultar información a la base de datos se hace a traves del SELECT y los metodos Query, QueryContext (este a diferencia del Query, recibe un contexto) y QueryRow (asegura que solo se devuelva una fila), para insertar, actualizar, eliminar y demas comandos se usa el metodo Exec y ExecContext (recibe un contexto).

Si quiero utilizar las filas, lo puedo hacer usando el metodo Query que me devuelve las filas (recordar que debo cerrar las filas para liberar el recurso), si quiero trabajar solo con el resultado de la consulta puedo usar el metodo Exec.

Instrucciones preparadas

Le dice a la base de datos que prepare una consulta y que la ejecute varias veces con diferentes valores, esto es mas eficiente que ejecutar toda la consulta varias veces.


func main() {
  // ejecutar una consulta preparada
  stmt, err := db.Prepare("INSERT INTO products (name) VALUES (?)")
  if err != nil {
    log.Fatal(err)
  }
  defer stmt.Close()

  result1, err := stmt.Exec("Product 1")
  if err != nil {
    log.Fatal(err)
  }

  result2, err := stmt.Exec("Product 2")
  if err != nil {
    log.Fatal(err)
  }
}

Transacciones

Las transacciones se usan para agrupar varias consultas en una sola transacción, si una consulta falla, se pueden revertir todas las consultas.

func main() {
  tx, err := db.Begin()
  if err != nil { log.Fatal(err) }

  stmtInvoice, err := tx.Prepare("INSERT INTO invoices (client) VALUES (?)")
  if err != nil { tx.Rollback() }
  defer stmtInvoice.Close()

  invRes, err := stmtInvoice.Exec("Juan")
  if err != nil { tx.Rollback() }

  invID, err := invRes.LastInsertId()
  if err != nil { tx.Rollback() }

  stmtItem, err := tx.Prepare("INSERT INTO invoice_items (invoice_id, product, price) VALUES (?, ?, ?)")
  if err != nil { tx.Rollback() }
  defer stmtItem.Close()

  _, err = stmtItem.Exec(invID, "Product 1", 10)
  if err != nil { tx.Rollback() }

  err = tx.Commit()
  if err != nil { tx.Rollback() }
}

Datos nulos

Para trabajar con datos nulos se puede usar el paquete sql.NullString, sql.NullInt64, sql.NullFloat64, sql.NullBool, sql.NullTime y demás. Se usa para trabajar con valores nulos en la base de datos y se debe crear una variable intermedia para hacer la asignación.

func main() {
  type Product struct { Name string }
  for rows.Next() {
    var nameNull sql.NullString
    p := Product{}
    err := rows.Scan(&nameNull)
    if err != nil {
      log.Fatal(err)
    }
    // si nameNull.Valid es verdadero, quiere decir que nameNull.String tiene un valor
    if nameNull.Valid {
      p.Name = nameNull.String
    }
  }

  // se puede hacer lo mismo trabajando con un puntero
  type Product struct { Name string }
  for rows.Next() {
    var name *string
    p := Product{}
    err := rows.Scan(&name)
    if err != nil {
      log.Fatal(err)
    }
    if name != nil {
      p.Name = *name
    }
  }
}

GoLand IDE

MAC: