Skip to content

Instantly share code, notes, and snippets.

@juanamari94
Last active October 10, 2023 21:09
Show Gist options
  • Save juanamari94/a06fbcc59d646750301d98aa7ca36289 to your computer and use it in GitHub Desktop.
Save juanamari94/a06fbcc59d646750301d98aa7ca36289 to your computer and use it in GitHub Desktop.
Notas GoLang

#Documentación GoLang Español

Detalles

  • Escribo todo al mismo tiempo que nos capacitan para poder reflejar y expresar bien lo que nos dicen los chicos de GrupoEsfera.
  • Me inspiré mucho con los ejemplos y dichos de GoTour, pero traté de complementarlos con el conocimiento de Diego.
  • Separé Variables, Punteros, Estructuras, Slices y Mapas porque me parecen conceptos deberían tener sus propias secciones para no confundir(me).
  • Si me confundo Pointer Receiver y Pointer Retriever, no es mi culpa.

Instalación y variables de Golang

En GoLang todos los proyectos están en sus respectivas carpetas en el mismo lugar: src.

En bin están los archivos ejecutables. Está tanto el "main" como también los tests automatizados.

En pkg están las dependencias utilizadas.

Hay un directorio de la arquitectura con la que está compilando compilando.

Una variable de entorno es una variable que se define en el sistema operativo para caracterizar un valor para que aquellos programas que corran en ese entorno (shell). Comunican el entorno con un programa: GOPATH nos permite saber dónde están las cosas que queremos ejecutar, por ejemplo si se usa go build hello. Si no seteamos GOPATH, no vamos a encontrar ninguna de las herramientas que necesitamos. PATH (conceptual) es una variable que dice dónde están los ejecutables.

Resumiendo:

  • GOPATH es dónde buscar nuestros archivos para compilarlos y ejecutarlos.
  • GOROOT es la instalación de Go.

Un IMPORT PATH es un string que identifica unívocamente a un paquete. Los paquetes de la librería estándar tienen import paths cortos tales como "fmt" y "net/http". Se tiene que elegir un PATH (nombre, entre otras cosas) que sea poco probable que se repita.

###Compilar y correr

Para compilar el código se puede usar cualquiera de los dos siguientes comandos: go install [nombre] o bien go build [nombre]. Si los PATH están bien especificados los binarios van a ir a la carpeta bin. El comando go run [nombre] compila y corre el archivo.

Código

Convenciones

  • Las variables y funciones se escriben en CamelCase. holaComoEstas
  • Las constantes se escriben con el primer caracter en mayúscula. SoyUnaConstante
  • Aquellas funciones y variables que son exportadas deberían tener la primera letra mayúscula (por ejemplo: Hola). SoyUnaFuncionONombreExportada
  • Aquellas funciones y variables que no son exportadas deberían estar en minúscula, siguiendo las convenciones de CamelCase.
  • Los paquetes van todos en minúscula y en lo posible con nombres cortos. github.com/juan/hello
  • Es buena idea declarar las constantes a nivel global.

Convenciones de chic@ bien

  • Es mejor que hayan más líneas pero que se entienda más el código.
  • La lógica de negocio debe ser explícita para terceros y por eso está bueno evitar usar #magitrucos del código para cosas de tanta relevancia.
  • Si buscamos claridad en nuestor código y hay cruces entre los nombres de variables/funciones/etc a utilizar es bueno juntarse y llegar a un concenso en el que se plantea un concepto superador que satisfaga a ambos extremos.

Variables

GoLang es un lenguaje tipado, si bien existen formas en las que se infieren los tipos.

Hay varias formas de declarar variables:

var i int = 1 es una forma explícita de declarar (y asignar) una variable.

i := 1 es otra forma más corta de declarar variables, en la cual el tipo es inferido.

Los tipos de datos básicos son:

bool
string
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32
     // represents a Unicode code point
float32 float64
complex64 complex128

Constantes

Las constantes se declaran como variables, pero se usa la palabra clave const para definirlas. No se pueden declarar con :=.

Un ejemplo sería: const Pi = 20

Punteros

  • Un puntero es una variable que contiene la dirección de memoria de otra.
  • Es un tipo atómico.
  • El tipo *Tes un puntero a T. Su valor 0 es nil.
  • El operador & genera un puntero a su operador. Devuelve su dirección en memoria.
  • No hay aritmética de punteros, pero sí asignación.
i := 42
p = &i // Dirección de memoria donde está guardada i.
fmt.Println(*p) // Referencia al valor de la dirección que estoy almacenando.
*p = 21 // Ahora i pasa a valer 21.
  • & es el operador recíproco: de indirección.
  • * es el operador de "dereferencia", apunta al valor que almacena.

Estructuras

  • Es una colección de campos.
  • Por ende podemos decir que es un valor compuesto, estructurado.
  • Es un valor asociativo: tiene campos a los que le asigna un valor particular.
  • La declaración type declara un nuevo tipo que definimos nosotros. En C era typedef. Es lo mismo.
  • En el bloque de struct (la estructura) se definen las variables que la componen.

Ejemplo de definición de una estructura (struct)

type Vertex struct {
	X int // ↓
	Y int // Valores que componen a la estructura.
}

Ejemplo de uso

import "fmt"

func main() {
	fmt.Println(Vertex{1,2})
	fmt.Println(Vertex{})
}
  • Vertex{} inicializa una variable con los valores por default de los tipos que la componen.
  • Se puede acceder a los valores dentro de la struct de la siguiente manera: [variable].[nombre de la propiedad], por ejemplo: v.X donde v referencia una estructura.
func main() {
	v := Vertex{1,2}
	v.X = 4
	fmt.Println(v.X)
}

Punteros a estructuras

  • Los campos de una estructura pueden ser accedidos mediante punteros.
  • Para acceder al campo X de una estructura cuando tenemos el puntero p a la estructura podemos escribir simplemente p.X, evitándonos usar la referenciación explícita (*p).X
  • Me permiten modificar a qué apunta, es decir, cambiar referencias.

Por ejemplo:

import "fmt"

func main() {
	v := Vertex{1, 2}
	p := &v
	p.X = 2
	fmt.Println(p.X)
}
  • Podemos decir que cuando trabajamos con estructuras los punteros están implícitos.
  • Son internal pointers o punteros internos.

Struct Literals

  • Un valor literal es aquel que lo único que entiende es lo que explicitamente está escrito.
  • La diferencia entre un literal y una variable es que un literal solo tiene su valor. Su valor es inmediato. Las variables tienen un salto, los punteros dos saltos, los literales no tienen. Una variable es p , un literal es 1. Un puntero *p tiene dos saltos para llegar a su valor. p tiene uno solo. 1 no tiene, es bien explícito.

Un ejemplo de diferentes asignaciones:

type Vertex struct {
	X, Y int
}

var (
	v1 = Vertex{1, 2}  // has type Vertex
	v2 = Vertex{X: 1}  // Y:0 is implicit
	v3 = Vertex{}      // X:0 and Y:0
	p  = &Vertex{1, 2} // has type *Vertex
)

Map

  • Los maps son asociativas, tales como las estructuras. Mapean claves a valores.
  • El zero-value de un map es nil. No tiene llaves, y no se pueden agregar keys.
  • La función make devuelve un mapa de un tipo dado.

Un ejemplo de un map:

package main

import "fmt"

type Vertex struct { // Estructura que voy a usar con el map
	Lat, Long float64
}

var m map[string]Vertex

func main() {
	m = make(map[string]Vertex)
	m["Bell Labs"] = Vertex{
		40.68433, -74.39967,
	}
	fmt.Println(m["Bell Labs"])
}

Literales de Map

  • Son como los literales de struct, pero las keys son requeridas. En cierta forma es parecido a JSON.
type Vertex struct {
	Lat, Long float64
}

var m = map[string]Vertex{ // Qué lindo, ¿no?
	"Bell Labs": Vertex{
		40.68433, -74.39967,
	},
	"Google": Vertex{
		37.42202, -122.08408,
	},
}
  • Si Vertex ya es explícito se puede evitar poner `"Bell Labs": Vertex`` Véase:
var m = map[string]Vertex{ // Más lindo aún.
	"Bell Labs": {40.68433, -74.39967},
	"Google":    {37.42202, -122.08408},
}

Mutando Mapas

  • Insertar o actualizar un elemento en un mapa: m[key] = elem
  • Recuperar un elemento de un mapa: elem = m[key]
  • Borrar un elemento de un mapa: delete(m[key])
  • Ver si un valor está dentro de un mapa es una función que retorna dos valores: por un lado, si está el elemento (si no está es nil), y por otro lado true si se encuentra o false si no.

Ejemplo de ver si un valor está dentro de un mapa:

	v, ok := m["Answer"]
	fmt.Println("The value:", v, "Present?", ok)
  • v va a contener el valor si está dentro del mapa, si no es nil.
  • ok va a devolver true si el valor está dentro del mapa, y false si no lo está.

Arrays

  • El tipo [n]T es un array de n elementos del tipo T.
  • La longitud del array es parte del tipo.
  • No se puede modificar su tamaño.
  • Digamos que es estático, al menos en su tamaño.

var a [10]int declara la variable a como un array de diez enteros (integers).

Ejemplo de un array:

package main

import "fmt"

func main() {
	var a [2]string
	a[0] = "Hello"
	a[1] = "World"
	fmt.Println(a[0], a[1])
	fmt.Println(a)

	primes := [6]int{2, 3, 5, 7, 11, 13}
	fmt.Println(primes)
}

Slices

  • Un array tiene un tamaño fijo.
  • En cambio, un slice es una vista flexible y de tamaño dinámico dentro de los elementos de un array.
  • En la práctica los slices son mucho más comunes que los arrays.
  • El tipo []T es un slice con elementos del tipo T.
  • La expresión a[0:5] crea un slice de los primeros cinco elementos del array a.
  • Los slices aportan comportamiento basados en referencia.
  • No almacenan datos, sino que describen una sección del array sobre el que actúan como wrapper. Cambiar los elementos de un slice modifica los elementos correspondientes de éste.
  • Por ende no hacen copias, son referencias.
  • El zero value de un slice es nil y no wrappea ningún array.
  • Se puede hacer un slice de un slice.

Ejemplo:

package main

import "fmt"

func main() {
	primes := [6]int{2, 3, 5, 7, 11, 13}

	var s []int = primes[1:4]
	fmt.Println(s)
}

/* output:
[3 5 7]
Devuelve desde la posición 1 hasta la 3.
"Los valores desde 1 hasta 4".
*/

Diferentes formas de hacer un slice

  • Cuando se hace un slice se pueden omitir los límites que se pueden definir para usar los defaults.
  • El default para el límite inferior es 0 y para el límite superior la longitud del array.

Ejemplo de expresiones slice:

Dado un array var a [10]int

  • a[0:10] → valores desde 0 hasta 10.
  • a[:10] → valores hasta 10.
  • a[0:] → valores desde 0.
  • a[:] → todos los valores.

Longitud y Capacidad de un slice

  • Un slice tiene tanto longitud (length) como capacidad (capacity).
  • La longitud de un slice es el número de elementos que contiene el mismo.
  • La capacidad de un slice es el número de elementos que tiene el array al que referencia.
  • La longitud de un slice se obtiene con len(s).
  • La capacidad de un slice se obtiene con cap(s).

Slice Literal

  • Esto es un array literal: [3]bool(true, true, false).
  • Esto es un slice literal: []bool(true, true, false). Donde crea un array y un slice de ese array. Devuelve el slice.

Ejemplo de varios slices:

import "fmt"

func main() {
	q := []int{2, 3, 5, 7, 11, 13} // Devuelve un slice de ints.
	fmt.Println(q)

	r := []bool{true, false, true, true, false, true} // Devuelve un slice de bools.
	fmt.Println(r)

	s := []struct {
		i int
		b bool
	}{
		{2, true},
		{3, false},
		{5, true},
		{7, true},
		{11, false},
		{13, true},
	} // Todo esto devuelve un slice de tuplas del tipo de la estructura anónima que se declaró.
	fmt.Println(s)
}

Crear slices con make

  • Los slices pueden ser creados con la función make. Así es como se vuelve posible crear arrays de tamaño dinámico.
  • La función make ubica un array "zeroed" y devuelve un slice que referencia a ese array.

a := make([]int, 5) // donde len(a) = 5

Para especificar una capacidad se puede escribir:

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:]      // len(b)=4, cap(b)=4

Agregando a un Slice

  • La función append permite agregar nuevos elementos a un slice.
  • Cumple las expectativas de dinamismo de programador del Siglo XXI.
  • El primer parámetro s de appendes un slice de tipo T, y el resto son valores T a agregar al slice.
  • El valor resultante de appendes un slice que contiene todos los elementos del slice original más los valores provistos.
  • Si el valor que wrappea el slice es demasiado chico para que entren todos los nuevos valores, se alocará un array más grande. El slice retornado va a apuntar al nuevo array.

Ejemplo de append:

package main

import "fmt"

func main() {
	var s []int
	printSlice(s)

	// append works on nil slices.
	s = append(s, 0)
	printSlice(s)

	// The slice grows as needed.
	s = append(s, 1)
	printSlice(s)

	// We can add more than one element at a time.
	s = append(s, 2, 3, 4)
	printSlice(s)
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

Funciones

Las funciones pueden tomar cero o más argumentos (como se podrán imaginar). El tipo de retorno es explícito y pueden retornar más de un valor. Se definen con la palabra clave func.

Por ejemplo: una función que retorna un único valor.

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

Los argumentos se pueden acortar si son del mismo tipo, por ejemplo en la función anterior: func sum(x, y int) int.

Retorno de más de un valor

Hay dos formas de expresar esto, la más explícita es:

func swap(x, y string) (string, string) {
	return y, x
}

y otra en la cual se determina de antemano qué variables se retornarán:

func twoValues(number int) (x, y int) {
	x = number * 4 / 9
	y = number * 3
	return
}

Una declaración return sin argumentos devuelve los valores definidos al principio de la función. A la declaración return sin argumentos se le dice "naked return" o "retorno desnudo" ⚆ _ ⚆.

Funciones como valores

  • Las funciones también son valores y se pueden tratar como tales.
  • Los valores pueden ser usados como argumentos de funciones y como valores de retorno.

Por ejemplo:

import(
	"fmt"
	"math"
)

// Es una función que recibe una función con dos argumentos float64 que duelve un float64. La función que recibe esta función devuelve un float64. Copado, ¿no?
func compute(fn func(float64, float64) float64) float64 {
	return fn(3,4) // Ejecuta la función que le llegó como parámetro.
}

func main() {
	hypot := func(x, y float64) float64 {
		return math.Sqrt(x*x + y*y)
	}
	
	fmt.Println(hypot(5, 12)) // Ejecuta hypot.

	fmt.Println(compute(hypot)) // Pasa la función como parámetro pero no la ejecuta. La ejecución está dentro de la variable compute.
	fmt.Println(compute(math.Pow))
}
  • Algo muy interesante de cómo se trata la función contenida en la variable hypot es su scope. Esta función no se puede exportar y la única forma de llegar a ella es mediante la referencia que mantiene hypot.
  • Este tipo de funciones se usan mucho para tareas que en un único contexto tienen sentido.
  • Un uso más realista para compute podría ser ver el tiempo que tardan las funciones.

Closures (o clausuras)

  • Una clausura o closure es una función que referencia y hace uso de variables fuera de su cuerpo o estructura.
  • La función puede acceder y asignar estas funciones fuera de su cuerpo.

Por ejemplo:

package main

import "fmt"

func adder() func(int) int {
	sum := 0
	return func(x int) int { // Clausura
		sum += x
		return sum
	}
}

func main() {
	pos, neg := adder(), adder() // Referencio
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i), // llamo
			neg(-2*i), // llamo
		)
	}
}
  • La función adder devuelve una clausura. Cada clausura está ligada a su variable sum.

Métodos

  • Go no tiene clases, pero se pueden definir métodos sobre los tipos, las estructuras.
  • Los métodos tienen un argumento especial que va entre medio de su nombre y la declaración de función. A éste argumento se le llama receiver.

Por ejemplo:

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
}
  • Abs() tiene un receiver del tipo Vertex.
  • Son básicamente una función con un argumento más (especial) que es el receiver.
  • Yo puedo evitar el receiver y poner el tipo como un parámetro más siguiente la estructura normal de una función, pero no voy a poner llamar a la función (método en realidad) haceindo type.Method(), sino que va a ser Method(type).
  • Puedo enviar punteros de ese tipo al receiver en vez de el tipo. Esto es importante ya que normalmente se suelen usar más puntero de ese tipo que el valor en sí, ya que los métodos a veces buscan modificar las referencias en vez de usar sus valores.
  • La diferencia entre un pointer receiver y un value receiver es que value receiver utiliza una copia de del valor original, mientras que el pointer receiver utiliza la referencia en sí. Me deja modificar los valores de lo que recibí.
  • Con estructuras se interepreta automáticamente que es un puntero, no hay que hacer & explícito. Solamente funciona con métodos.
  • Si uso el puntero optimizo ya que no hago copias de valores si no que manejo el "valor" que me llegó original.
  • "Si querés una copia, hacetela."
  • "En caso de dudas sobre qué vas a hacer, usá punteros."

Interfaces

  • Una interfaz es un conjunto de firmas de métodos.
  • Permiten implementar el polimorfismo.
  • Un valor del tipo de interface puede contener a cualquier valor que la implemente.
type Abser interface {
	Abs() float64
}

func main() {
	var a Abser
	f := MyFloat(-math.Sqrt2)
	v := Vertex{3, 4}
	a = f
	a = &v
}

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
  • Podemos decir que por atrás el valor de una interfaz tiene una tupla que mapea un valor y un tipo concreto.

Declaraciones de Control de Flujo

For

Sorprendentemente, en Go no existen los ciclos while. Sin embargo, se pueden definir dentro de un for. No usan paréntesis.

Ejemplo de un for genérico:

while := 0 // Dónde está mi while? Acá está.
for i := 0; i < 100; i++ {
	while += i
}

Un for que se comporta como un while:

while := 0 // No planeo dejar de usar variables que se llamen while en estos ejemplos.
for while < 1000 {
	while += 1 // Se está tornando confuso esto.
}

// Por las dudas algo más claro:

sum := 0
for sum < 1000 {
	sum += 1 // Gracias por estar Go.
}

Un for ever (while para los amigos), o un ciclo infinito:

import fmt
for {
	fmt.Println("Extraño mis whiles.")
}

Se pueden hacer asignaciones y definir varios steps en un único for, por ejemplo:

import fmt
for i, j := 0, 10; i < j; i, j -= 1, i += 1 {
// Pensé que la idea era que se entienda más.
	fmt.Pritntln(i, j)
}

Range

  • range es una forma del ciclo for que itera sobre un slice o un mapa (map).
  • En cada iteración range retorna dos valores: el índice, y una copia del valor en ese índice.

Ejemplo:

package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
	for i, v := range pow {
		fmt.Printf("2**%d = %d\n", i, v)
	}
}
  • El ejemplo anterior trabaja con tanto los índices como los valores del slice.
  • Si se desea solamente obtener el valor, y no el índice:
for _, value := range pow {
	fmt.Printf("%d\n", value)
}
  • Si se desea utilizar solamente el índice puede directamente no incluirse value , se saca el , value.
for i := range pow {
	pow[i] = 1 << uint(i) // == 2**i
}

If

Tampoco usan paréntesis (ツ)_/¯.

A continuación pueden ver varios ejemplos de la declaración if, incluyendo if-else, if-elseif .

const number = 1
if number < 1 {
	return -1
} else {
	return 1
}

// o bien

if number < 1 {
	return -1
}
return 1

// o con el milenario else if

if number < 1 {
	return -1 
} else if number > 1 {
	return 1
}
return 0

Switch

switch es muy parecido a como es en otros lenguajes de programación, un ejemplo mostrando sistemas operativos, pero no tiene break:

import (
	"fmt"
	"runtime"
)

os := runtime.GOOS
switch os { // también podíamos ⧸⧸escribirlo así: switch os := runtime.GOOS; os { ... }
case "darwin":
	fmt.Println("OS X.")
case "linux":
	fmt.Println("Linux.")
default:
	// freebsd, openbsd,
	// plan9, windows...
	fmt.Printf("%s.", os)
}

Amo los ejemplos de A tour of Go

Como siempre, switch evalúa desde arriba para abajo, si se usa fallthrough no se sale del switch en un break.

Si bien hay formas muchísimo más elegantes (y en cierta forma más entendibles que usar un switch para ver sistemas operativos), sus ventajas son:

  • Es más rapido.
  • Está todo en un mismo lugar.

Un ejemplo más agradable:

import (
	"fmt"
	"runtime"
)

var osLabel string
switch os := runtime.GOOS; os {
case "darwin":
	osLabel = "OS X"
case "linux":
	osLabel = "Linux"
default:
	osLabel = os
}
fmt.Printf("%s. \n", osLabel)
  • Acá no repetimos código.
  • En el caso default se usa la descripción propia del sistema operativo.
  • No tiene sentido que se repita el switch. Debería estar en una función.
Switch sin condición
  • Un switch sin condición es equivalente a un switch true (no era muy legible eso).
  • Esto puede resultar como una mejor forma de reescribir cadenas if-else muy largas.
import "fmt"

t := time.Now()
switch {
case t.Hour() < 12:
	fmt.Println("Good morning!")
case t.Hour() < 17:
	fmt.Println("Good afternoon.")
default:
	fmt.Println("Good evening.")
}

Defer

  • Marca un statement que se va a ejecutar cuando la función en la que está declarado finalice.
  • Está bueno para hacer cleanup de cosas que necesito cuando está el contexto pero que después se cierre.
  • ¿Cuál es la diferencia entre escribirlo al final y usar defer? Que con defer las cosas se evalúan al principio (donde está la instrucción con defer, aún si se ejecuta después).
  • Vive en el mismo scope de la función que lo contiene.

Ejemplo:

package main

import "fmt"

func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
}

/* output:
hello
world
*/
  • No conviene usarlas mucho para lógica de negocio ya que no es muy explícito a primera vista lo que hacen.
  • Las cosas que no son explícitas en cuanto al orden de ejecución conviene usarlas con cuidado.
Stacking defers (defers apilados)
  • Los defers se pueden apilar en una estructura de datos que conocemos muy bien. Cuando una función retorna, sus llamadas diferidas (deferred calls) son ejecutadas en un orden Last In First Out (LIFO).

Ejemplo:

package main

import "fmt"

func main() {
	fmt.Println("counting")

	for i := 0; i < 10; i++ {
		defer fmt.Println(i)
	}

	fmt.Println("done")
}

/*output:
counting
done 9 8 7 6 5 4 3 2 1 0

# le saqué los \n para ocupar menos espacio.
*/
  • Se puede apreciar que no es tan claro lo que hace, si bien es un ejemplo simple.
  • Es buena idea evitar usar #magitrucos del lenguaje para resolver problemas, para que el código sea entendible para terceros.

Concurrencia

Ejemplo de código: funciones, variables y paquetes

// Package stringutil contains utility functions for working with strings.
package stringutil

// Reverse returns its argument string reversed rune-wise left to right.
func Reverse(s string) string {
	r := []rune(s) // Integers that map strings to their unicode value.
	for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
		r[i], r[j] = r[j], r[i]
	}
	return string(r)
}

En la siguiente línea: for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1se inicializan tanto icomo j, sin embargo hay un detalle: se trabaja como una tupla cuando en realidad no lo es. Esto se traduce en que i tiene la siguiente asignación: i := 0 y j la siguiente: j := len(r)-1.

Si tuviésemos que hacer un mapa de las asignaciones, quedaría así:

i → 0
j → len(r)-1

o bien representado en tuplas:

(i, 	j)
 ↓      ↓
(0, len(r)-1)

Si bien es un poco confuso en esa estructura for, lo mismo ocurre en la línea dentro del ciclo:

r[i], r[j] = r[j], r[i] donde r[i] -> r[j] y r[j] -> r[i]

**Aclaración: ** Nótese que uso → y ↓ para representar una asignación.

Nombres exportados

En Go, los nombres definen si son exportados (provienen de otro paquete), o bien si son internos (propios del paquete actual).

Externos son aquellos cuyo nombre comienza con su primer caracter en mayúscula, por ejemplo: Hola

Internos, en cambio, son aquellos cuyo nombre está completamente en minúscula, por ejemplo: adios

package main

import (
	"fmt"
	"math"
)

func main() {
	fmt.Println(math.Pi)
}

Nótese que tanto Pi como Println, donde muy posiblemente Pi sea una variable ya que es un valor constante y donde Println es una función o método empiezan ambos con su primer caracter en mayúscula.

Tests

GoLang provee una humilde plataforma de testing, cuyo paquete se llama testing. Un código que se utliza para testear la fución anterior para revertir una cadena de caracteres es el siguiente:

package stringutil

import "testing"

func TestReverse(t *testing.T) {
	cases := []struct {
		in, want string
	}{
		{"Hello, world", "dlrow ,olleH"},
		{"Hello, 世界", "界世 ,olleH"},
		{"", ""},
	}
	for _, c := range cases {
		got := Reverse(c.in)
		if got != c.want {
			t.Errorf("Reverse(%q) == %q, want %q", c.in, got, c.want)
		}
	}
}

Veamos la primer sección, donde se declara un array de estructuras.

cases := []struct {
		in, want string
	}{
		{"Hello, world", "dlrow ,olleH"},
		{"Hello, 世界", "界世 ,olleH"},
		{"", ""},
}

Nótese cómo aquí también se manipula cases, que es un array de estructuras y se define sus valores mediante tuplas, tal como en la función.

Un mapeo de esto sería:

in		->	{"Hello, world",
want	->	"dlrow ,olleH"},
in		->	{"Hello, 世界", 
want	->	"界世 ,olleH"},
int		->	{"",
want 	->	 ""}

Ahora vamos a hablar de la otra sección:

for _, c := range cases {
	got := Reverse(c.in)
	if got != c.want {
		t.Errorf("Reverse(%q) == %q, want %q", c.in, got, c.want)
	}
}

La función range en for _, c := range cases devuelve dos valores, donde el primero es irrelevante ya que simplemente queremos el valor de cada tupla.

En got := Reverse(c.in) se llama a la función sobre lo que queremos revertir para testear.

if got != c.want {
	t.Errorf("Reverse(%q) == %q, want %q", c.in, got, c.want)
}

En cada iteración se corrobora si la string invertida provista por la estructura de testo por la función Reverse es igual a lo que se esperaba. De no serlo, el test falla.

SQL con GoLang

  • Tiene una librería para manejar SQL y un driver para cada DBMS distinto.
  • Para 'abrir' la base de datos se usa db, err := sql.Open("mysql", "user:password@/dbname?parseTime=True")
  • "mysql" es el tipo de base de datos que es, la otra línea define el usuario, la contraseña, el nombre de la DB y por último que parsee los DateTime a un tipo de fecha propio de Go.
  • Casi siempre, independientemente del driver (porque nótese que no usamos el driver mysql, lo usa database/sql que es el paquete cuyas funciones nosotros hacemos uso de.
  • Está bueno buscar errores en cada lugar que es probable que devuelva un error, por ende:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)

db, err := sql.Open("mysql", "user:password@/dbname?parseTime=True")

if err != nil {
	panic(err.Error())
}
defer db.Close()
  • La idea es que maneje los errores de forma apropiada y no mediante un panic.
  • db.Open no maneja la conexión propiamente dicha, db.Ping() testea la conexión.
  • El _ en import _ "github.com/go-sql-driver/msql" hace que el paquete sea invisible para nuestro programa, pero lo usa database/sql, por lo que es requerido por atrás.
  • La query la ejecutamos haceindo: rows, err := db.Query("SELECT * FROM table")
  • Con su respectivo manejo de errores.
  • Sin embargo tenemos que iterar para poder recibirlo:
import "time" // No se olviden de esto

type Cliente struct {
		Id int
	Nombre string
	Apellido string
	Categoria string
	FechaNacimiento time.Time
}

var clientes []Cliente
for rows.Next(){
	c := new(Cliente)
	err = rows.Scan(&c.Id, &c.Nombre, &c.Apellido, &c.Categoria, &c.FechaNacimiento)
	if err != nil {
		panic(err.Error())
	}
	append(clientes, *c)
}
  • Lo que se hace en el último bloque de código es "escanear" y "mapear" los resultados de los registros devueltos por la query que hicimos en row, err := db.Query("SELECT * FROM table")
  • Scan recibe punteros a los valores de una estructura a la que queremos mapear los resultados de cada fila.

Insertar Filas

  • Para insertar filas las sentencias son un poco diferentes.
  • Vamos a crear una función aparte para chequear por errores (que en realidad deberían ser específicas para cada error) porque es un poco insoportable verlos en todos lados.
func checkForErrors(err error) {
	if err != nil {
		panic(err.Error())
	}
}

func AddCliente(cliente Cliente) {
	query, err := db.Prepare("INSERT INTO Cliente (nombre, apellido, categoria, fechaNacimiento) VALUES (?, ?, ?, ?)")
	checkForErrors(err)
	_, err = query.Exec(cliente.Nombre, cliente.Apellido, cliente.Categoria, cliente.FechaNacimiento)
	checkForErrors(err)
}
  • Query.Exec ejecuta la query y es donde se pasan los parámetros que se ubican en los placeholders la parte de la inserción (VALUES (?, ?, ?, ?)), db.Prepare (que se asigna a query) se hace antes para definir cómo será la query.
  • UPDATE y DELETE son muy similares.

Gin-gonic

  • Es un framework muy simple para crear aplicaciones web.
  • La verdad es que la documentación (al final del documento) es mucho mejor para explicar cómo funciona, pero...
  • Es un framework para aplicaciones web que usa (naturalmente) el protocolo HTTP y sus verbos.
  • Se pueden hacer tanto sitios webs como APIs.
  • Los verbos HTTP son: GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE
  • No tiene un framework fuerte encima, tal como lo es MVC, pero lo podemos codear.
  • La forma en que se acceden a los recursos es mediante rutas mapeadas a respectivos verbos HTTP.
  • Miren la docu que es re clara.

##Paquetes

Es un concepto central para hacer sistemas grandes, a diferencia de C que tenía namespaces limitados.

Todos los fuentes comienzan con la palabra clave package. Por ejemplo, package name. Para importar hacer uso de sus funcionalidades debo referenciar al paquete, tal como name.operation. Dicho de otra manera, el nombre del paquete se usa para acceder a las funcionalidades contenidas dentro de sí.

La convención de nombrado de paquetes dicta lo siguiente:

  • El nombre del archivo debe ser el último elemento del import path. El nombre del paquete debe ser el mismo que el directorio en el que está, tal como java.

  • Los comandos ejecutables siempre tienen que declarar el package main

  • No es necesario que los nombres de los paquetes difieran siempre y cuando el import path sea único.

Fuentes y Recursos

@NatalyClaret
Copy link

Gracias por el contenido que publicaste, estoy buscando información sobre como solucionar problemas de N+1 querys en GoLang, ¿tendrás alguna información?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment